<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <title>开源资源分享</title>
    <link>http://www.yanzuoguang.com</link>
    <description>开源资源分享</description>
    <language>zh-CN</language>
    <item>
      <title>java开源邮箱服务器</title>
      <link>/article/1561</link>
      <content:encoded>&lt;h1&gt;java开源邮箱服务器&lt;/h1&gt; &lt;p&gt;Java开源邮箱服务器的选择有很多，比如Apache James、OpenJWebMail、JBoss Mail等。以下是一个使用Apache James实现的简单示例：&lt;/p&gt; &lt;p&gt;首先，确保你的机器上安装了Java，并且JAVA_HOME环境变量已经设置。&lt;/p&gt; &lt;ol&gt; &lt;li&gt;下载并解压Apache James服务器：&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;你可以从官方网站下载最新版本：http://james.apache.org/download.cgi&lt;/p&gt; &lt;ol start="2"&gt; &lt;li&gt;配置James服务器：&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;编辑&lt;code&gt;conf/james.properties&lt;/code&gt;文件，配置SMTP服务器和邮件存储路径。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;# 配置SMTP服务器 mailet.default.encoding=UTF- # 配置邮件存储路径 repository.folder=var/mail/ &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;启动James服务器：&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;在James安装目录下运行以下命令启动服务器：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;bin/run.sh &lt;/code&gt;&lt;/pre&gt; &lt;ol start="4"&gt; &lt;li&gt;配置James服务器允许远程连接：&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;编辑&lt;code&gt;conf/smtpserver.xml&lt;/code&gt;文件，将&lt;code&gt;servername&lt;/code&gt;设置为&amp;quot;0.0.0.0&amp;quot;，这样就可以允许远程连接。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;server&amp;gt;   &amp;lt;smtpserver&amp;gt;     &amp;lt;port&amp;gt;25&amp;lt;/port&amp;gt;     &amp;lt;authRequired&amp;gt;false&amp;lt;/authRequired&amp;gt;     &amp;lt;servername&amp;gt;0.0.0.0&amp;lt;/servername&amp;gt;   &amp;lt;/smtpserver&amp;gt; &amp;lt;/server&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="5"&gt; &lt;li&gt;使用Java代码发送邮件：&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;使用JavaMail API发送邮件。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;import javax.mail.*; import javax.mail.internet.*;   public class SendEmail {     public static void main(String[] args) {         try {             Properties props = new Properties();             props.put(&amp;quot;mail.smtp.host&amp;quot;, &amp;quot;localhost&amp;quot;);             props.put(&amp;quot;mail.smtp.port&amp;quot;, &amp;quot;25&amp;quot;);               Session session = Session.getInstance(props, null);               Message msg = new MimeMessage(session);             msg.setFrom(new InternetAddress(&amp;quot;from@example.com&amp;quot;));             InternetAddress[] address = {new InternetAddress(&amp;quot;to@example.com&amp;quot;)};             msg.setRecipients(Message.RecipientType.TO, address);             msg.setSubject(&amp;quot;Hello&amp;quot;);             msg.setText(&amp;quot;This is a test mail&amp;quot;);               Transport.send(msg);             System.out.println(&amp;quot;Sent successfully&amp;quot;);         } catch (Exception ex) {             ex.printStackTrace();         }     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;确保你的邮箱服务器运行中，并且根据实际情况修改&lt;code&gt;props.put&lt;/code&gt;中的主机地址和端口。&lt;/p&gt; &lt;p&gt;以上是一个简单的使用Apache James的例子。根据你的具体需求，你可能需要进一步配置和调整James服务器。&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 10 Oct 2024 06:00:19 GMT</pubDate>
    </item>
    <item>
      <title>docker同机器不同network通讯</title>
      <link>/article/1560</link>
      <content:encoded>&lt;h1&gt;docker同机器不同network通讯&lt;/h1&gt; &lt;p&gt;在Docker中，如果你想让位于同一台机器上但属于不同Docker网络的容器能够通讯，你需要确保网络之间能够互相访问。Docker提供了默认的&lt;code&gt;bridge&lt;/code&gt;网络，但如果你创建了额外的网络，容器之间默认不能直接通讯，除非你显式地配置它们。&lt;/p&gt; &lt;p&gt;以下是一个简单的示例，演示如何设置两个容器在同一台机器上通过不同的Docker网络进行通讯：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;创建两个Docker网络：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;docker network create --driver bridge my_network1 docker network create --driver bridge my_network2 &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;启动两个运行在不同网络的容器：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;docker run --network my_network1 -d --name container1 busybox /bin/sh -c &amp;quot;while true; do sleep 3600; done&amp;quot; docker run --network my_network2 -d --name container2 busybox /bin/sh -c &amp;quot;while true; do sleep 3600; done&amp;quot; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;使用&lt;code&gt;docker exec&lt;/code&gt;进入其中一个容器，并尝试ping另一个容器：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-powershell"&gt;docker exec -it container1 sh / # ping container2 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果你按照上面的步骤操作，你会发现无法ping通另一个容器，因为它们位于不同的Docker网络中。为了让它们可以通讯，你需要在网络之间添加路由规则或者使用用户定义的桥接网络。&lt;/p&gt; &lt;ol start="4"&gt; &lt;li&gt;为容器配置静态路由（不推荐，不同网络间通常应通过网络设计或服务发现机制进行通信）：&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;在一个网络中的容器执行以下命令，添加到另一网络的静态路由：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;/ # route add -net 172.20.0.0 netmask 255.255.0.0 gw &amp;lt;另一网络容器的IP&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;请注意，上述命令中的&lt;code&gt;172.20.0.0/16&lt;/code&gt;是Docker默认分配的网段，如果你的网络配置与此不同，需要相应地修改。&lt;/p&gt; &lt;ol start="5"&gt; &lt;li&gt;使用桥接网络，确保容器能够在同一台机器上的不同网络间通讯：&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;docker network connect bridge container2 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这样，&lt;code&gt;container2&lt;/code&gt;将能够通过&lt;code&gt;bridge&lt;/code&gt;网络访问&lt;code&gt;container1&lt;/code&gt;，因为Docker的&lt;code&gt;bridge&lt;/code&gt;网络是所有容器默认连接的。&lt;/p&gt; &lt;p&gt;总结，要使同一台机器上的不同Docker网络中的容器能够通讯，你需要确保网络之间互相可达，或者将容器配置到共享的网络（如&lt;code&gt;bridge&lt;/code&gt;网络）中。&lt;/p&gt;</content:encoded>
      <pubDate>Wed, 09 Oct 2024 08:09:00 GMT</pubDate>
    </item>
    <item>
      <title>美国国债解决方案</title>
      <link>/article/1559</link>
      <content:encoded>&lt;p&gt;今天是2024-6-21日，今天主要聊聊美国危机，美国股市和虚拟币，针对最近美国危机主要有如下几点：&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;美国国债34万亿，利息非常高，其中美国自己持有自己的国债27万亿，也就是真正的外国国债只有7万亿，这7万亿可能会导致美国和其他国家发生战争。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;美国民众不再信任美国政府，最主要是因为战争和穷两点，穷是因为利益，可以通过钱来解决，反战的其实只有部分人，完全可以通过暴力镇压下去。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;美国昂撒财阀和犹太财阀的利益和政治博弈问题，这个问题其实根本原因还是利益，但是他们已共存100多年了，也不在乎多几年，最主要是解决了眼前的利益问题：第一个是两者互相吞掉对方，就会解决美国的问题；第二个是战争该如何打，打完以后的利益该如何分配，这也是美国财阀扶持美国民众闹事的根本原因，也是利益争夺的手段，昂撒要更多的利益。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;美国州独立问题，这本质上还是利益问题，可以通过钱来解决。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;美国制造业空心化的问题，这需要美国大力扶持本土制造业，以及降低人员的工资，但是目前美国民众不愿意干低薪的工作，并且环境保护协会等，所以这需要美国民众愿意，只有打击民众，让民众尝试到更苦的，美国才会愿意去干这些低薪的工作，也需要通过钱来扶持制造业，并且解决需要时间，假如不能解决还是会爆发上面的问题。制造业空心化可以通过生产军火来扶持，生产其他产品会面临中国的商业竞争，只有生产军火中国不会造成竞争问题，所以美国需要一场战争，从而解决制造业空心化的问题。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;美国金融霸权是否能够持续下去，全球去美元趋势强烈，美元和中东石油绑定，目前中东多国不愿意再被美国摘桃子，但是美国通过通过军事霸权来维稳和控制中东国家，以色列是美国控制中东的手段，所以中东国家围攻以色列，美国之所以没有反击，一是因为美国没钱了，二是等待所有敌人全部跳出来，三是派兵需要时间，这也是最近美国派兵到中东还没有动手的真正原因。并在战争胜利后，通过军事霸权强制推行金融霸权。一旦战败，则美国都没了。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;和俄罗斯的乌克兰问题，中国台海问题，这只是美国拖延中俄的注意力的手段，防止中俄全线支持中东，这也是中东诸国将文物存在中国的原因，因为中国看透了，台海根本就不会爆发战争，除非美国疯了；这里的关键是中俄是否会给时间让美国解决问题，趁机把战争扩大，但是即使扩大，受难的也是美国民众，对美国资本则没有损害，反而可以趁机发更多的战争财，美国资本完全可以分到钱以后撤离。&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;p&gt;如上这些问题，是美国最主要的问题，下面来讨论该如何解决钱的问题，那么美国哪里有钱呢？有钱的地方有：&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;虚拟币：美国把握住了赵长鹏，赵是虚拟币平台交易的控制人，他能对赵下手，也就能对虚拟币平台下手，从而将虚拟币全部国有化，战争阶段散户是不能对美国形成战争压力的，所以美国完全可以无视这部分压力，从而国有化虚拟币平台。虚拟币平台10万亿美元，一旦国家化，美国完全可以解决1/4的债务问题。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;其他国家的美国货币：美国通过加息，让美金回流美国银行，从而造成其他国家资产贬值，当美国降息时美元再次回流第三方国家时，可以购买更便宜的资产，从而推动美元升值，于是美元实际能挂钩的物资价值变多了；让其他国家美国资本缺少美金流动，当需要进口粮食等必须品时，购买不到从而造成本币贬值，然后必须向美国借钱，美国货币升值，然后美元回流实时购买更多的资产，并可以趁机提出购买矿石企业等等。目前美国只加息到5%，是因为美国觉得够了，不需要加息到以前收割时候的11%。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;美联储：美联储可以印刷纸币，也可以直接在银行增加虚拟数字美元，并将数字虚拟货币借钱给美国银行，而后回收虚拟数字美元，只要控制虚拟数字币在股市，没有流向市场，则不会造成实际资产价值下降；美联储完全可以增加20万亿美国资产，推高股市价格，并在高位套现离开，从而降低市场上存在的美元货币。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;美国银行：美国银行会承受一波加息压力，当承受不住时，选择破产倒闭，从而将美元留在美国，降低了市场上流通的美元，造成美国货币升值；并且美国可以将钱投向股市，推高股票价值，并在高价卖出股票套现，从而将价值回流，将世界上存在的美元减少。并且美国银行回收和美国战争的国家货币，反正战胜则啥都有，战败则啥都没有了。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;美国股市：美国通过控制美国股灾来减少市场上流通的货币，比如说原来美国股市总价值为45万亿美元，股灾后总价值为5万亿美元，贬值85%以上，并在低价回购股票，则市场上可流通的货币减少了40万亿美元，这40万亿美元可以用于偿还国债，并还多余4万亿美元。只要美国银行在高价位抛售，并稍后两天宣布降息，美国股市将形成抛售潮，并形成股灾，那么股票贬值只会超过85%。然后再推动美国股市升值，进一步降低流通的美元，比如说升值到15万亿，则市场上货币量减少了50万亿。&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;中东的石油：美国通过股市和虚拟币可以回收美元总价值50亿美元以上，全部偿还完美国国债后，都还剩余14万亿美元，14万亿美元足够美国打赢以色列战争，打赢则可以获取到中东国家的石油开采，这里有价值500万亿的石油，足够等美国完全解决美国制造业空心化的问题。&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;p&gt;总体来说有3个手段：减少市场上流通的美元，让美元绑定的资产增加，通过战争解决空心化增加美国民众手里的钱。最终结论：美国战争胜利了，则啥都有了；战争失败了，受苦的是美国民众，美国资本都跑路了。&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 21 Jun 2024 03:44:00 GMT</pubDate>
    </item>
    <item>
      <title>股市印花税建议</title>
      <link>/article/1558</link>
      <content:encoded>&lt;p&gt;建议股市1天同一只股票同一个人交易1-10次以内印花税为正常税收，交易10-30次翻5倍，交易30次-50次再翻10倍，交易50次以上为10%.&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 25 Mar 2024 09:31:00 GMT</pubDate>
    </item>
    <item>
      <title>股票分红登记日查询方法详解（让你轻松掌握分红最佳时间）</title>
      <link>/article/1557</link>
      <content:encoded>&lt;p&gt;股票分红是股票投资者所期待的好消息，它代表着公司利润的分配，也是股票投资者获得回报的重要方式。但是，要想在分红的时候获得化的收益，就需要掌握分红登记日的查询方法。下面，我们就来详细了解一下。&lt;/p&gt; &lt;p&gt;一、什么是分红登记日？&lt;/p&gt; &lt;p&gt;分红登记日是指股票公司在公布分红方案后，股票交易所规定的一个截止日期。这个日期之前，股票投资者需要持有股票才能享受到分红的权益，否则就无法获得分红。一般来说，分红登记日前是股票的除权日。&lt;/p&gt; &lt;p&gt;二、分红登记日查询方法&lt;/p&gt; &lt;p&gt;1.股票公司公告&lt;/p&gt; &lt;p&gt;股票公司会在公告中公布分红方案，其中会明确分红的金额、分红的时间、分红的比例以及分红登记日等信息。因此，股票投资者可以通过查看股票公司的公告来了解分红登记日。&lt;/p&gt; &lt;p&gt;2.股票交易所查询&lt;/p&gt; &lt;p&gt;股票交易所也会在其官网上公示分红相关的信息，包括分红方案、分红金额、分红登记日等。股票投资者可以通过股票交易所的官网来查询分红登记日。&lt;/p&gt; &lt;p&gt;3.财经媒体报道&lt;/p&gt; &lt;p&gt;财经媒体也会报道股票分红的相关信息，包括分红方案、分红登记日等。股票投资者可以通过财经媒体来了解分红登记日。&lt;/p&gt; &lt;p&gt;三、如何选择的分红登记日？&lt;/p&gt; &lt;p&gt;在了解了分红登记日的查询方法后，股票投资者还需要掌握如何选择的分红登记日。一般来说，分红登记日距离分红支付日越近，股票的价格就越高。因此，股票投资者可以在分红登记日前买入股票，然后在分红支付日后将股票出售，以获得化的收益。&lt;/p&gt; &lt;p&gt;总之，对于股票投资者来说，掌握分红登记日的查询方法以及如何选择的分红登记日是非常重要的。希望本篇能够帮助股票投资者更好地了解分红登记日，从而实现更好的。&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 07 Jul 2023 07:17:00 GMT</pubDate>
    </item>
    <item>
      <title>A股：分红最慷慨的上市公司名单！</title>
      <link>/article/1556</link>
      <content:encoded>&lt;h1&gt;A股：分红最慷慨的上市公司名单！&lt;/h1&gt; &lt;p&gt;本期文章给大家分享A股分红最慷慨的上市公司十强，评估指标为：&lt;strong&gt;分红融资比例&lt;/strong&gt;（就是用历史分红总额除以实际融资金额，得出比值进行排名）&lt;/p&gt; &lt;p&gt;**这十家公司如下：&lt;/p&gt; &lt;p&gt;**&lt;/p&gt; &lt;p&gt;1.贵州茅台 分红总额：1480.23亿 融资金额：22.44亿 分红融资比65.95&lt;/p&gt; &lt;p&gt;2.宁沪高速 分红总额：348.62亿 融资金额：6.3亿 分红融资比42.03&lt;/p&gt; &lt;p&gt;3.江铃汽车 分红总额：106.51亿 融资金额：3.18亿 分红融资比33.54&lt;/p&gt; &lt;p&gt;4.山西汾酒 分红总额：75.51亿 融资金额：2.26亿 分红融资比33.34&lt;/p&gt; &lt;p&gt;5.福耀玻璃 分红总额：220.21亿 融资金额：7.13亿 分红融资比26.40&lt;/p&gt; &lt;p&gt;6.兖州能源 分红总额：445.05亿 融资金额：12.7亿 分红融资比21.31&lt;/p&gt; &lt;p&gt;7.格力电器 分红总额：1009.28亿 融资金额：52.69亿 分红融资比19.15&lt;/p&gt; &lt;p&gt;8.五粮液 分红总额：613.31亿 融资金额：38.15亿 分红融资比16.08&lt;/p&gt; &lt;p&gt;9.德赛电池 分红总额：9.9亿 融资金额：0.69亿 分红融资比14.29&lt;/p&gt; &lt;p&gt;10.达安基因 分红总额：22.91亿 融资金额：1.61亿 分红融资比14.26&lt;/p&gt; &lt;p&gt;&lt;strong&gt;除了以上榜单，按照历史分红总金额排名前五甲为：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;工商银行：11952.27亿（为分红总金额之最）&lt;/p&gt; &lt;p&gt;中国石油 ：6234亿&lt;/p&gt; &lt;p&gt;中国石化：5184亿&lt;/p&gt; &lt;p&gt;中国神华：2824.97亿&lt;/p&gt; &lt;p&gt;中国人寿：1758.02亿&lt;/p&gt; &lt;p&gt;补充：分红是基于上市公司历年数据汇总得出，并不代表有绝对的投资收益，我们在选择入手一只股票之前还需关注该公司未来发展趋势，所处行业的大背景，是否具备稳定性 成长性，业绩预期能否被市场逐一验&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 07 Jul 2023 02:51:00 GMT</pubDate>
    </item>
    <item>
      <title>送转股是什么意思</title>
      <link>/article/1555</link>
      <content:encoded>&lt;h1&gt;送转股是什么意思&lt;/h1&gt; &lt;p&gt;送转股是送股和转股和合称。送股是指股份公司对原有股东采取无偿派发股票的行为。送股时，将上市公司的留存收益转入股本账户，留存收益包括盈余公积和未分配利润。转股是上市公司分红的一种形式，是采取从资本公积金中转增股份的办法分红的方式。&lt;/p&gt; &lt;p&gt;送转股的基础是利润或资本公积。资本公积是投资者投入资产较取得的对应股本的溢价。如果以现金分红，不管是被投资企业以资本公积还是以利润向投资者进行分配原则上都不会涉及增值税。（假如政策允许以资本公积分红。）事情往往不是这么简单，送转股后投资者得到的并非现金，而是股票，股票就有被出售的可能，因此就产生了今天要谈到的问题，送转股转让成本价怎么确定？表面上看，投资者取得送转股并未支付对价，取得成本为0，但一页知税认为投资者的取得成本并非为零，而是被投资企业资本公积和留存收益中投资者对应应享受的部分。因为，送转股后被投资企业资本公积或者留存收益账户金额会进行相应减少，投资者享受的份额的价值也会减少。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;由限售股产生的送转股的取得成本&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;对于限售股产生的送转股的取得成本总局公告2016年第53号中做了明确规定。《国家税务总局关于营改增试点若干征管问题的公告》（国家税务总局公告2016年第53号）规定，单位将其持有的限售股在解禁流通后对外转让的，按照以下规定确定买入价：&lt;/p&gt; &lt;p&gt;1、上市公司实施股权分置改革时，在股票复牌之前形成的原非流通股股份，以及股票复牌首日至解禁日期间由上述股份孳生的送、转股，以该上市公司完成股权分置改革后股票复牌首日的开盘价为买入价。&lt;/p&gt; &lt;p&gt;2、公司首次公开发行股票并上市形成的限售股，以及上市首日至解禁日期间由上述股份孳生的送、转股，以该上市公司股票首次公开发行（IPO）的发行价为买入价。&lt;/p&gt; &lt;p&gt;3、因上市公司实施重大资产重组形成的限售股，以及股票复牌首日至解禁日期间由上述股份孳生的送、转股，以该上市公司因重大资产重组股票停牌前一交易日的收盘价为买入价。&lt;/p&gt; &lt;p&gt;可见，不管是何种方式产生的送转股，总局均没有按照投资者表面上的支付对价0作为取得成本。这正体现了买入价不单单看支付对价的思路，同时也符合风险投资收益不征收增值税的原则。这一原则在财税〔2016〕140号文件，金融商品持有期间（含到期）取得的非保本收益，不征收增值税的规定中也有体现。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;由流通股产生的送转股的取得成本&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;相对于限售股而言，由流通股产生的送转股的取得成本总局无明确规定，业内也有很大的争议。有人认为，由于投资者未支付对价，转让计算销售额时买入价就是0。一页知税并不这么认为，流通股产生的送转股与限售股产生的送转股本质上并没有区别，买入价按0计算一不符合总局对送转股买入价的定价思路；二不符合风险投资收益不征收增值税的基本原则；第三限售股和流通股定价方式不同还造成税收不公平。一页知税认为，送转股前后投资者享受的权益并未增加，取得股票数量增加是对原持有股票的稀释，送转股买入价应参考被稀释股票下降的价值。实际业务中我们可以把送转股看作是取得现金分红和再投资两个步骤，分红金额就是股东大会作出送转决定当天的收盘价乘以持股数量，而在投资取得股票买入价等于分红金额除以股票增加数量，实际上就是当日收盘价。那么该部分送转股转让时，计税基础=转让金额-股东大会作出送转决定日收盘价*取得送转股数量。这样既保证了税收公平也保证了股息红利不征收增值税的原则得以落实。&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 07 Jul 2023 02:08:00 GMT</pubDate>
    </item>
    <item>
      <title>魔兽80级怀旧服</title>
      <link>/article/wow</link>
      <content:encoded>&lt;p&gt;魔兽新开测试服务器，欢迎大家注册测试。游戏程序可在如何连接中下载。&lt;/p&gt; &lt;p&gt;&lt;a href="http://wow.yanzuoguang.com:9000" target="_blank"&gt;http://wow.yanzuoguang.com:9000&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Wed, 21 Dec 2022 09:28:00 GMT</pubDate>
    </item>
    <item>
      <title>缓存应用案例分析</title>
      <link>/article/1553</link>
      <content:encoded>&lt;h2&gt;为什么需要缓存&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;降低数据库查询压力&lt;/li&gt; &lt;li&gt;提前准备好结果数据,从而提升服务器性能&lt;/li&gt; &lt;li&gt;幂等性判断&lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;什么数据需要缓存&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;列表信息&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;变动不频繁列表,如: 员工列表&lt;/li&gt; &lt;li&gt;变动频繁的列表,如: 库存列表&lt;/li&gt; &lt;/ul&gt; &lt;ol start="2"&gt; &lt;li&gt;单个信息通过主键缓存&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;加载产品信息&lt;/li&gt; &lt;li&gt;加载渠道信息&lt;/li&gt; &lt;/ul&gt; &lt;ol start="3"&gt; &lt;li&gt;多服务器部署中,幂等性任务处理&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;订单处理中对订单进行缓存,并防止同时下单&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;环境准备工作&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;配置JetCache运行环境配置文件&amp;quot;tbd-redis.yml&amp;quot;:&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;# 端口 #spring: #  redis: #    # redis服务器地址（默认为loaclhost） #    host: 192.168.0.206 #    # redis端口（默认为6379） #    port: 6379 #    # redis数据库索引(默认为0)，避免和其他应用冲突，途比达序号为8，胖丁序号为6，数据中心序号为7 #    database: 8 #    # redis访问密码（默认为空） #    password: root,.123 #    #超时连接 #    timeout: 1000ms #    jedis: #      pool: #        #最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 #        max-wait: 1000ms #        #最大连接数据库连接数,设 0 为没有限制 #        max-active: 100 #        #最大等待连接中的数量,设 0 为没有限制 #        max-idle: 8 #        #最小等待连接中的数量,设 0 为没有限制 #        min-idle: 0  remoteCache: &amp;amp;remoteCache   #  type: redis   #  keyConvertor: fastjson   #  valueEncoder: kryo   #  valueDecoder: kryo   #  host: ${spring.redis.host:localhost}   #  port: ${spring.redis.port:6379}   #  password: ${spring.redis.password}   #  database: ${spring.redis.database}   #  poolConfig:   #    minIdle: 5   #    maxIdle: 20   #    maxTotal: 50   type: redis.lettuce   keyConvertor: fastjson   valueEncoder: kryo   valueDecoder: kryo   # uri格式：redis://密码@ip:端口/redis库名?timeout=5s  url[0]   # uri: redis://${spring.redis.password}@${spring.redis.password}:${spring.redis.port:6379}/7?timeout=5s   # redis数据库索引(默认为0)，避免和其他应用冲突，途比达序号为8，胖丁序号为6，数据中心序号为7   # uri格式：redis://密码@ip:端口/redis库名?timeout=5s   uri: redis://root,.123@192.168.100.4:6379/8?timeout=1s   poolConfig:     minIdle: 0     maxIdle: 8     maxTotal: 100  localCache: &amp;amp;localCache   type: caffeine   keyConvertor: fastjson   jetcache:   statIntervalMinutes: 15   areaInCacheName: false   local:     # 默认1小时本地缓存     default:       &amp;lt;&amp;lt;: *localCache       expireAfterWriteInMillis: 60000       expireAfterAccessInMillis: 60000     # 長時本地緩存，主要用于要求时效一般     longTime:       &amp;lt;&amp;lt;: *localCache       expireAfterWriteInMillis: 300000       expireAfterAccessInMillis: 180000     # 短時本地緩存，主要用于要求时效较高的配置     shortTime:       &amp;lt;&amp;lt;: *localCache       expireAfterWriteInMillis: 3000       expireAfterAccessInMillis: 3000   remote:     # 默认1小时的远程缓存     default:       expireAfterWriteInMillis: 3600000       &amp;lt;&amp;lt;: *remoteCache     # 长时远程緩存，主要用于要求时效要求一般的集中式缓存     longTime:       expireAfterWriteInMillis: 7200000       &amp;lt;&amp;lt;: *remoteCache     # 短時远程緩存，主要用于要求时效较高的集中式缓存     shortTime:       expireAfterWriteInMillis: 300000       &amp;lt;&amp;lt;: *remoteCache &lt;/code&gt;&lt;/pre&gt; &lt;ol start="2"&gt; &lt;li&gt;pom.xml中引入jar包&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-xml"&gt;&amp;lt;dependency&amp;gt;   &amp;lt;groupId&amp;gt;com.yanzuoguang&amp;lt;/groupId&amp;gt;   &amp;lt;artifactId&amp;gt;yzg-util-redis&amp;lt;/artifactId&amp;gt; &amp;lt;/dependency&amp;gt; 或者 &amp;lt;dependency&amp;gt;   &amp;lt;groupId&amp;gt;com.alicp.jetcache&amp;lt;/groupId&amp;gt;   &amp;lt;artifactId&amp;gt;jetcache-starter-redis-lettuce&amp;lt;/artifactId&amp;gt; &amp;lt;/dependency&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;项目中 &amp;quot;bootstrap.yml&amp;quot; 增加 引入配置文件&amp;quot;tbd-redis.yml &amp;quot;&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;spring:   cloud:     config:       name: tbd-redis &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;列表信息缓存&lt;/h2&gt; &lt;p&gt;一般来说列表信息缓存信息都需要时效处理,这种缓存不需要删除,但是缓存时间不能太长(建议最长1-5分钟),但是需要定时失效.并通过整个对象转换为Json字符串后MD5,来确定是否自动重新刷新.&lt;/p&gt; &lt;p&gt;实现方式:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/** * 查询分销渠道 * * @param req * @return */ @Cached(   // 默认规则名称:缓存时效规则，以及存储方式   area=&amp;quot;shortTime&amp;quot;,   // 缓存名称,通常定义到缓存常量中,防止缓存名称冲突   name = CacheName.SALE_SALE_QUERY,    // 缓存关键字,用于做唯一性判断，可以不配置，则由框架自动进行key值生成   key = &amp;quot;#req.toJsonMd5()&amp;quot;,    // 结果为空时是否缓存   cacheNullValue = true,    // 同时缓存到redis，内存中(降低redis压力),可以分别配置redis缓存时间和   cacheType = CacheType.BOTH ,   // 本次配置时效单位．注意：配置文件中的值和这个单位无关.   timeUnit=TimeUnit.SECONDS,   // 本地内存缓存失效时间，可以单独配置，一般通过area属性来控制   localExpire = 3,    // Redis缓存失效时间，可以单独配置，一般通过area属性来控制   expire = 3 ) // 开启自动刷新 @CacheRefresh(   // 自动刷新时间   refresh = CacheName.REFRESH_TIME,    // 不访问后多长时间停止刷新   stopRefreshAfterLastAccess = CacheName.REFRESH_STOP ) // 开启多线程缓存保护,即 @Cached.key 相等时则不缓存 @CachePenetrationProtect PageSizeData&amp;lt;SaleSaleLoadResVo&amp;gt; query(SaleSaleQueryReqVo req); &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;单个信息缓存&lt;/h2&gt; &lt;p&gt;一般来说单个信息缓存信息都可以保留较长时效,这种缓存需要在增加，修改时自动删除,缓存可以设置较长时间(建议最长30-60分钟),可以支持定时刷新，从而提升加载速度．并开启多线程保护．多个请求时，只一个人去加载．这个通过主键加载时不能通过扩展参数返回２种以上的不同结果．&lt;/p&gt; &lt;p&gt;实现方式:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;/** * 查询分销渠道 * * @param req * @return */ @Cached(   // 默认规则名称:缓存时效规则，以及存储方式,在配置文件中定义，默认为: default   area=&amp;quot;default&amp;quot;,   // 缓存名称,通常定义到缓存常量中,防止缓存名称冲突   name = CacheName.SALE_SALE,    // 缓存关键字,用于做唯一性判断,单个缓存中必须配置.   key = &amp;quot;#req.saleId&amp;quot;,    // 结果为空时是否缓存   cacheNullValue = true,    // 同时缓存到redis，内存中(降低redis压力),可以分别配置redis缓存时间和   cacheType = CacheType.BOTH  ) // 开启自动刷新 @CacheRefresh(   // 自动刷新时间   refresh = CacheName.REFRESH_TIME,    // 不访问后多长时间停止刷新   stopRefreshAfterLastAccess = CacheName.REFRESH_STOP ) // 开启多线程缓存保护,即 @Cached.key 相等时则不缓存 @CachePenetrationProtect SaleSaleLoadResVo load(SaleSaleLoadReqVo req);  /** * 分销渠道规则保存/修改 * * @param req * @return */ // 强制现有缓存失效 @CacheInvalidate(   // 默认规则名称:缓存时效规则，以及存储方式,在配置文件中定义，默认为: default   area=&amp;quot;default&amp;quot;,   // 缓存名称   name = CacheName.SALE_SALE,    // 缓存主键，得和 @Cached.key 相等   key = &amp;quot;#req.saleId&amp;quot; ) String save(SaleSaleSaveReqVo req);  /**  * 删除分销信息  *  * @param req  * @return  */ // 强制现有缓存失效 @CacheInvalidate(   // 默认规则名称:缓存时效规则，以及存储方式,在配置文件中定义，默认为: default   area=&amp;quot;default&amp;quot;,   // 缓存名称   name = CacheName.SALE_SALE,    // 缓存主键，得和 @Cached.key 相等   key = &amp;quot;#req.saleId&amp;quot; ) int remove(SaleSaleRemoveReqVo req);  /** * 渠道上下架 * * @param req * @return */ // 强制现有缓存失效 @CacheInvalidate(   // 默认规则名称:缓存时效规则，以及存储方式,在配置文件中定义，默认为: default   area=&amp;quot;default&amp;quot;,   // 缓存名称   name = CacheName.SALE_SALE,    // 缓存主键，得和 @Cached.key 相等   key = &amp;quot;#req.saleId&amp;quot; ) String inline(SaleSaleInlineReqVo req); &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;多服务器部署中,幂等性任务处理&lt;/h2&gt; &lt;p&gt;这种一般用于判断请求是否重复，并把一些重复的资源放入缓存中．从而提升最近一段时间该订单的加载速度．&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Compent class Classs{    @CreateCache(name = &amp;quot;order:save:&amp;quot;, expire = 3600, cacheType = CacheType.BOTH)   private Cache&amp;lt;String, OrderLoadResVo&amp;gt; cacheOrderSave;    // 生成分布式锁执行函数，同一个key同一时间只会有一个函数执行,100秒内每1秒钟检测一次   CacheLock.run(     // 缓存对象     cacheOrderSave,     // 等待最长时长     waitTime,      // 等待时间间隔     waitUnit,      // 等待关键字     key + &amp;quot;_RUN&amp;quot;,      // 当key对应函数,没有其他人执行时，开始执行本函数．奇谈人执行则等待     new Runnable() {       @Override       public void run() {         // 读取缓存         OrderLoadResVo loadCache = cacheOrderSave.get(key);         // ....          // 写入缓存         cacheOrderSave.put(order.getOrderId(), load);         cacheOrderSave.put(channelKey, load);       }     }); } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Thu, 31 Mar 2022 03:01:00 GMT</pubDate>
    </item>
    <item>
      <title>只用120行Java代码写一个自己的区块链-4实现真正的p2p网络</title>
      <link>/article/1552</link>
      <content:encoded>&lt;p&gt;在之前的&lt;a href="http://www.cnblogs.com/mignet/p/120_line_java_blockchain_2.html" target="_blank"&gt;文章&lt;/a&gt;中，我们模拟了节点网络通讯，很多朋友反馈说，他们想看真正的节点网络通讯而不是单节点的模拟。本章将满足你们。😌&lt;/p&gt; &lt;p&gt;我将本章的内容放在了com.v5ent.real.p2p包中，大家可以在&lt;a href="https://github.com/Mignet/blockchain" target="_blank"&gt;源码&lt;/a&gt;中找到我更新的代码。&lt;/p&gt; &lt;p&gt;通过本文，你将可以做到：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;创建自己的真实peer-to-peer网络&lt;/li&gt; &lt;li&gt;多个节点通过p2p网络同步区块内容&lt;/li&gt; &lt;li&gt;在自我节点实现RPC通讯，并向区块中写数据&lt;/li&gt; &lt;li&gt;在自我节点查看整个区块内容&lt;/li&gt; &lt;li&gt;不含虚拟货币在内的其他区块链知识&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;学习本章至少需要两台可ping通的机器（虚拟机也可以），并且安装了jdk8（如果只有jdk6，可以把代码改为jdk6支持的形式）。&lt;/p&gt; &lt;p&gt;基于之前的文章，本章将重写p2p部分的内容。我们首先要理清思路，提出区块链通讯需要解决的问题。&lt;/p&gt; &lt;p&gt;1.如果我第一次作为全节点启动，我需要干什么&lt;/p&gt; &lt;p&gt;2.如果我已经启动，别的节点和我通讯，我需要交互哪些信息&lt;/p&gt; &lt;p&gt;3.如果我已经启动，我怎么和自己的客户端交互&lt;/p&gt; &lt;p&gt;这些问题有一个很好的参考对象，就是&lt;strong&gt;比特币&lt;/strong&gt;，我们先来看看比特币的通讯过程&lt;/p&gt; &lt;h2&gt;一、比特币节点连接建立&lt;/h2&gt; &lt;p&gt;1.寻找比特币网络中的有效节点，此步骤通常有两种方法：&lt;/p&gt; &lt;p&gt;（1）使用“DNS种子”（DNS seeds），DNS种子提供比特币节点的IP地址列表，Bitcoin Core客户端提供五种不同的DNS种子，通常使用默认方式。这里不展开谈论。&lt;/p&gt; &lt;p&gt;（2）手动通过-seednode命令指定一个比特币节点的IP地址作为比特币种子节点（为什么叫种子，我的理解就是，根据种子，得到更多）&lt;/p&gt; &lt;p&gt;这里我们使用简单的方式来处理节点，用一个文件来存储地址列表-peers.list，第一次连接的时候更新这个文件，获取更多连接时也更新这个文件。简单演示我们直接把要测试的机器ip和端口加进去。&lt;/p&gt; &lt;p&gt;我有两台机，我把端口8015用作新的p2p网络的默认监听端口，那么我的peers.list的内容是：&lt;/p&gt; &lt;p&gt;10.16.0.205:8015&lt;/p&gt; &lt;p&gt;10.16.3.77:8015&lt;/p&gt; &lt;p&gt;2.与发现的有效比特币节点进行初始“握手”，建立连接 &lt;img src="http://www.yanzuoguang.com/upload/2022/03/6iga1qbav6hhpoeciu5rqqv00u.png" alt="1" /&gt;&lt;/p&gt; &lt;p&gt;节点发送一条包含基本认证内容的version消息开始“握手”通信过程，该消息包括如下内容：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;nVersion：客户端的比特币P2P协议所采用的版本（例如：70002）。&lt;/li&gt; &lt;li&gt;nLocalServices：一组该节点支持的本地服务列表，当前仅支持NODE_NETWORK&lt;/li&gt; &lt;li&gt;nTime：当前时间&lt;/li&gt; &lt;li&gt;addrYou：当前节点可见的远程节点的IP地址（上例中NodeB IP）&lt;/li&gt; &lt;li&gt;addrMe：当前节点的IP地址（上例中NodeA IP）&lt;/li&gt; &lt;li&gt;subver：指示当前节点运行的软件类型的子版本号（例如：”/Satoshi:0.9.2.1/”）&lt;/li&gt; &lt;li&gt;BestHeight：当前节点区块链的区块高度（初始为0，即只包含创世区块）&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;我们简化一下，这里我们最关心的就是区块高度bestHeight，我们就传递这个好了。区块高度就是区块链的长度。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;if (&amp;quot;VERSION&amp;quot;.equalsIgnoreCase(cmd)) {   // 对方发来握手信息，我方发给对方区块高度和最新区块的hash   pt.peerWriter.write(&amp;quot;VERACK &amp;quot; + blockChain.size() + &amp;quot; &amp;quot; + blockChain.get(blockChain.size() - 1).getHash()); }else if (&amp;quot;VERACK&amp;quot;.equalsIgnoreCase(cmd)) {   // 获取区块高度   String[] parts = payload.split(&amp;quot; &amp;quot;);   bestHeight = Integer.parseInt(parts[0]);   //哈希暂时不校验 } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;3.新节点建立更多的连接，使节点在网络中被更多节点接收，保证连接更稳定 &lt;img src="http://www.yanzuoguang.com/upload/2022/03/r55igii3jih2lp0ntg709p0ks4.png" alt="2" /&gt;&lt;/p&gt; &lt;p&gt;这里我们就两台机，如果你有更多机器，可以实现一下这个通讯，本章我们简单实现一下。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;if (&amp;quot;ADDR&amp;quot;.equalsIgnoreCase(cmd)) {   // 对方发来地址，建立连接并保存   if (!peers.contains(payload)) {     String peerAddr = payload.substring(0, payload.indexOf(&amp;quot;:&amp;quot;));     int peerPort = Integer.parseInt(payload.substring(payload.indexOf(&amp;quot;:&amp;quot;) + 1));     peerNetwork.connect(peerAddr, peerPort);     peers.add(payload);     PrintWriter out = new PrintWriter(peerFile);     for (int k = 0; k &amp;lt; peers.size(); k++) {       out.println(peers.get(k));     }     out.close();   } } else if (&amp;quot;GET_ADDR&amp;quot;.equalsIgnoreCase(cmd)) {   //对方请求更多peer地址，随机给一个   Random random = new Random();   pt.peerWriter.write(&amp;quot;ADDR &amp;quot; + peers.get(random.nextInt(peers.size()))); } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;4.交换“区块清单”（注：该步骤仅在全节点上会执行，且从与节点建立连接就开始进行）本系列内容只使用全节点。&lt;/p&gt; &lt;p&gt;&lt;em&gt;全节点&lt;/em&gt;&lt;/p&gt; &lt;p&gt;&lt;em&gt;全节点沿着区块链按时间倒叙一直追溯到创世区块，建立一个完整的UTXO数据库，通过查询UTXO是否未被支付来验证交易的有效性。&lt;/em&gt;&lt;/p&gt; &lt;p&gt;&lt;em&gt;SPV节点&lt;/em&gt;&lt;/p&gt; &lt;p&gt;&lt;em&gt;SPV节点通过向其他节点请求某笔交易的Merkle路径（Merkle树我可能会在后续章节讲到），如果路径正确无误，并且该交易之上已有6个或以上区块被确认，则证明该交易不是双重支付。&lt;/em&gt;&lt;/p&gt; &lt;p&gt;全节点在连接到其他节点后，需要构建完整的区块链，如果是新节点，它仅包含静态植入客户端中的0号区块（创世区块）。注意了，创世区块是静态的（硬编码）。 &lt;img src="http://www.yanzuoguang.com/upload/2022/03/q9ndpke5aghv5ohl1sodsr36c3.png" alt="3" /&gt;&lt;/p&gt; &lt;p&gt;如前文所言，我们在区块链中取最长的链，区块高度比我高，我就向对方获取区块。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;else if (&amp;quot;BLOCK&amp;quot;.equalsIgnoreCase(cmd)) {   //把对方给的块存进链中   LOGGER.info(&amp;quot;Attempting to add block...&amp;quot;);   LOGGER.info(&amp;quot;Block: &amp;quot; + payload);   Block newBlock = gson.fromJson(payload, Block.class);   if (!blockChain.contains(newBlock)) {     // 校验区块，如果成功，将其写入本地区块链     if (BlockUtils.isBlockValid(newBlock, blockChain.get(blockChain.size() - 1))) {       if (blockChain.add(newBlock) &amp;amp;&amp;amp; !catchupMode) {         LOGGER.info(&amp;quot;Added block &amp;quot; + newBlock.getIndex() + &amp;quot; with hash: [&amp;quot;+ newBlock.getHash() + &amp;quot;]&amp;quot;);         peerNetwork.broadcast(&amp;quot;BLOCK &amp;quot; + payload);       }     }   } } else if (&amp;quot;GET_BLOCK&amp;quot;.equalsIgnoreCase(cmd)) {   //把对方请求的块给对方   LOGGER.info(&amp;quot;Sending block[&amp;quot; + payload + &amp;quot;] to peer&amp;quot;);   Block block = blockChain.get(Integer.parseInt(payload));   if (block != null) {     LOGGER.info(&amp;quot;Sending block &amp;quot; + payload + &amp;quot; to peer&amp;quot;);     pt.peerWriter.write(&amp;quot;BLOCK &amp;quot; + gson.toJson(block));   } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;到这里，我们基本上完成了p2p网络中关于区块的通讯。&lt;/p&gt; &lt;p&gt;其实比特币等虚拟货币中还有很多通讯，关于交易的，这里我们不需要，不做讨论。&lt;/p&gt; &lt;h3&gt;让我们开始编码吧!&lt;/h3&gt; &lt;p&gt;整合上文提到的所有通讯，Node.java的代码如下&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;private static final Logger LOGGER = LoggerFactory.getLogger(Node.class);  /** 本地区块链 */ private static List&amp;lt;Block&amp;gt; blockChain = new LinkedList&amp;lt;Block&amp;gt;();  public static void main(String[] args) throws IOException, InterruptedException {   int port = 8015;   LOGGER.info(&amp;quot;Starting peer network...  &amp;quot;);   PeerNetwork peerNetwork = new PeerNetwork(port);   peerNetwork.start();   LOGGER.info(&amp;quot;[  Node is Started in port:&amp;quot;+port+&amp;quot;  ]&amp;quot;); 17         ArrayList&amp;lt;String&amp;gt; peers = new ArrayList&amp;lt;&amp;gt;();   File peerFile = new File(&amp;quot;peers.list&amp;quot;);   if (!peerFile.exists()) {     String host = InetAddress.getLocalHost().toString();     FileUtils.writeStringToFile(peerFile, host+&amp;quot;:&amp;quot;+port);   }   for (Object peer : FileUtils.readLines(peerFile)) {     String[] addr = peer.toString().split(&amp;quot;:&amp;quot;);     peerNetwork.connect(addr[0], Integer.parseInt(addr[1]));   }   TimeUnit.SECONDS.sleep(2);    peerNetwork.broadcast(&amp;quot;VERSION&amp;quot;);    // hard code genesisBlock   Block genesisBlock = new Block();   genesisBlock.setIndex(0);   genesisBlock.setTimestamp(&amp;quot;2017-07-13 22:32:00&amp;quot;);//my son's birthday   genesisBlock.setVac(0);   genesisBlock.setPrevHash(&amp;quot;&amp;quot;);   genesisBlock.setHash(BlockUtils.calculateHash(genesisBlock));   blockChain.add(genesisBlock);    final Gson gson = new GsonBuilder().create();   LOGGER.info(gson.toJson(blockChain));   int bestHeight = 0;   boolean catchupMode = true;    /** * p2p 通讯 */   while (true) {     //对新连接过的peer写入文件，下次启动直接连接     for (String peer : peerNetwork.peers) {       if (!peers.contains(peer)) {         peers.add(peer);         FileUtils.writeStringToFile(peerFile, peer);       }     }     peerNetwork.peers.clear();      // 处理通讯     for (PeerThread pt : peerNetwork.peerThreads) {       if (pt == null || pt.peerReader == null) {         break;       }       List&amp;lt;String&amp;gt; dataList = pt.peerReader.readData();       if (dataList == null) {         LOGGER.info(&amp;quot;Null ret retry.&amp;quot;);         System.exit(-5);         break;       }        for (String data:dataList) {         LOGGER.info(&amp;quot;Got data: &amp;quot; + data);         int flag = data.indexOf(' ');         String cmd = flag &amp;gt;= 0 ? data.substring(0, flag) : data;         String payload = flag &amp;gt;= 0 ? data.substring(flag + 1) : &amp;quot;&amp;quot;;         if (StringUtils.isNotBlank(cmd)) {           if (&amp;quot;VERSION&amp;quot;.equalsIgnoreCase(cmd)) {             // 对方发来握手信息，我方发给对方区块高度和最新区块的hash             pt.peerWriter.write(&amp;quot;VERACK &amp;quot; + blockChain.size() + &amp;quot; &amp;quot; + blockChain.get(blockChain.size() - 1).getHash());           }else if (&amp;quot;VERACK&amp;quot;.equalsIgnoreCase(cmd)) {             // 获取区块高度             String[] parts = payload.split(&amp;quot; &amp;quot;);             bestHeight = Integer.parseInt(parts[0]);             //哈希暂时不校验           } else if (&amp;quot;GET_BLOCK&amp;quot;.equalsIgnoreCase(cmd)) {             //把对方请求的块给对方             LOGGER.info(&amp;quot;Sending block[&amp;quot; + payload + &amp;quot;] to peer&amp;quot;);             Block block = blockChain.get(Integer.parseInt(payload));             if (block != null) {               LOGGER.info(&amp;quot;Sending block &amp;quot; + payload + &amp;quot; to peer&amp;quot;);               pt.peerWriter.write(&amp;quot;BLOCK &amp;quot; + gson.toJson(block));             }           } else if (&amp;quot;BLOCK&amp;quot;.equalsIgnoreCase(cmd)) {             //把对方给的块存进链中             LOGGER.info(&amp;quot;Attempting to add block...&amp;quot;);             LOGGER.info(&amp;quot;Block: &amp;quot; + payload);             Block newBlock = gson.fromJson(payload, Block.class);             if (!blockChain.contains(newBlock)) {               // 校验区块，如果成功，将其写入本地区块链               if (BlockUtils.isBlockValid(newBlock, blockChain.get(blockChain.size() - 1))) {                 if (blockChain.add(newBlock) &amp;amp;&amp;amp; !catchupMode) {                   LOGGER.info(&amp;quot;Added block &amp;quot; + newBlock.getIndex() + &amp;quot; with hash: [&amp;quot;+ newBlock.getHash() + &amp;quot;]&amp;quot;);                   peerNetwork.broadcast(&amp;quot;BLOCK &amp;quot; + payload);                 }               }             }           }else if (&amp;quot;GET_ADDR&amp;quot;.equalsIgnoreCase(cmd)) {             //对方请求更多peer地址，随机给一个             Random random = new Random();             pt.peerWriter.write(&amp;quot;ADDR &amp;quot; + peers.get(random.nextInt(peers.size())));           } else if (&amp;quot;ADDR&amp;quot;.equalsIgnoreCase(cmd)) {             // 对方发来地址，建立连接并保存             if (!peers.contains(payload)) {               String peerAddr = payload.substring(0, payload.indexOf(&amp;quot;:&amp;quot;));               int peerPort = Integer.parseInt(payload.substring(payload.indexOf(&amp;quot;:&amp;quot;) + 1));               peerNetwork.connect(peerAddr, peerPort);               peers.add(payload);               PrintWriter out = new PrintWriter(peerFile);               for (int k = 0; k &amp;lt; peers.size(); k++) {                 out.println(peers.get(k));               }               out.close();             }           }          }       }     }      // ********************************     //         比较区块高度,同步区块     // ********************************      int localHeight = blockChain.size();      if (bestHeight &amp;gt; localHeight) {       catchupMode = true;       LOGGER.info(&amp;quot;Local chain height: &amp;quot; + localHeight);       LOGGER.info(&amp;quot;Best chain Height: &amp;quot; + bestHeight);       TimeUnit.MILLISECONDS.sleep(300);        for (int i = localHeight; i &amp;lt; bestHeight; i++) {         LOGGER.info(&amp;quot;请求块 &amp;quot; + i + &amp;quot;...&amp;quot;);         peerNetwork.broadcast(&amp;quot;GET_BLOCK &amp;quot; + i);       }     } else {       if (catchupMode) {         LOGGER.info(&amp;quot;[p2p] - Caught up with network.&amp;quot;);       }       catchupMode = false;     }        // ****************     // 循环结束     // ****************     TimeUnit.MILLISECONDS.sleep(200);   } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;PeerNetwork简单封装了一下p2p通讯的细节，篇幅有限我这里只列出核心交互，具体实现可以去看我的&lt;a href="https://github.com/Mignet/blockchain/tree/master/src/main/java/com/v5ent/real/p2p" target="_blank"&gt;github源码&lt;/a&gt;中的类：PeerThread、PeerReader、PeerWriter。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;RPC&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;接下来，我们将讨论关于本地节点客户端的概念。在比特币中，除了bitcoin-core，还有bitcoin-cli，这是做什么的呢。&lt;/p&gt; &lt;p&gt;它其实是用来做本地节点交互的，比如我作为本地节点，我需要发起交易，需要查看我的资产等等，后来发展出gui界面，就是大家俗称的钱包。&lt;/p&gt; &lt;p&gt;在本章中，我们也需要这样一个客户端通讯，用来将我们的vac写入链中（之前的文章，我们是用控制台输入的，实际的做法是提供加密的rpc调用）我们接下来实现RPC服务&lt;/p&gt; &lt;p&gt;首先我们要在Node.java中加入通讯逻辑&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;LOGGER.info(&amp;quot;Starting RPC daemon... &amp;quot;);  RpcServer rpcAgent = new RpcServer(port+1);   rpcAgent.start();   LOGGER.info(&amp;quot;[  RPC agent is Started in port:&amp;quot;+(port+1)+&amp;quot;  ]&amp;quot;); // for循环体中增加,处理RPC服务 for (RpcThread th:rpcAgent.rpcThreads) {   String request = th.req;   if (request != null) {     String[] parts = request.split(&amp;quot; &amp;quot;);     parts[0] = parts[0].toLowerCase();     if (&amp;quot;getinfo&amp;quot;.equals(parts[0])) {       String res = gson.toJson(blockChain);       th.res = res;     } else if (&amp;quot;send&amp;quot;.equals(parts[0])) {       try {         int vac = Integer.parseInt(parts[1]);         // 根据vac创建区块         Block newBlock = BlockUtils.generateBlock(blockChain.get(blockChain.size() - 1), vac);         if (BlockUtils.isBlockValid(newBlock, blockChain.get(blockChain.size() - 1))) {           blockChain.add(newBlock);           th.res = &amp;quot;write Success!&amp;quot;;           peerNetwork.broadcast(&amp;quot;BLOCK &amp;quot; + gson.toJson(newBlock));         } else {           th.res = &amp;quot;RPC 500: Invalid vac Error\n&amp;quot;;         }       } catch (Exception e) {         th.res = &amp;quot;Syntax (no '&amp;lt;' or '&amp;gt;'): send &amp;lt;vac&amp;gt; &amp;lt;privateKey&amp;gt;&amp;quot;;         LOGGER.error(&amp;quot;invalid vac&amp;quot;, e);       }     } else {       th.res = &amp;quot;Unknown command: \&amp;quot;&amp;quot; + parts[0] + &amp;quot;\&amp;quot;&amp;quot;;     }   } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;独立线程处理&lt;/p&gt; &lt;p&gt;RpcThread.java&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.v5ent.real.p2p;  import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.Socket; import java.util.concurrent.TimeUnit;  import org.slf4j.Logger; import org.slf4j.LoggerFactory; /**  * 处理单个rpc连接  * @author Mignet  */ public class RpcThread extends Thread {          private static final Logger LOGGER = LoggerFactory.getLogger(RpcThread.class);          private Socket socket;     String res;     String req;      /**      * 默认构造函数      * @param socket      */     public RpcThread(Socket socket){         this.socket = socket;     }      @Override     public void run(){         try{             req = null;             res = null;             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));             String input;             out.println(&amp;quot;================Welcome RPC Daemon==============&amp;quot;);             while((input = in.readLine()) != null){                 if (&amp;quot;HELP&amp;quot;.equalsIgnoreCase(input)){                     out.println(&amp;quot;############################################## COMMANDS ###############################################&amp;quot;);                     out.println(&amp;quot;#     1) getinfo       - Gets block chain infomations.                                                #&amp;quot;);                     out.println(&amp;quot;#     2) send &amp;lt;vac&amp;gt;    - Write &amp;lt;vac&amp;gt; to blockChain                                                    #&amp;quot;);                     out.println(&amp;quot;#######################################################################################################&amp;quot;);                 } else {                     req = input;                     while (res == null){                         TimeUnit.MILLISECONDS.sleep(25);                     }                     out.println(res);                     req = null;                     res = null;                 }             }         } catch (Exception e){             LOGGER.info(&amp;quot;An RPC client has disconnected.&amp;quot;,e);         }     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;RpcServer.java&lt;/p&gt; &lt;pre&gt;&lt;code class="language-java"&gt;package com.v5ent.real.p2p;  import java.net.ServerSocket; import java.util.ArrayList; import java.util.List;  import org.slf4j.Logger; import org.slf4j.LoggerFactory;  /**  * RPC服务  *  * 注意：不要把这个端口开放给外网  * @author Mignet  */ public class RpcServer extends Thread {     private static final Logger LOGGER = LoggerFactory.getLogger(RpcServer.class);     private int port;     private boolean runFlag = true;      List&amp;lt;RpcThread&amp;gt; rpcThreads;      /**      * 默认配置      */     public RpcServer()     {         this.port = 8016;         this.rpcThreads = new ArrayList&amp;lt;&amp;gt;();     }      /**      * 指定端口      * @param port Port to listen on      */     public RpcServer(int port)     {         this.port = port;         this.rpcThreads = new ArrayList&amp;lt;&amp;gt;();     }      @Override     public void run()     {         try         {             ServerSocket socket = new ServerSocket(port);             while (runFlag)             {                 RpcThread thread = new RpcThread(socket.accept());                 rpcThreads.add(thread);                 thread.start();             }             socket.close();         } catch (Exception e){             LOGGER.error(&amp;quot;rpc error in port:&amp;quot; + port,e);         }     } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;跑起来&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;1.使用mvn的install命令打包&lt;/p&gt; &lt;p&gt;2.新建peers.list,把要组建网络的ip地址填入去，&lt;strong&gt;在本机执行jar命令，启动第一个节点。注意，这时候它会尝试连接别的节点，连接不上&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/s4fivjmhn8i1jq5q1kuvl8m22c.png" alt="4" /&gt;&lt;/p&gt; &lt;p&gt;3.把jar包和peers.list上传到其他机器，启动第二个节点&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/0m8gdc4m96ie4r9rl151lquum3.png" alt="5" /&gt;&lt;/p&gt; &lt;p&gt;4.我们用cmd在本机打开新的窗口，执行nc 127.0.0.1 8016,连接到本机节点的rpc服务，输入help查看支持的命令：&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/61unm9cgc4havo7p94acj663hp.png" alt="6" /&gt;&lt;/p&gt; &lt;p&gt;5.节点1（本机）增加一个区块：在rpc命令中，我们实现了1，查看区块链；2，写入vac数据，来验证一下&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/2grvvl11e0imqqno758ale4j1n.png" alt="7" /&gt;&lt;/p&gt; &lt;p&gt;我们先输入getinfo查看一下，然后send 88，看到写入成功了，再输入getinfo，果然看到了新的块在链中。&lt;/p&gt; &lt;p&gt;我们也可以看到节点控制台的输出中关于新增区块的信息&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/vui07oqu22j4iqgoas0savcmlp.png" alt="8" /&gt;&lt;/p&gt; &lt;p&gt;6.验证是不是同步了：我们看一下另一台机器&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/op6rpsgb6uje4o81e9ds6b4qk3.png" alt="9" /&gt;&lt;/p&gt; &lt;p&gt;我们在这台机器上也使用nc localhost 8016来连接看看区块&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/u23ltadg9kgmirc7irbnprpo43.png" alt="10" /&gt;&lt;/p&gt; &lt;p&gt;7.我们再从这个结点写入一个块（send 666）&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/rvm03lt8e4gppqc5sgauubkg3u.png" alt="11" /&gt;&lt;/p&gt; &lt;p&gt;看看本机接收到了没&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/03/0rbntc8jm4jiqo6f2huqt7ltih.png" alt="12" /&gt;&lt;/p&gt; &lt;p&gt;很完美。&lt;/p&gt; &lt;p&gt;到此基本演示了区块链通讯真实的样子。当然，这里有很多可以改进的地方，比如安全性，比如命令的模式，比如不用sleep而是用Future，比如使用netty等更高效更成熟的通讯框架。&lt;/p&gt; &lt;p&gt;如果想利用区块链来发行数字货币，那么在此基础上，还要有公私钥签名交易，交易通讯，校验，使用共识算法来选举及奖励货币等。&lt;/p&gt; &lt;p&gt;还有什么区块链知识是本系列没有提到的吗？有的，使用默克尔树来快速验证区块和整个链.&lt;/p&gt; &lt;p&gt;关于币的问题也可以问，比如UTXO模型可以讲一讲吗？如果有问题请大家留言&lt;/p&gt;</content:encoded>
      <pubDate>Thu, 17 Mar 2022 01:35:00 GMT</pubDate>
    </item>
    <item>
      <title>mysql的sql语句长度_mysql 的sql语句长度</title>
      <link>/article/1551</link>
      <content:encoded>&lt;h1&gt;mysql的sql语句长度_mysql 的sql语句长度&lt;/h1&gt; &lt;p&gt;mysql的sql语句的长度是有限制的，刚刚了解这一点，赶紧记下来。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;查询方法&amp;gt; show VARIABLES like '%max_allowed_packet%'; +--------------------------+------------+ | Variable_name            | Value      | +--------------------------+------------+ | max_allowed_packet       | 1024       | | slave_max_allowed_packet | 1073741824 | +--------------------------+------------+ &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里的单位是byte,现在我的mysql的sql语句长度貌似是1k, 以下是网络上的经验，记录以下：&lt;/p&gt; &lt;p&gt;修改方法&lt;/p&gt; &lt;h2&gt;1) 方法1&lt;/h2&gt; &lt;p&gt;可以编辑my.cnf来修改(windows下my.ini),在[mysqld]段或者mysql的server配置段进行修改。(在[mysqld]下加上max_allowed_packet=16M，保存重启mysql后)&lt;/p&gt; &lt;pre&gt;&lt;code class="language-properties"&gt;max_allowed_packet = 20M &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果找不到my.cnf可以通过&lt;/p&gt; &lt;pre&gt;&lt;code class="language-sql"&gt;mysql --help | grep my.cnf &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;去寻找my.cnf文件。&lt;/p&gt; &lt;h2&gt;2) 方法2&lt;/h2&gt; &lt;p&gt;(很妥协，很纠结的办法)&lt;/p&gt; &lt;p&gt;进入mysql server, 在mysql 命令行中运行&lt;/p&gt; &lt;pre&gt;&lt;code class="language-mysql"&gt;set global max_allowed_packet = 2*1024*1024*10 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后关闭掉这此mysql server链接，再进入。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-mysql"&gt;show VARIABLES like '%max_allowed_packet%'; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;查看下max_allowed_packet是否编辑成功 如果有网友提供更好的方法，我将感激不尽！&lt;/p&gt;</content:encoded>
      <pubDate>Tue, 15 Feb 2022 09:49:31 GMT</pubDate>
    </item>
    <item>
      <title>表互锁该如何解决</title>
      <link>/article/1550</link>
      <content:encoded>&lt;h1&gt;表互锁该如何解决&lt;/h1&gt; &lt;h2&gt;存在流程操作表如下&lt;/h2&gt; &lt;ul&gt; &lt;li&gt;库存下单 -&amp;gt; A,B,C,E,H&lt;/li&gt; &lt;li&gt;排号 -&amp;gt; B,A,C,D,E,F,G,I&lt;/li&gt; &lt;li&gt;退票 -&amp;gt; B,A,C,G&lt;/li&gt; &lt;li&gt;过闸 -&amp;gt; A,G,C&lt;/li&gt; &lt;li&gt;生成班次 -&amp;gt; .....&lt;/li&gt; &lt;li&gt;调整班次 -&amp;gt; ....&lt;/li&gt; &lt;/ul&gt; &lt;h2&gt;可能会存在的锁&lt;/h2&gt; &lt;h3&gt;同表锁定:&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;下单-&amp;gt;A表.a记录+1 , A表.b记录+1&lt;/li&gt; &lt;li&gt;退单-&amp;gt;A表.b记录+1 , A表.a记录+1&lt;/li&gt; &lt;/ul&gt; &lt;h3&gt;不同表互锁:&lt;/h3&gt; &lt;ul&gt; &lt;li&gt;下单-&amp;gt;A表.a记录+1, B表.a记录+1&lt;/li&gt; &lt;li&gt;退单-&amp;gt;B表.a记录+1, A表.a记录+1&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;在两个事务第一步做完后，第二步两个事务互相等待对方完成.这时，就会锁住数据．&lt;/p&gt; &lt;h2&gt;同表锁定解决&lt;/h2&gt; &lt;p&gt;按主键进行排序，并依次执行，可以解决同表锁定的问题．&lt;/p&gt; &lt;h2&gt;不同表锁定解决&lt;/h2&gt; &lt;ol&gt; &lt;li&gt;为每个业务定义临时变量，将该业务的数据全部放入临时变量．&lt;/li&gt; &lt;li&gt;定义接口，操作临时变量，Dao层实现该接口&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;interface 接口 {    void create(临时变量) } &lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt; &lt;li&gt;在具体操作中，通过Spring注入接口数组的方式来运行调用数据库．&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-java"&gt;@Autowired private List&amp;lt;接口&amp;gt; unitCreates;  // 利用接口循环创建单据，防止更新表顺序冲突 for (接口 unitCreate : unitCreates) {     unitCreate.create(临时变量); } &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Mon, 14 Feb 2022 03:03:00 GMT</pubDate>
    </item>
    <item>
      <title>MySql数据库远程连接慢的问题（MySql5.7）</title>
      <link>/article/1549</link>
      <content:encoded>&lt;h1&gt;MySql数据库远程连接慢的问题（MySql5.7）&lt;/h1&gt; &lt;h2&gt;原理：配置参数关闭DNS解析&lt;/h2&gt; &lt;p&gt;WIndows操作系统，MySql配置参数增加&lt;code&gt;skip-name-resolve&lt;/code&gt;&lt;/p&gt; &lt;p&gt;配置文件：My.ini 在C：\ProgramData\MySQL\MySQL Server 5.7\my.ini&lt;/p&gt; &lt;pre&gt;&lt;code class="language-my.cnf"&gt;[mysqld]  # 增加以下行（注意节点名称是mysqld，不是mysql） skip-name-resolve &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;重新启动Mysql就可以了&lt;/p&gt;</content:encoded>
      <pubDate>Wed, 19 Jan 2022 03:01:00 GMT</pubDate>
    </item>
    <item>
      <title>mavon-editor 使用教程</title>
      <link>/article/1548</link>
      <content:encoded>&lt;h1&gt;mavon-editor 使用教程&lt;/h1&gt; &lt;p&gt;mavon-editor是一款基于vue的markdown编辑器，比较适合博客系统。由于官网对于一些细节的说明不够详细，这里对这里对该编辑器的使用做一个总结。&lt;/p&gt; &lt;h4&gt;安装&lt;/h4&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;npm install mavon-editor --save &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;基本使用&lt;/h4&gt; &lt;p&gt;在vue-cli构建的脚手架离得main.js可以像这样引用：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;// 全局注册 import Vue from 'vue' import mavonEditor from 'mavon-editor' import 'mavon-editor/dist/css/index.css' // use Vue.use(mavonEditor) new Vue({   'el': '#main' }) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在具体的组件里html里定义挂载点&lt;/p&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;div id=&amp;quot;main&amp;quot;&amp;gt;     &amp;lt;mavon-editor v-model=&amp;quot;value&amp;quot;/&amp;gt; &amp;lt;/div&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;strong&gt;效果&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/01/sg0nr7ob1ei2vp7e86a1igf8bg.webp" alt="1" /&gt;&lt;/p&gt; &lt;h4&gt;图片上传&lt;/h4&gt; &lt;p&gt;先将挂在点里的方法写好，就像这样&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;el-main class=&amp;quot;content-content&amp;quot;&amp;gt;   &amp;lt;mavon-editor                  v-model = 'editorContent'                 :ishljs=&amp;quot;true&amp;quot;                 :codeStyle=&amp;quot;code_style&amp;quot;                 ref=md @imgAdd=&amp;quot;$imgAdd&amp;quot; @imgDel=&amp;quot;$imgDel&amp;quot;                 /&amp;gt; &amp;lt;/el-main&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;主要是将&lt;code&gt;ref=md @imgAdd=&amp;quot;$imgAdd&amp;quot; @imgDel=&amp;quot;$imgDel&amp;quot;&lt;/code&gt;放进去，其他是我的其他代码。 &lt;strong&gt;然后定义&lt;code&gt;$imgAdd&lt;/code&gt;和&lt;code&gt;$imgDel&lt;/code&gt;方法:&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;// 绑定@imgAdd event $imgAdd(pos, $file) {   // 第一步.将图片上传到服务器.   var formdata = new FormData();   formdata.append('image', $file);   this.img_file[pos] = $file;   this.$http({     url: '/api/edit/uploadimg',     method: 'post',     data: formdata,     headers: { 'Content-Type': 'multipart/form-data' },   }).then((res) =&amp;gt; {     let _res = res.data;     // 第二步.将返回的url替换到文本原位置![...](0) -&amp;gt; ![...](url)     this.$refs.md.$img2Url(pos, _res.url);   }) },   $imgDel(pos) {     delete this.img_file[pos];   } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里注意我的&lt;code&gt;this.$https&lt;/code&gt;就是我的axios对象（用过vue-cli的都懂的），然后&lt;code&gt;this.img_file&lt;/code&gt;是我之前定义的一个空对象。&lt;code&gt;this.$refs.md&lt;/code&gt;和挂在点定义的要保持一致。 剩下的就是交给服务端去处理了： &lt;a href="https://www.jianshu.com/p/b7eadc8b270b" target="_blank"&gt;koa解决文件上传&lt;/a&gt; 大家有不明白的地方或者需要我补充更正的地方欢迎留言~&lt;/p&gt; &lt;h4&gt;参考文献&lt;/h4&gt; &lt;p&gt;&lt;a href="https://github.com/hinesboy/mavonEditor/blob/master/README.md" target="_blank"&gt;https://github.com/hinesboy/mavonEditor/blob/master/README.md&lt;/a&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 14 Jan 2022 10:19:00 GMT</pubDate>
    </item>
    <item>
      <title>Vue中使用markdown</title>
      <link>/article/1547</link>
      <content:encoded>&lt;p&gt;最近一直在做node+vue+mysql的博客项目，想用mardown编辑器，找了很多方法，最后总结出以下几点： &lt;em&gt;刚开始还想用最笨拙的方式，从数据库取出数据后给添加上p标签就算了，但还是太单薄了&lt;/em&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-jsx"&gt;给后台取出的数据添加p标签，记录一下，免得以后用到会忘 this.articleDetail.context = (isNaN(this.articleDetail.context)) ? this.articleDetail.context.replace(/^/gm, '&amp;lt;p&amp;gt;').replace(/$/gm, '&amp;lt;/p&amp;gt;') : null; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;1) 使用marked解析markdown文字&lt;/h3&gt; &lt;p&gt;这个就只是解析markdown文字，并不能编辑，倒是可以从数据库中读取markdown文字进行解析,另外代码高亮还要另外解析，用highlight.js&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;cnpm install marked &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;article class=&amp;quot;context&amp;quot; v-html=&amp;quot;compiledMarkdown&amp;quot;&amp;gt;&amp;lt;/article&amp;gt; &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   export default{     data(){       return {         articleDetail: [],//数据，可以从数据库中读取       }     },     computed:{       compiledMarkdown () {         //this.articleDetail.context数据         return marked(this.articleDetail.context, { sanitize: true })       }     }   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;2) 使用mavonEditor&lt;/h3&gt; &lt;p&gt;mavonEditor既可作为编辑使用，也可作为解析使用 API：&lt;a href="https://github.com/hinesboy/mavonEditor" target="_blank"&gt;https://github.com/hinesboy/mavonEditor&lt;/a&gt; 刚开始做的时候可以想到用mavonEditor作为编辑器使用，v-model中绑定数据，可以提交到数据库中，但就是没想到怎么去把数据库中的数据取出来再进行解析，感觉看官方API也是一头雾水，网上怎么也搜不到相关内容，于是还是沉下心来看API，总算是想到还可以用此来解析。&lt;/p&gt; &lt;h5&gt;引入&lt;/h5&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;npm install mavon-editor --save &lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;&lt;code class="language-js"&gt;// 全局注册 // import with ES6 import Vue from 'vue' import mavonEditor from 'mavon-editor' import 'mavon-editor/dist/css/index.css' // use Vue.use(mavonEditor) &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;作为编辑器使用&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;mavon-editor v-model=&amp;quot;context&amp;quot; :toolbars=&amp;quot;toolbars&amp;quot; @keydown=&amp;quot;&amp;quot; /&amp;gt; &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   export default{     data(){       return {         context: ' ',//输入的数据         toolbars: {           bold: true, // 粗体           italic: true, // 斜体           header: true, // 标题           underline: true, // 下划线           mark: true, // 标记           superscript: true, // 上角标           quote: true, // 引用           ol: true, // 有序列表           link: true, // 链接           imagelink: true, // 图片链接           help: true, // 帮助           code: true, // code           subfield: true, // 是否需要分栏           fullscreen: true, // 全屏编辑           readmodel: true, // 沉浸式阅读           /* 1.3.5 */           undo: true, // 上一步           trash: true, // 清空           save: true, // 保存（触发events中的save事件）           /* 1.4.2 */           navigation: true // 导航目录         }       }     }   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;从数据库中获取刚刚存入的markdown文字解析到页面来&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;mavon-editor               class=&amp;quot;md&amp;quot;               :value=&amp;quot;articleDetail.context&amp;quot;//获取数据               :subfield = &amp;quot;prop.subfield&amp;quot;               :defaultOpen = &amp;quot;prop.defaultOpen&amp;quot;               :toolbarsFlag = &amp;quot;prop.toolbarsFlag&amp;quot;               :editable=&amp;quot;prop.editable&amp;quot;               :scrollStyle=&amp;quot;prop.scrollStyle&amp;quot;               &amp;gt;&amp;lt;/mavon-editor&amp;gt;  &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   export default{     computed: {       prop () {         let data = {           subfield: false,// 单双栏模式           defaultOpen: 'preview',//edit： 默认展示编辑区域 ， preview： 默认展示预览区域            editable: false,           toolbarsFlag: false,           scrollStyle: true         }         return data       }     },   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Fri, 14 Jan 2022 10:04:00 GMT</pubDate>
    </item>
    <item>
      <title>vue中使用markdown</title>
      <link>/article/1546</link>
      <content:encoded>&lt;h1&gt;vue中使用markdown&lt;/h1&gt; &lt;p&gt;参考资料：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="https://github.com/hinesboy/mavonEditor" target="_blank"&gt;https://github.com/hinesboy/mavonEditor&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="https://www.jianshu.com/p/aca26ec75ec3" target="_blank"&gt;https://www.jianshu.com/p/aca26ec75ec3&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;示例：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;div&amp;gt;   &amp;lt;!--markdown编辑--&amp;gt;   &amp;lt;no-ssr&amp;gt;     &amp;lt;mavon-editor :toolbars=&amp;quot;toolbars&amp;quot; v-model=&amp;quot;value&amp;quot;/&amp;gt;   &amp;lt;/no-ssr&amp;gt;   &amp;lt;!--markdown展示--&amp;gt;   &amp;lt;!--在 mavon-editor 标签中添加class=&amp;quot;md&amp;quot; 和在上一层嵌套&amp;lt;no-ssr&amp;gt;都可以达到解析markdown语法的效果 --&amp;gt;   &amp;lt;mavon-editor class=&amp;quot;md&amp;quot; previewBackground=&amp;quot;white&amp;quot;                  :toolbarsFlag=&amp;quot;false&amp;quot; defaultOpen='preview' :subfield=&amp;quot;false&amp;quot;                 :editable=&amp;quot;false&amp;quot; :scrollStyle=&amp;quot;true&amp;quot; :toolbars=&amp;quot;toolbars&amp;quot;                  :value=&amp;quot;value&amp;quot;/&amp;gt;   &amp;lt;/div&amp;gt; &amp;lt;/template&amp;gt;  &amp;lt;script&amp;gt;   export default {     name: 'login',     data () {       return {         value: '',         toolbars: {           bold: true, // 粗体           italic: true, // 斜体           header: true, // 标题           underline: true, // 下划线           strikethrough: true, // 中划线           mark: true, // 标记           superscript: true, // 上角标           subscript: true, // 下角标           quote: true, // 引用           ol: true, // 有序列表           ul: true, // 无序列表           link: true, // 链接           imagelink: true, // 图片链接           code: true, // code           table: true, // 表格           fullscreen: true, // 全屏编辑           readmodel: true, // 沉浸式阅读           htmlcode: true, // 展示html源码           help: true, // 帮助           /* 1.3.5 */           undo: true, // 上一步           redo: true, // 下一步           trash: true, // 清空           save: true, // 保存（触发events中的save事件）           /* 1.4.2 */           navigation: true, // 导航目录           /* 2.1.8 */           alignleft: true, // 左对齐           aligncenter: true, // 居中           alignright: true, // 右对齐           /* 2.2.1 */           subfield: true, // 单双栏模式           preview: true, // 预览         }       }     },   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Fri, 14 Jan 2022 09:55:55 GMT</pubDate>
    </item>
    <item>
      <title>在 vue3 中使用 markdown 编辑器 md-editor-v3</title>
      <link>/article/1545</link>
      <content:encoded>&lt;p&gt;本文将介绍编辑器的使用和伴随的某些开发技巧。&lt;/p&gt; &lt;p&gt;该编辑器支持的功能有：基础的md编辑、md语法快捷键、记录保存、暗黑主题、图片上传/复制图片上传/&lt;strong&gt;裁剪图片上传&lt;/strong&gt;、格式化内容、浏览器全屏/屏幕全屏、仅预览模式等功能，静待使用。&lt;/p&gt; &lt;p&gt;详细的编辑器api参考：&lt;a href="https://imzbf.github.io/md-editor-v3" target="_blank"&gt;文档&lt;/a&gt;。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;图片裁剪预览&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/01/vsaevkjn3ehmnq8rm6pbq9mosi.webp" alt="1" /&gt;&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/01/oq5uelp114gthov32c50bbaato.webp" alt="2" /&gt;&lt;/p&gt; &lt;ul&gt; &lt;li&gt;编辑器预览&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/01/7o58depfc2giiosm5ggbc2e9a2.webp" alt="3" /&gt;&lt;/p&gt; &lt;h2&gt;1. 基本使用&lt;/h2&gt; &lt;p&gt;这里演示两种环境三种写法：&lt;/p&gt; &lt;h3&gt;1.1 npm安装用法&lt;/h3&gt; &lt;p&gt;这种方式支持两种写法，除了&lt;code&gt;.vue&lt;/code&gt;模板写法，还有&lt;code&gt;jsx&lt;/code&gt;语法。&lt;/p&gt; &lt;p&gt;安装&lt;/p&gt; &lt;pre&gt;&lt;code class="language-shell"&gt;yarn add md-editor-v3 或者 npm install md-editor-v3 -save &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;.vue&lt;/code&gt;模板基础使用&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt;   &amp;lt;md-editor v-model=&amp;quot;text&amp;quot; /&amp;gt;  &amp;lt;/template&amp;gt;    &amp;lt;script lang=&amp;quot;ts&amp;quot;&amp;gt;  import { defineComponent } from 'vue';  import MdEditor from 'md-editor-v3';  import 'md-editor-v3/lib/style.css';    export default defineComponent({    components: { MdEditor },    data() {      return { text: '' };    }  });  &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;jsx&lt;/code&gt;语法基础使用&lt;/p&gt; &lt;pre&gt;&lt;code class="language-jsx"&gt;import { defineComponent, ref } from 'vue'; import MdEditor from 'md-editor-v3'; import 'md-editor-v3/lib/style.css';  export default defineComponent({   name: 'MdEditor',   setup() {     const text = ref('');     return () =&amp;gt; (       &amp;lt;MdEditor modelValue={text.value} onChange={(v: string) =&amp;gt; (text.value = v)} /&amp;gt;     );   } }); &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;1.2 script标签引入用法&lt;/h3&gt; &lt;p&gt;链接可前往&lt;a href="https://cdn.jsdelivr.net" target="_blank"&gt;https://cdn.jsdelivr.net&lt;/a&gt;搜索&lt;code&gt;md-editor-v3&lt;/code&gt;。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;!--添加样式--&amp;gt; &amp;lt;link href=&amp;quot;https://cdn.jsdelivr.net/npm/md-editor-v3@1.2.0/lib/style.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot; /&amp;gt; &amp;lt;!--引入vue3--&amp;gt; &amp;lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/vue@3.1.5/dist/vue.global.prod.min.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt; &amp;lt;!--引入组件--&amp;gt; &amp;lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/md-editor-v3@1.2.0/lib/md-editor-v3.umd.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;注册组件&lt;/p&gt; &lt;pre&gt;&lt;code class="language-jsx"&gt;const App = {  data() {    return {       text: 'Hello Editor!!'    };  } };  Vue.createApp(App).use(MdEditorV3).mount('#md-editor-v3'); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用组件&lt;/p&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;div id=&amp;quot;md-editor-v3&amp;quot;&amp;gt;   &amp;lt;md-editor-v3 v-model=&amp;quot;text&amp;quot; /&amp;gt; &amp;lt;/div&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;2. 渲染内容&lt;/h2&gt; &lt;p&gt;该编辑器使用&lt;code&gt;marked&lt;/code&gt;解析&lt;code&gt;md&lt;/code&gt;为&lt;code&gt;html&lt;/code&gt;，没有扩展语法。&lt;/p&gt; &lt;p&gt;通常来讲，编辑内容存储为&lt;code&gt;md&lt;/code&gt;格式，渲染内容时，通过&lt;code&gt;marked&lt;/code&gt;解析为html。&lt;/p&gt; &lt;h3&gt;2.1 默认渲染&lt;/h3&gt; &lt;p&gt;从&lt;code&gt;1.3.0&lt;/code&gt;版本后，编辑器支持了&lt;code&gt;previewOnly&lt;/code&gt;功能，可以直接使用编辑器预览文章，没有bar、编辑等等。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt;   &amp;lt;md-editor     v-model=&amp;quot;text&amp;quot;     previewOnly   /&amp;gt; &amp;lt;/template&amp;gt;  &amp;lt;script lang=&amp;quot;ts&amp;quot;&amp;gt; import { defineComponent } from 'vue'; import MdEditor from 'md-editor-v3'; import 'md-editor-v3/lib/style.css';  export default defineComponent({   components: { MdEditor },   data() { return { text: '## 我只会显示预览内容' }; } }); &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;2.2 主动解析演示&lt;/h3&gt; &lt;p&gt;这种方式用于保存&lt;code&gt;md&lt;/code&gt;，然后自行解析&lt;code&gt;md&lt;/code&gt;内容。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-jsx"&gt;import marked from 'marked';  // 代码高亮 import hljs from 'highlight.js'; // 自选代码高亮样式 import 'highlight.js/scss/atom-one-dark.scss';  // 用于记录标题数，根据业务代替 let count = 0; // 记录标题内容 const headstemp = [];  // marked设置 const rendererMD = new marked.Renderer();  // 调整标题内容 rendererMD.heading = (text, level) =&amp;gt; {   headstemp.push({ text, level });   count++;   return `&amp;lt;h${level} id=&amp;quot;heading-${count}&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;h-text&amp;quot;&amp;gt;${text}&amp;lt;/span&amp;gt;&amp;lt;/h${level}&amp;gt;`; };  // 设置图片内容，统一显示一张缓存图，用于懒加载~ rendererMD.image = (href, _, text) =&amp;gt;   `&amp;lt;img data-src=&amp;quot;${href}&amp;quot; src=&amp;quot;/cos/2020/1211175603.png&amp;quot; alt=&amp;quot;${text}&amp;quot; &amp;gt;`;  marked.setOptions({   highlight(code) {     return hljs.highlightAuto(code).value   },   renderer: rendererMD });  // 这里的html就是插入到页面的元素文本了 const html = marked('## md内容'); &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;2.3 标题导航实现&lt;/h3&gt; &lt;p&gt;上面的例子&lt;code&gt;headstemp&lt;/code&gt;记录了解析过程中的所有标题，作用是借助UI库的组件&lt;code&gt;Anchor&lt;/code&gt;，构建一个标题导航。&lt;/p&gt; &lt;p&gt;下面演示一个基于&lt;code&gt;ant-design-vue&lt;/code&gt;的版本，如果你使用的UI库是类似的锚点组件，那么代码将只需要小改动即可。代码使用&lt;code&gt;jsx&lt;/code&gt;语法，&lt;code&gt;vue&lt;/code&gt;模板语法请自行分离代码~&lt;/p&gt; &lt;p&gt;Recursive.tsx 导航中的链接组件&lt;/p&gt; &lt;pre&gt;&lt;code class="language-jsx"&gt;import { Anchor } from 'ant-design-vue'; import { defineComponent, PropType } from 'vue';  const { Link } = Anchor;  export interface Head {   text: string;   level: number; }  export interface TocItem extends Head {   anchor: string;   children?: Array&amp;lt;TocItem&amp;gt;; }  const Recursive = defineComponent({   props: {     tocItem: {       type: Object as PropType&amp;lt;TocItem&amp;gt;,       default: () =&amp;gt; []     }   },   setup({ tocItem }) {     return (       &amp;lt;Link href={`#${tocItem.anchor}`} title={tocItem.text}&amp;gt;         {tocItem.children &amp;amp;&amp;amp;           tocItem.children.map((item) =&amp;gt; &amp;lt;Recursive key={item.anchor} tocItem={item} /&amp;gt;)}       &amp;lt;/Link&amp;gt;     );   } });  export default Recursive; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Topicfy.tsx 用于生成整个导航内容&lt;/p&gt; &lt;pre&gt;&lt;code class="language-jsx"&gt;import { Anchor } from 'ant-design-vue'; import { computed, defineComponent, PropType, ref, watch } from 'vue';  import Recursive, { Head, TocItem } from './Recursive';  const Topicfy = defineComponent({   props: {     // 解析得到的标题列表     heads: {       type: Array as PropType&amp;lt;Array&amp;lt;Head&amp;gt;&amp;gt;     }   },   setup(props) {     const topics = computed(() =&amp;gt; {       const tocItems: TocItem[] = [];        // 标题计数器       let count = 0;        const add = (text: string, level: number) =&amp;gt; {         count++;          const item = { anchor: `heading-${count}`, level, text };          if (tocItems.length === 0) {           // 第一个 item 直接 push           tocItems.push(item);         } else {           let lastItem = tocItems[tocItems.length - 1]; // 最后一个 item            if (item.level &amp;gt; lastItem.level) {             // item 是 lastItem 的 children             for (let i = lastItem.level + 1; i &amp;lt;= 6; i++) {               const { children } = lastItem;               if (!children) {                 // 如果 children 不存在                 lastItem.children = [item];                 break;               }               // 重置 lastItem 为 children 的最后一个 item               lastItem = children[children.length - 1];                if (item.level &amp;lt;= lastItem.level) {                 // item level 小于或等于 lastItem level 都视为与 children 同级                 children.push(item);                 break;               }             }           } else {             // 置于最顶级             tocItems.push(item);           }         }       };        props.heads?.forEach((item) =&amp;gt; {         add(item.text, item.level);       });       return tocItems;     });      return () =&amp;gt; (       &amp;lt;Anchor affix={false} showInkInFixed={true}&amp;gt;         {topics.value.map((item) =&amp;gt; (           &amp;lt;Recursive key={item.anchor} tocItem={item} /&amp;gt;         ))}       &amp;lt;/Anchor&amp;gt;     );   } });  export default Topicfy; &lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt; &lt;p&gt;该组件是19年参考了网络上的实现完成的，非本人完全原创，react版本参考&lt;a href="https://github.com/imzbf/blog-template-nextjs/tree/master/src/components/Topicfy" target="_blank"&gt;Topicfy&lt;/a&gt;&lt;/p&gt; &lt;/blockquote&gt; &lt;h4&gt;2.4 获取html代码&lt;/h4&gt; &lt;p&gt;编辑器考虑到了可能后端不存储&lt;code&gt;md&lt;/code&gt;格式的文本，而是&lt;code&gt;html&lt;/code&gt;内容，所以提供了&lt;code&gt;onHtmlChanged&lt;/code&gt;方法，用于编辑内容变化后，&lt;code&gt;marked&lt;/code&gt;编译了内容的回调，入参即是&lt;code&gt;html&lt;/code&gt;内容。&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt;   &amp;lt;md-editor     v-model=&amp;quot;text&amp;quot;     @onHtmlChanged=&amp;quot;saveHtml&amp;quot;   /&amp;gt; &amp;lt;/template&amp;gt;  &amp;lt;script lang=&amp;quot;ts&amp;quot;&amp;gt; import { defineComponent } from 'vue'; import MdEditor from 'md-editor-v3'; import 'md-editor-v3/lib/style.css';  export default defineComponent({   components: { MdEditor },   data() { return { text: '' }; },   methods: { saveHtml(h: string) { console.log(h) }} }); &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;&lt;code&gt;jsx&lt;/code&gt;语法相同。&lt;/p&gt; &lt;h2&gt;3. 编辑器的功能演示&lt;/h2&gt; &lt;h3&gt;3.1 扩展库链接&lt;/h3&gt; &lt;p&gt;编辑器扩展内容大多使用了&lt;code&gt;cdn&lt;/code&gt;，考虑了无外网情况，支持了内网链接扩展，演示（假设外部库都在根目录下）：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt;   &amp;lt;md-editor     v-model=&amp;quot;text&amp;quot;     highlightJs=&amp;quot;/highlight.min.js&amp;quot;     highlightCss=&amp;quot;/atom-one-dark.min.css&amp;quot;     prettierCDN=&amp;quot;/standalone.js&amp;quot;     prettierMDCDN=&amp;quot;/parser-markdown.js&amp;quot;     cropperJs=&amp;quot;/cropper.min.js&amp;quot;     cropperCss=&amp;quot;/cropper.min.css&amp;quot;   /&amp;gt; &amp;lt;/template&amp;gt;  &amp;lt;script lang=&amp;quot;ts&amp;quot;&amp;gt; import { defineComponent } from 'vue'; import MdEditor from 'md-editor-v3'; import 'md-editor-v3/lib/style.css';  export default defineComponent({   components: { MdEditor },   data() { return { text: '' }; } }); &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt; &lt;p&gt;v1.2.0版本目前支持上述链接，图标链接将在后续补丁中添加。&lt;/p&gt; &lt;/blockquote&gt; &lt;h3&gt;3.2 工具栏自定义&lt;/h3&gt; &lt;p&gt;默认的全部工具栏，并且每个功能都绑定了快捷键，如果需要选择性显示工具栏，提供了两个api：&lt;code&gt;toolbars&lt;/code&gt;和&lt;code&gt;toolbarsExclude&lt;/code&gt;，前者显示数组中的全部，后者屏蔽数组中的全部，后者的权重更大。下面是个参考：&lt;/p&gt; &lt;p&gt;案例不显示&lt;code&gt;github&lt;/code&gt;按钮&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt;   &amp;lt;md-editor     v-model=&amp;quot;text&amp;quot;     :toolbars=&amp;quot;toobars&amp;quot;   /&amp;gt;      &amp;lt;md-editor     v-model=&amp;quot;text&amp;quot;     :toolbarsExclude=&amp;quot;toolbarsExclude&amp;quot;   /&amp;gt; &amp;lt;/template&amp;gt;  &amp;lt;script lang=&amp;quot;ts&amp;quot;&amp;gt; import { defineComponent } from 'vue'; import MdEditor from 'md-editor-v3'; import 'md-editor-v3/lib/style.css';  export default defineComponent({   components: { MdEditor },   data() {     return {       text: '',       toobars: ['bold', 'underline', 'italic', 'strikeThrough',       'sub','sup','quote','unorderedList', 'orderedList', 'codeRow',       'code', 'link', 'image', 'table', 'revoke',       'next', 'save', 'pageFullscreen', 'fullscreen',       'preview', 'htmlPreview'],       toolbarsExclude: ['github']     };   } }); &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;3.3 扩展语言&lt;/h3&gt; &lt;p&gt;编辑器默认内置了中文和英文，并且两者都可以通过扩展api覆盖，该功能主要用来设置内容提示，比如弹窗中的标题等。&lt;/p&gt; &lt;p&gt;扩展一门语言，我们取名为&lt;code&gt;zh-NB&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt;   &amp;lt;md-editor     v-model=&amp;quot;text&amp;quot;     :language=&amp;quot;language&amp;quot;     :languageUserDefined=&amp;quot;languageUserDefined&amp;quot;   /&amp;gt; &amp;lt;/template&amp;gt;  &amp;lt;script lang=&amp;quot;ts&amp;quot;&amp;gt; import { defineComponent } from 'vue'; import MdEditor, { StaticTextDefaultValue } from 'md-editor-v3'; import 'md-editor-v3/lib/style.css';  const languageUserDefined: { 'zh-NB': StaticTextDefaultValue } = {   'zh-NB': {     toolbarTips: {       bold: '加粗',       underline: '下划线',       italic: '斜体',       strikeThrough: '删除线',       title: '标题',       sub: '下标',       sup: '上标',       quote: '引用',       unorderedList: '无序列表',       orderedList: '有序列表',       codeRow: '行内代码',       code: '块级代码',       link: '链接',       image: '图片',       table: '表格',       revoke: '后退',       next: '前进',       save: '保存',       prettier: '美化',       pageFullscreen: '浏览器全屏',       fullscreen: '屏幕全屏',       preview: '预览',       htmlPreview: 'html代码预览',       github: '源码地址'     },     titleItem: {       h1: '一级标题',       h2: '二级标题',       h3: '三级标题',       h4: '四级标题',       h5: '五级标题',       h6: '六级标题'     },     linkModalTips: {       title: '添加',       descLable: '链接描述：',       descLablePlaceHolder: '请输入描述...',       urlLable: '链接地址：',       UrlLablePlaceHolder: '请输入链接...',       buttonOK: '确定',       buttonUpload: '上传'     },     // v1.2.0新增     clipModalTips: {       title: '裁剪图片上传',       buttonUpload: '上传'     },     // v1.1.4新增     copyCode: {       text: '复制代码',       tips: '已复制'     }   } };  export default defineComponent({   components: { MdEditor },   data() {     return {       text: '',       language: &amp;quot;zh-NB&amp;quot;,       languageUserDefined     };   } }); &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt; &lt;p&gt;如果key = 'zh-CN'，就可以实现中文覆盖，依次类推。&lt;/p&gt; &lt;/blockquote&gt; &lt;h3&gt;3.4 主题切换&lt;/h3&gt; &lt;p&gt;这一块相对比较简单了，内置了&lt;strong&gt;暗黑主题&lt;/strong&gt;和&lt;strong&gt;默认主题&lt;/strong&gt;，通过&lt;code&gt;theme&lt;/code&gt;api切换，demo如下：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt;   &amp;lt;md-editor     v-model=&amp;quot;text&amp;quot;     :theme=&amp;quot;theme&amp;quot;   /&amp;gt; &amp;lt;/template&amp;gt;  &amp;lt;script lang=&amp;quot;ts&amp;quot;&amp;gt; import { defineComponent } from 'vue'; import MdEditor from 'md-editor-v3'; import 'md-editor-v3/lib/style.css';  export default defineComponent({   components: { MdEditor },   data() {     return {       text: '',       theme: 'dark'     };   } }); &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Fri, 14 Jan 2022 09:49:00 GMT</pubDate>
    </item>
    <item>
      <title>Spring Cloud 多网卡环境下Eureka服务注册IP选择问题</title>
      <link>/article/1544</link>
      <content:encoded>&lt;h2&gt;问题场景&lt;/h2&gt; &lt;p&gt;服务器上分别配置了eth0, eth1和eth2三块网卡，只有eth1的地址可供其它机器访问，eth0和eth2的 IP 无效。在这种情况下，服务注册时Eureka Client会自动选择eth0作为服务ip, 导致其它服务无法调用&lt;/p&gt; &lt;p&gt;由于官方并没有写明Eureka Client探测本机IP的逻辑，所以只能翻阅源代码。Eureka Client的源码在eureka-client模块下，com.netflix.appinfo包下的InstanceInfo类封装了本机信息，其中就包括了IP地址。在 Spring Cloud 环境下，Eureka Client并没有自己实现探测本机IP的逻辑，而是交给Spring的InetUtils工具类的findFirstNonLoopbackAddress()方法完成的。&lt;/p&gt; &lt;h2&gt;解决方案&lt;/h2&gt; &lt;p&gt;通过上面源码分析可以得知，spring cloud肯定能配置一个网卡忽略列表。通过查文档资料得知确实存在该属性：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-properties"&gt;spring.cloud.inetutils.ignored-interfaces[0]=eth0 # 忽略eth0, 支持正则表达式 &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;因此，第一种方案就是通过配置application.properties让应用忽略无效的网卡。&lt;/p&gt; &lt;h2&gt;配置host&lt;/h2&gt; &lt;p&gt;当网查遍历逻辑都没有找到合适ip时会走JDK的InetAddress.getLocalHost()。该方法会返回当前主机的hostname, 然后会根据hostname解析出对应的ip。因此第二种方案就是配置本机的hostname和/etc/hosts文件，直接将本机的主机名映射到有效IP地址。&lt;/p&gt; &lt;h2&gt;手工指定IP(推荐)&lt;/h2&gt; &lt;p&gt;添加以下配置：&lt;/p&gt; &lt;pre&gt;&lt;code class="language-yml"&gt;eureka:   instance:     hostname: 192.168.100.201     # 指定此实例的ip     ip-address: 192.168.100.201     # 注册时使用ip而不是主机名     prefer-ip-address: true &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Tue, 11 Jan 2022 06:47:00 GMT</pubDate>
    </item>
    <item>
      <title>面试官问我：一个 TCP 连接可以发多少个 HTTP 请求？我竟然回答不上来...</title>
      <link>/article/1543</link>
      <content:encoded>&lt;h3&gt;&lt;strong&gt;正文&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;曾经有这么一道经典面试题：从 URL 在浏览器被被输入到页面展现的过程中发生了什么？&lt;/p&gt; &lt;p&gt;相信大多数准备过的同学都能回答出来，但是如果继续问：收到的 HTML 如果包含几十个图片标签，这些图片是以什么方式、什么顺序、建立了多少连接、使用什么协议被下载下来的呢？&lt;/p&gt; &lt;p&gt;要搞懂这个问题，我们需要先解决下面五个问题：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;现代浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开？什么情况下会断开？&lt;/li&gt; &lt;li&gt;一个 TCP 连接可以对应几个 HTTP 请求？&lt;/li&gt; &lt;li&gt;一个 TCP 连接中 HTTP 请求发送可以一起发送么（比如一起发三个请求，再三个响应一起接收）？&lt;/li&gt; &lt;li&gt;为什么有的时候刷新页面不需要重新建立 SSL 连接？&lt;/li&gt; &lt;li&gt;浏览器对同一 Host 建立 TCP 连接到数量有没有限制？&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;&lt;strong&gt;第一个问题&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;现代浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开？什么情况下会断开？&lt;/p&gt; &lt;p&gt;在 HTTP/1.0 中，一个服务器在发送完一个 HTTP 响应后，会断开 TCP 链接。但是这样每次请求都会重新建立和断开 TCP 连接，代价过大。所以虽然标准中没有设定，某些服务器对 Connection: keep-alive 的 Header 进行了支持。意思是说，完成这个 HTTP 请求之后，不要断开 HTTP 请求使用的 TCP 连接。这样的好处是连接可以被重新使用，之后发送 HTTP 请求的时候不需要重新建立 TCP 连接，以及如果维持连接，那么 SSL 的开销也可以避免，两张图片是我短时间内两次访问 https://www.github.com 的时间统计： &lt;img src="http://www.yanzuoguang.com/upload/2022/01/rg8hkr92rijcmq504co0kbn483.webp" alt="1" /&gt;&lt;/p&gt; &lt;p&gt;头一次访问，有初始化连接和 SSL 开销 &lt;img src="http://www.yanzuoguang.com/upload/2022/01/5sj4i74klojvlqo4nah5eaatnb.webp" alt="2" /&gt;&lt;/p&gt; &lt;p&gt;初始化连接和 SSL 开销消失了，说明使用的是同一个 TCP 连接&lt;/p&gt; &lt;p&gt;持久连接：既然维持 TCP 连接好处这么多，HTTP/1.1 就把 Connection 头写进标准，并且默认开启持久连接，除非请求中写明 Connection: close，那么浏览器和服务器之间是会维持一段时间的 TCP 连接，不会一个请求结束就断掉。&lt;/p&gt; &lt;p&gt;所以第一个问题的答案是：默认情况下建立 TCP 连接不会断开，只有在请求报头中声明 Connection: close 才会在请求完成后关闭连接。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;第二个问题&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;一个 TCP 连接可以对应几个 HTTP 请求？&lt;/p&gt; &lt;p&gt;了解了第一个问题之后，其实这个问题已经有了答案，如果维持连接，一个 TCP 连接是可以发送多个 HTTP 请求的。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;第三个问题&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;一个 TCP 连接中 HTTP 请求发送可以一起发送么（比如一起发三个请求，再三个响应一起接收）？&lt;/p&gt; &lt;p&gt;HTTP/1.1 存在一个问题，单个 TCP 连接在同一时刻只能处理一个请求，意思是说：两个请求的生命周期不能重叠，任意两个 HTTP 请求从开始到结束的时间在同一个 TCP 连接里不能重叠。&lt;/p&gt; &lt;p&gt;虽然 HTTP/1.1 规范中规定了 Pipelining 来试图解决这个问题，但是这个功能在浏览器中默认是关闭的。&lt;/p&gt; &lt;p&gt;先来看一下 Pipelining 是什么，RFC 2616 中规定了：&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;A client that supports persistent connections MAY &amp;quot;pipeline&amp;quot; its requests (i.e., send multiple requests without waiting for each response). A server MUST send its responses to those requests in the same order that the requests were received. 一个支持持久连接的客户端可以在一个连接中发送多个请求（不需要等待任意请求的响应）。收到请求的服务器必须按照请求收到的顺序发送响应。&lt;/p&gt; &lt;/blockquote&gt; &lt;p&gt;至于标准为什么这么设定，我们可以大概推测一个原因：由于 HTTP/1.1 是个文本协议，同时返回的内容也并不能区分对应于哪个发送的请求，所以顺序必须维持一致。比如你向服务器发送了两个请求 &lt;code&gt;GET/query?q=A&lt;/code&gt; 和 &lt;code&gt;GET/query?q=B&lt;/code&gt;，服务器返回了两个结果，浏览器是没有办法根据响应结果来判断响应对应于哪一个请求的。&lt;/p&gt; &lt;p&gt;Pipelining 这种设想看起来比较美好，但是在实践中会出现许多问题：&lt;/p&gt; &lt;ul&gt; &lt;li&gt;一些代理服务器不能正确的处理 HTTP Pipelining。&lt;/li&gt; &lt;li&gt;正确的流水线实现是复杂的。&lt;/li&gt; &lt;li&gt;Head-of-line Blocking 连接头阻塞：在建立起一个 TCP 连接之后，假设客户端在这个连接连续向服务器发送了几个请求。按照标准，服务器应该按照收到请求的顺序返回结果，假设服务器在处理首个请求时花费了大量时间，那么后面所有的请求都需要等着首个请求结束才能响应。&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;所以现代浏览器默认是不开启 HTTP Pipelining 的。&lt;/p&gt; &lt;p&gt;但是，HTTP2 提供了 Multiplexing 多路传输特性，可以在一个 TCP 连接中同时完成多个 HTTP 请求。至于 Multiplexing 具体怎么实现的就是另一个问题了。我们可以看一下使用 HTTP2 的效果。 &lt;img src="http://www.yanzuoguang.com/upload/2022/01/0tk2j0oqugjgaqjbf7ivnfiqt1.webp" alt="3" /&gt;&lt;/p&gt; &lt;p&gt;绿色是发起请求到请求返回的等待时间，蓝色是响应的下载时间，可以看到都是在同一个 Connection，并行完成的&lt;/p&gt; &lt;p&gt;所以这个问题也有了答案：在 HTTP/1.1 存在 Pipelining 技术可以完成这个多个请求同时发送，但是由于浏览器默认关闭，所以可以认为这是不可行的。在 HTTP2 中由于 Multiplexing 特点的存在，多个 HTTP 请求可以在同一个 TCP 连接中并行进行。&lt;/p&gt; &lt;p&gt;那么在 HTTP/1.1 时代，浏览器是如何提高页面加载效率的呢？主要有下面两点：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;维持和服务器已经建立的 TCP 连接，在同一连接上顺序处理多个请求。&lt;/li&gt; &lt;li&gt;和服务器建立多个 TCP 连接。&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;&lt;strong&gt;第四个问题&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;为什么有的时候刷新页面不需要重新建立 SSL 连接？&lt;/p&gt; &lt;p&gt;在第一个问题的讨论中已经有答案了，TCP 连接有的时候会被浏览器和服务端维持一段时间。TCP 不需要重新建立，SSL 自然也会用之前的。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;第五个问题&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;浏览器对同一 Host 建立 TCP 连接到数量有没有限制？&lt;/p&gt; &lt;p&gt;假设我们还处在 HTTP/1.1 时代，那个时候没有多路传输，当浏览器拿到一个有几十张图片的网页该怎么办呢？肯定不能只开一个 TCP 连接顺序下载，那样用户肯定等的很难受，但是如果每个图片都开一个 TCP 连接发 HTTP 请求，那电脑或者服务器都可能受不了，要是有 1000 张图片的话总不能开 1000 个TCP 连接吧，你的电脑同意 NAT 也不一定会同意。&lt;/p&gt; &lt;p&gt;所以答案是：有。Chrome 最多允许对同一个 Host 建立六个 TCP 连接。不同的浏览器有一些区别。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;那么回到最开始的问题，收到的 HTML 如果包含几十个图片标签，这些图片是以什么方式、什么顺序、建立了多少连接、使用什么协议被下载下来的呢？&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;如果图片都是 HTTPS 连接并且在同一个域名下，那么浏览器在 SSL 握手之后会和服务器商量能不能用 HTTP2，如果能的话就使用 Multiplexing 功能在这个连接上进行多路传输。不过也未必会所有挂在这个域名的资源都会使用一个 TCP 连接去获取，但是可以确定的是 Multiplexing 很可能会被用到。&lt;/p&gt; &lt;p&gt;如果发现用不了 HTTP2 呢？或者用不了 HTTPS（现实中的 HTTP2 都是在 HTTPS 上实现的，所以也就是只能使用 HTTP/1.1）。那浏览器就会在一个 HOST 上建立多个 TCP 连接，连接数量的最大限制取决于浏览器设置，这些连接会在空闲的时候被浏览器用来发送新的请求，如果所有的连接都正在发送请求呢？那其他的请求就只能等等了。&lt;/p&gt;</content:encoded>
      <pubDate>Tue, 11 Jan 2022 06:40:00 GMT</pubDate>
    </item>
    <item>
      <title>微信支持H5跳转App、跳转小程序</title>
      <link>/article/1542</link>
      <content:encoded>&lt;p&gt;继小程序灰度测试分享朋友圈刷屏后，滴滴滴，这...&lt;img src="http://www.yanzuoguang.com/upload/2022/01/vprio82saggv0qcgpcabsaa15r.webp" alt="1" /&gt;&lt;/p&gt; &lt;p&gt;闲着也闲着，顺藤摸瓜点了进去，好家伙，产品小姐姐写这个文案还是太含蓄了，我猜有可能是老干妈还没吃够&lt;/p&gt; &lt;p&gt;根据刀哥多年写代码要看文档的经验来看，证实了这次更新不仅支持了打开小程序，连app也顺带支持了，这个信息量着实有点大,下面让我们一起来看文档.&lt;/p&gt; &lt;h2&gt;新增开放标签&lt;/h2&gt; &lt;blockquote&gt; &lt;p&gt;定义: 微信开放标签是微信公众平台面向网页开发者提供的扩展标签集合。通过使用微信开放标签，网页开发者可安全便捷地使用微信或系统的能力，为微信用户提供更优质的网页体验&lt;/p&gt; &lt;/blockquote&gt; &lt;h4&gt;有哪些开放标签?&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;&lt;code&gt;&amp;lt;wx-open-launch-weapp&amp;gt;&lt;/code&gt; 打开小程序&lt;/li&gt; &lt;li&gt;&lt;code&gt;&amp;lt;wx-open-launch-app&amp;gt;&lt;/code&gt; 打开app&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;接入要求&lt;/h4&gt; &lt;ul&gt; &lt;li&gt;主体要求: 仅开放给已认证的服务号&lt;/li&gt; &lt;li&gt;系统要求： 微信版本要求为：7.0.12及以上。系统版本要求为：iOS 10.3及以上、Android 5.0及以上&lt;/li&gt; &lt;/ul&gt; &lt;h4&gt;接入方法&lt;/h4&gt; &lt;p&gt;打开小程序使用步骤与微信JS-SDK类似，需要绑定安全域名、引入JS文件等步骤&lt;/p&gt; &lt;p&gt;打开app需要前往微信开放平台的管理中心-公众账号或小程序详情-接口信息-网页跳转移动应用-关联设置中绑定所需要跳转的App&lt;/p&gt; &lt;p&gt;示例:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;配置最新JSSDK&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-js"&gt;wx.config({   // 其它配置不变， openTagList配置可使用的开放标签,    openTagList: ['wx-open-launch-app, wx-open-launch-weapp'] }) &lt;/code&gt;&lt;/pre&gt; &lt;ol&gt; &lt;li&gt;页面使用开放标签&lt;/li&gt; &lt;/ol&gt; &lt;ul&gt; &lt;li&gt;打开小程序&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;wx-open-launch-weapp   id=&amp;quot;launch-btn&amp;quot;   // 小程序id   username=&amp;quot;gh_xxxxxxxx&amp;quot;   // 小程序路径   path=&amp;quot;/a/b/c?d=1&amp;amp;e=2#fg&amp;quot; &amp;gt;   &amp;lt;template&amp;gt;     &amp;lt;style&amp;gt;.btn { padding: 12px }&amp;lt;/style&amp;gt;     &amp;lt;button class=&amp;quot;btn&amp;quot;&amp;gt;打开小程序&amp;lt;button&amp;gt;   &amp;lt;/template&amp;gt; &amp;lt;/wx-open-launch-weapp&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;打开app&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;wx-open-launch-app   id=&amp;quot;launch-btn&amp;quot;   // 移动应用appid   appid=&amp;quot;your-appid&amp;quot;   // 额外信息, 客户端自行解析   extinfo=&amp;quot;your-extinfo&amp;quot; &amp;gt;   &amp;lt;template&amp;gt;     &amp;lt;style&amp;gt;.btn { padding: 12px }&amp;lt;/style&amp;gt;     &amp;lt;button class=&amp;quot;btn&amp;quot;&amp;gt;App内查看&amp;lt;/button&amp;gt;   &amp;lt;/template&amp;gt; &amp;lt;/wx-open-launch-app&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;ul&gt; &lt;li&gt;回调&lt;/li&gt; &lt;/ul&gt; &lt;pre&gt;&lt;code class="language-html"&gt;&amp;lt;script&amp;gt;   var btn = document.getElementById('launch-btn');   // 调起成功回调   btn.addEventListener('launch', function (e) {     console.log('success');   });   // 调起失败回调   btn.addEventListener('error', function (e) {     console.log('fail', e.detail);   }); &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt;</content:encoded>
      <pubDate>Tue, 11 Jan 2022 06:28:00 GMT</pubDate>
    </item>
    <item>
      <title>HTTPS就安全了吗？会被抓包吗？看完这篇你有对答如流</title>
      <link>/article/1541</link>
      <content:encoded>&lt;h3&gt;&lt;strong&gt;HTTPS&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;随着 HTTPS 建站的成本下降，现在大部分的网站都已经开始用上 HTTPS 协议。大家都知道 HTTPS 比 HTTP 安全，也听说过与 HTTPS 协议相关的概念有 SSL 、非对称加密、 CA证书等，但对于以下灵魂三拷问可能就答不上了：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;为什么用了 HTTPS 就是安全的？&lt;/li&gt; &lt;li&gt;HTTPS 的底层原理如何实现？&lt;/li&gt; &lt;li&gt;用了 HTTPS 就一定安全吗？&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;本文将层层深入，从原理上把 HTTPS 的安全性讲透。&lt;/p&gt; &lt;p&gt;去年推送的一篇技术文，没看的可以了解下（&lt;a href="http://mp.weixin.qq.com/s?__biz=MzI5ODI5NDkxMw==&amp;amp;mid=2247489132&amp;amp;idx=1&amp;amp;sn=c15c4bf118abad5bea9afc287585f760&amp;amp;chksm=eca95d82dbded494d33755649ad9879e32a3fe8b287cb2ecadb173238aa4ac65df3b6cf16aa7&amp;amp;scene=21#wechat_redirect" target="_blank"&gt;面试官问我：一个 TCP 连接可以发多少个 HTTP 请求？我竟然回答不上来...&lt;/a&gt;）这篇文章阅读量9千+。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;HTTPS 的实现原理&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;大家可能都听说过 HTTPS 协议之所以是安全的是因为 HTTPS 协议会对传输的数据进行加密，而加密过程是使用了非对称加密实现。但其实，HTTPS 在内容传输的加密上使用的是对称加密，非对称加密只作用在证书验证阶段。&lt;/p&gt; &lt;p&gt;相关文章：&lt;/p&gt; &lt;ul&gt; &lt;li&gt; &lt;p&gt;&lt;a href="http://mp.weixin.qq.com/s?__biz=MzI5ODI5NDkxMw==&amp;amp;mid=2247492349&amp;amp;idx=1&amp;amp;sn=a338476276aead0654bc22bdbbc27682&amp;amp;chksm=ecaaa913dbdd20050c54422158ce78e0a412ce4237268be0c9d7bf72f03e8addd2e02d1a3f40&amp;amp;scene=21#wechat_redirect" target="_blank"&gt;你连 HTTPS 原理都不懂，还讲“中间人攻击”？&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;a href="http://mp.weixin.qq.com/s?__biz=MzI5ODI5NDkxMw==&amp;amp;mid=2247491506&amp;amp;idx=2&amp;amp;sn=45d3c0a831abed3d80fe56ac293a8724&amp;amp;chksm=eca9545cdbdedd4a279dc5ca11dfeee981b316d152867365614a53a10cbfd1810823c4e075ff&amp;amp;scene=21#wechat_redirect" target="_blank"&gt;HTTPS 原理分析：带着疑问层层深入&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;&lt;a href="http://mp.weixin.qq.com/s?__biz=MzI5ODI5NDkxMw==&amp;amp;mid=2247488437&amp;amp;idx=1&amp;amp;sn=1bd2e7dec37999efae9a3d35ceb15605&amp;amp;chksm=eca9585bdbded14d0f198ce352ff3678d025b84eeabb4a354e08ee3df4212f8c615f2a34bb80&amp;amp;scene=21#wechat_redirect" target="_blank"&gt;HTTPS加密原理&lt;/a&gt;&lt;/p&gt; &lt;/li&gt; &lt;/ul&gt; &lt;p&gt;HTTPS的整体过程分为证书验证和数据传输阶段，具体的交互过程如下：&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/01/2o61muap56j8uok38fh0euh0cn.webp" alt="1" /&gt;&lt;/p&gt; &lt;p&gt;&lt;strong&gt;① 证书验证阶段&lt;/strong&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;浏览器发起 HTTPS 请求&lt;/li&gt; &lt;li&gt;服务端返回 HTTPS 证书&lt;/li&gt; &lt;li&gt;客户端验证证书是否合法，如果不合法则提示告警&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;&lt;strong&gt;② 数据传输阶段&lt;/strong&gt;&lt;/p&gt; &lt;ol&gt; &lt;li&gt;当证书验证合法后，在本地生成随机数&lt;/li&gt; &lt;li&gt;通过公钥加密随机数，并把加密后的随机数传输到服务端&lt;/li&gt; &lt;li&gt;服务端通过私钥对随机数进行解密&lt;/li&gt; &lt;li&gt;服务端通过客户端传入的随机数构造对称加密算法，对返回结果内容进行加密后传输&lt;/li&gt; &lt;/ol&gt; &lt;h4&gt;&lt;/h4&gt; &lt;h4&gt;&lt;strong&gt;为什么数据传输是用对称加密？&lt;/strong&gt;&lt;/h4&gt; &lt;p&gt;首先，非对称加密的加解密效率是非常低的，而 http 的应用场景中通常端与端之间存在大量的交互，非对称加密的效率是无法接受的；&lt;/p&gt; &lt;p&gt;另外，在 HTTPS 的场景中只有服务端保存了私钥，一对公私钥只能实现单向的加解密，所以 HTTPS 中内容传输加密采取的是对称加密，而不是非对称加密。&lt;/p&gt; &lt;h4&gt;&lt;strong&gt;为什么需要 CA 认证机构颁发证书？&lt;/strong&gt;&lt;/h4&gt; &lt;p&gt;HTTP 协议被认为不安全是因为传输过程容易被监听者勾线监听、伪造服务器，而 HTTPS 协议主要解决的便是网络传输的安全性问题。&lt;/p&gt; &lt;p&gt;首先我们假设不存在认证机构，任何人都可以制作证书，这带来的安全风险便是经典的 &lt;strong&gt;“中间人攻击”&lt;/strong&gt; 问题。 “中间人攻击”的具体过程如下：&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/01/2tr4594o5ghevqsm33i7guimi8.webp" alt="2" /&gt;WX20191126-212406@2x.png&lt;/p&gt; &lt;p&gt;过程原理：&lt;/p&gt; &lt;ol&gt; &lt;li&gt;本地请求被劫持（如DNS劫持等），所有请求均发送到中间人的服务器&lt;/li&gt; &lt;li&gt;中间人服务器返回中间人自己的证书&lt;/li&gt; &lt;li&gt;客户端创建随机数，通过中间人证书的公钥对随机数加密后传送给中间人，然后凭随机数构造对称加密对传输内容进行加密传输&lt;/li&gt; &lt;li&gt;中间人因为拥有客户端的随机数，可以通过对称加密算法进行内容解密&lt;/li&gt; &lt;li&gt;中间人以客户端的请求内容再向正规网站发起请求&lt;/li&gt; &lt;li&gt;因为中间人与服务器的通信过程是合法的，正规网站通过建立的安全通道返回加密后的数据&lt;/li&gt; &lt;li&gt;中间人凭借与正规网站建立的对称加密算法对内容进行解密&lt;/li&gt; &lt;li&gt;中间人通过与客户端建立的对称加密算法对正规内容返回的数据进行加密传输&lt;/li&gt; &lt;li&gt;客户端通过与中间人建立的对称加密算法对返回结果数据进行解密&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;由于缺少对证书的验证，所以客户端虽然发起的是 HTTPS 请求，但客户端完全不知道自己的网络已被拦截，传输内容被中间人全部窃取。&lt;/p&gt; &lt;h4&gt;&lt;strong&gt;浏览器是如何确保 CA 证书的合法性？&lt;/strong&gt;&lt;/h4&gt; &lt;h5&gt;1. 证书包含什么信息？&lt;/h5&gt; &lt;ul&gt; &lt;li&gt;颁发机构信息&lt;/li&gt; &lt;li&gt;公钥&lt;/li&gt; &lt;li&gt;公司信息&lt;/li&gt; &lt;li&gt;域名&lt;/li&gt; &lt;li&gt;有效期&lt;/li&gt; &lt;li&gt;指纹&lt;/li&gt; &lt;li&gt;……&lt;/li&gt; &lt;/ul&gt; &lt;h5&gt;2. 证书的合法性依据是什么？&lt;/h5&gt; &lt;p&gt;首先，权威机构是要有认证的，不是随便一个机构都有资格颁发证书，不然也不叫做权威机构。另外，证书的可信性基于信任制，权威机构需要对其颁发的证书进行信用背书，只要是权威机构生成的证书，我们就认为是合法的。所以权威机构会对申请者的信息进行审核，不同等级的权威机构对审核的要求也不一样，于是证书也分为免费的、便宜的和贵的。&lt;/p&gt; &lt;h5&gt;3. 浏览器如何验证证书的合法性？&lt;/h5&gt; &lt;p&gt;浏览器发起 HTTPS 请求时，服务器会返回网站的 SSL 证书，浏览器需要对证书做以下验证：&lt;/p&gt; &lt;ol&gt; &lt;li&gt; &lt;p&gt;验证域名、有效期等信息是否正确。证书上都有包含这些信息，比较容易完成验证；&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;判断证书来源是否合法。每份签发证书都可以根据验证链查找到对应的根证书，操作系统、浏览器会在本地存储权威机构的根证书，利用本地根证书可以对对应机构签发证书完成来源验证；&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/01/ofvru9p1iuhhkrvb4kso33ccoo.webp" alt="3" /&gt;WX20191127-084216@2x.png&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;判断证书是否被篡改。需要与 CA 服务器进行校验；&lt;/p&gt; &lt;/li&gt; &lt;li&gt; &lt;p&gt;判断证书是否已吊销。通过CRL（Certificate Revocation List 证书注销列表）和 OCSP（Online Certificate Status Protocol 在线证书状态协议）实现，其中 OCSP 可用于第3步中以减少与 CA 服务器的交互，提高验证效率&lt;/p&gt; &lt;/li&gt; &lt;/ol&gt; &lt;p&gt;以上任意一步都满足的情况下浏览器才认为证书是合法的。&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;这里插一个我想了很久的但其实答案很简单的问题： 既然证书是公开的，如果要发起中间人攻击，我在官网上下载一份证书作为我的服务器证书，那客户端肯定会认同这个证书是合法的，如何避免这种证书冒用的情况？&lt;/p&gt; &lt;p&gt;其实这就是非加密对称中公私钥的用处，虽然中间人可以得到证书，但私钥是无法获取的，一份公钥是不可能推算出其对应的私钥，中间人即使拿到证书也无法伪装成合法服务端，因为无法对客户端传入的加密数据进行解密。&lt;/p&gt; &lt;/blockquote&gt; &lt;h5&gt;4. 只有认证机构可以生成证书吗？&lt;/h5&gt; &lt;p&gt;如果需要浏览器不提示安全风险，那只能使用认证机构签发的证书。但浏览器通常只是提示安全风险，并不限制网站不能访问，所以从技术上谁都可以生成证书，只要有证书就可以完成网站的 HTTPS 传输。例如早期的 12306 采用的便是手动安装私有证书的形式实现 HTTPS 访问。&lt;/p&gt; &lt;h4&gt;&lt;strong&gt;本地随机数被窃取怎么办？&lt;/strong&gt;&lt;/h4&gt; &lt;p&gt;证书验证是采用非对称加密实现，但是传输过程是采用对称加密，而其中对称加密算法中重要的随机数是由本地生成并且存储于本地的，HTTPS 如何保证随机数不会被窃取？&lt;/p&gt; &lt;p&gt;其实 HTTPS 并不包含对随机数的安全保证，HTTPS 保证的只是传输过程安全，而随机数存储于本地，本地的安全属于另一安全范畴，应对的措施有安装杀毒软件、反木马、浏览器升级修复漏洞等。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;用了 HTTPS 会被抓包吗？&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;HTTPS 的数据是加密的，常规下抓包工具代理请求后抓到的包内容是加密状态，无法直接查看。&lt;/p&gt; &lt;p&gt;但是，正如前文所说，浏览器只会提示安全风险，如果用户授权仍然可以继续访问网站，完成请求。因此，只要客户端是我们自己的终端，我们授权的情况下，便可以组建中间人网络，而抓包工具便是作为中间人的代理。通常 HTTPS 抓包工具的使用方法是会生成一个证书，用户需要手动把证书安装到客户端中，然后终端发起的所有请求通过该证书完成与抓包工具的交互，然后抓包工具再转发请求到服务器，最后把服务器返回的结果在控制台输出后再返回给终端，从而完成整个请求的闭环。&lt;/p&gt; &lt;p&gt;既然 HTTPS 不能防抓包，那 HTTPS 有什么意义？ HTTPS可以防止用户在不知情的情况下通信链路被监听，对于主动授信的抓包操作是不提供防护的，因为这个场景用户是已经对风险知情。要防止被抓包，需要采用应用级的安全防护，例如采用私有的对称加密，同时做好移动端的防反编译加固，防止本地算法被破解。&lt;/p&gt; &lt;h3&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/h3&gt; &lt;p&gt;以下用简短的Q&amp;amp;A形式进行全文总结：&lt;/p&gt; &lt;p&gt;Q: HTTPS 为什么安全？ A: 因为 HTTPS 保证了传输安全，防止传输过程被监听、防止数据被窃取，可以确认网站的真实性。&lt;/p&gt; &lt;p&gt;Q: HTTPS 的传输过程是怎样的？ A: 客户端发起 HTTPS 请求，服务端返回证书，客户端对证书进行验证，验证通过后本地生成用于改造对称加密算法的随机数，通过证书中的公钥对随机数进行加密传输到服务端，服务端接收后通过私钥解密得到随机数，之后的数据交互通过对称加密算法进行加解密。&lt;/p&gt; &lt;p&gt;Q: 为什么需要证书？ A: 防止”中间人“攻击，同时可以为网站提供身份证明。&lt;/p&gt; &lt;p&gt;Q: 使用 HTTPS 会被抓包吗？ A: 会被抓包，HTTPS 只防止用户在不知情的情况下通信被监听，如果用户主动授信，是可以构建“中间人”网络，代理软件可以对传输内容进行解密。&lt;/p&gt; &lt;p&gt;顺手 po 一张学习的过程图，高清大图点这里☞  HTTPS学习草稿图.jpg&lt;/p&gt; &lt;p&gt;&lt;img src="http://www.yanzuoguang.com/upload/2022/01/ogmns8q67eh95r99rq0r72lprq.webp" alt="4" /&gt;&lt;/p&gt;</content:encoded>
      <pubDate>Mon, 10 Jan 2022 06:37:42 GMT</pubDate>
    </item>
    <item>
      <title>VPN 设置指南</title>
      <link>/article/vpn</link>
      <content:encoded>&lt;ol&gt; &lt;li&gt;登录路由器，输入用户名和密码，密码在Git的 groups/document/机密文档/内网文件 中.&lt;/li&gt; &lt;li&gt;打开VPN应用-&amp;gt;VPN管理-&amp;gt;PPTP用户新建用户，具体配置可见其他用户。&lt;/li&gt; &lt;/ol&gt;</content:encoded>
      <pubDate>Wed, 05 Jan 2022 03:02:00 GMT</pubDate>
    </item>
    <item>
      <title>Vue.use中都发生了什么？</title>
      <link>/article/1539</link>
      <content:encoded>&lt;h1&gt;Vue.use中都发生了什么？&lt;/h1&gt; &lt;p&gt;Vue.use中都发生了什么？&lt;/p&gt; &lt;blockquote&gt; &lt;p&gt;源码地址: &lt;a href="https://github.com/vuejs/vue/blob/dev/src/core/global-api/use.js" target="_blank"&gt;https://github.com/vuejs/vue/blob/dev/src/core/global-api/use.js&lt;/a&gt;&lt;/p&gt; &lt;/blockquote&gt; &lt;h5&gt;定义&lt;/h5&gt; &lt;p&gt;vue.use()往全局注入一个插件，供全局真接使用, 不需要单独引用&lt;/p&gt; &lt;p&gt;代码理解:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;import Router from 'vue-router' // 入口文件全局注入vue-router, 从而可以在全局使用this.$route Vue.use(Router) // 如果不使用vue.use 那么需在组件中使用都得单独引入 // a.vue import Router from 'vue-router'  // b.vue import Router from 'vue-router' &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;理解了其基本使用及作用，我们来看一下vue.use中都发生了什么&lt;/p&gt; &lt;p&gt;源码很少，所以直接摘抄了&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;Vue.use = function (plugin: Function | Object) {     // flow语法, 检测参数是否是函数或对象   // 拿到已安装插件列表   const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))    // 如果已经安装，直接跳出方法   if (installedPlugins.indexOf(plugin) &amp;gt; -1) {     return this   }    // additional parameters   // 取vue.use() 传入的参数   const args = toArray(arguments, 1)    // 将vue对象填充到第一位, 最后的结构为[vue,arg1,arg2,...]   args.unshift(this)    // 判断插件是否有install方法，如果有执行install方法，如果没有直接把插件当install执行   if (typeof plugin.install === 'function') {     plugin.install.apply(plugin, args)   } else if (typeof plugin === 'function') {     plugin.apply(null, args)   }    // 注册完成填充至已安装列表，保证每个插件只安装一次   installedPlugins.push(plugin)   return this } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;结合代码理解&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;// 代码摘抄自elementui入口文件 // 实现上就是在install中执行了全局注册的操作 // index.js const install = function(Vue, opts = {}) {   locale.use(opts.locale);   locale.i18n(opts.i18n);    components.forEach(component =&amp;gt; {     Vue.component(component.name, component);   });   // ... }  export default {   version: '2.5.4',   locale: locale.use,   i18n: locale.i18n,   install,   CollapseTransition,   Loading,   ... }  //  main.js import demo from './idnex' vue.use(demo) &lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;总结&lt;/h4&gt; &lt;p&gt;vue.use()为注册全局插件所用，接收函数或者一个包含install属性的对象为参数，如果参数带有install就执行install, 如果没有就直接将参数当install执行, 第一个参数始终为vue对象, 注册过的插件不会重新注册&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 31 Dec 2021 08:33:00 GMT</pubDate>
    </item>
    <item>
      <title>让Vue项目更丝滑的几个小技巧</title>
      <link>/article/1538</link>
      <content:encoded>&lt;h1&gt;让Vue项目更丝滑的几个小技巧&lt;/h1&gt; &lt;p&gt;前几天有朋友给我发了一段代码，然后说&lt;code&gt;Vue&lt;/code&gt;有&lt;code&gt;bug&lt;/code&gt;，他明明写的没问题，为啥数据就不响应呢，一定是&lt;code&gt;Vue&lt;/code&gt;的&lt;code&gt;bug&lt;/code&gt;？&lt;/p&gt; &lt;p&gt;我感觉他比尤雨溪要牛逼，高攀不起，就没有理他了。但是确实有时候我们在开发时候会遇到数据不响应的情况，那怎么办呢？&lt;/p&gt; &lt;h2&gt;数据不响应，可能是用法有问题&lt;/h2&gt; &lt;p&gt;比如下面这段代码:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;div&amp;gt;   &amp;lt;div&amp;gt;     &amp;lt;span&amp;gt;用户名: {{ userInfo.name }}&amp;lt;/span&amp;gt;     &amp;lt;span&amp;gt;用户性别: {{ userInfo.sex }}&amp;lt;/span&amp;gt;     &amp;lt;span v-if=&amp;quot;userInfo.officialAccount&amp;quot;&amp;gt;       公众号: {{ userInfo.officialAccount }}     &amp;lt;/span&amp;gt;   &amp;lt;/div&amp;gt;   &amp;lt;button @click=&amp;quot;handleAddOfficialAccount&amp;quot;&amp;gt;添加公众号&amp;lt;/button&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   export default {     data() {       return {         userInfo: {           name: '子君',           sex: '男'         }       }     },     methods: {       // 在这里添加用户的公众号       handleAddOfficialAccount() {         this.userInfo.officialAccount = '前端有的玩'       }     }   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在上面的代码中，我们希望给用户信息里面添加公众号属性，但是通过&lt;code&gt;this.userInfo.officialAccount = '前端有的玩'&lt;/code&gt; 添加之后，并没有生效，这是为什么呢？&lt;/p&gt; &lt;p&gt;这是因为在&lt;code&gt;Vue&lt;/code&gt;内部，数据响应是通过使用&lt;code&gt;Object.definePrototype&lt;/code&gt;监听对象的每一个键的&lt;code&gt;getter&lt;/code&gt;,&lt;code&gt;setter&lt;/code&gt;方法来实现的，但通过这种方法只能监听到已有属性，新增的属性是无法监听到的，但我就是想监听，小编你说咋办吧。下面小编提供了四种方式，如果有更多方式，欢迎下方评论区告诉我。&lt;/p&gt; &lt;h3&gt;1. 将本来要新增的属性提前在&lt;code&gt;data&lt;/code&gt;中定义好&lt;/h3&gt; &lt;p&gt;比如上面的公众号，我可以提前在&lt;code&gt;userInfo&lt;/code&gt;里面定义好，这样就不是新增属性了，就像下面这样&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;data() {   return {     userInfo: {       name: '子君',       sex: '男',       // 我先提前定义好       officialAccount: ''     }   } } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;2. 直接替换掉&lt;code&gt;userInfo&lt;/code&gt;&lt;/h3&gt; &lt;p&gt;虽然无法给&lt;code&gt;userInfo&lt;/code&gt;里面添加新的属性，但是因为&lt;code&gt;userInfo&lt;/code&gt;已经定义好了，所以我直接修改&lt;code&gt;userInfo&lt;/code&gt;的值不就可以了么，所以也可以像下面这样写&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;this.userInfo = {   // 将原来的userInfo 通过扩展运算法复制到新的对象里面   ...this.userInfo,   // 添加新属性   officialAccount: '前端有的玩' } &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;3. 使用&lt;code&gt;Vue.set&lt;/code&gt;&lt;/h3&gt; &lt;p&gt;其实上面两种方法都有点取巧的嫌疑，其实对于新增属性，&lt;code&gt;Vue&lt;/code&gt;官方专门提供了一个新的方法&lt;code&gt;Vue.set&lt;/code&gt;用来解决新增属性无法触发数据响应。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;Vue.set 方法定义&lt;/strong&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;/** * target 要修改的对象 * prpertyName 要添加的属性名称 * value 要添加的属性值 */ Vue.set( target, propertyName, value ) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上面的代码使用&lt;code&gt;Vue.set&lt;/code&gt;可以修改为&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;import Vue from 'vue'  // 在这里添加用户的公众号 handleAddOfficialAccount() {   Vue.set(this.userInfo,'officialAccount', '前端有的玩') } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但是每次要用到&lt;code&gt;set&lt;/code&gt;方法的时候，还要把&lt;code&gt;Vue&lt;/code&gt;引入进来，好麻烦，所以为了简便起见，&lt;code&gt;Vue&lt;/code&gt;又将&lt;code&gt;set&lt;/code&gt;方法挂载到了&lt;code&gt;Vue&lt;/code&gt;的原型链上了，即&lt;code&gt;Vue.prototype.$set = Vue.set&lt;/code&gt;,所以在&lt;code&gt;Vue&lt;/code&gt;组件内部可以直接使用&lt;code&gt;this.$set&lt;/code&gt;代替&lt;code&gt;Vue.set&lt;/code&gt;&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;this.$set(this.userInfo,'officialAccount', '前端有的玩') &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;小编发现有许多同学不知道什么时候应该用&lt;code&gt;Vue.set&lt;/code&gt;，其实只有当你要赋值的属性还没有定义的时候需要使用&lt;code&gt;Vue,set&lt;/code&gt;，其他时候一般不会需要使用。&lt;/p&gt; &lt;h3&gt;4. 使用&lt;code&gt;$forceUpdate&lt;/code&gt;&lt;/h3&gt; &lt;p&gt;我觉得&lt;code&gt;$forceUpdate&lt;/code&gt;的存在，让许多前端开发者不会再去注意数据双向绑定的原理，因为不论什么时候，反正我修改了&lt;code&gt;data&lt;/code&gt;之后，调用一下&lt;code&gt;$forceUpdate&lt;/code&gt;就会让&lt;code&gt;Vue&lt;/code&gt;组件重新渲染，&lt;code&gt;bug&lt;/code&gt;是不会存在的。但是实际上这个方法并不建议使用，因为它会引起许多不必要的性能消耗。&lt;/p&gt; &lt;h3&gt;针对数组的特定方式&lt;/h3&gt; &lt;p&gt;其实不仅仅是对象，数组也存在数据修改之后不响应的情况，比如下面这段代码&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;div&amp;gt;   &amp;lt;ul&amp;gt;     &amp;lt;li v-for=&amp;quot;item in list&amp;quot; :key=&amp;quot;item&amp;quot;&amp;gt;       {{ item }}   &amp;lt;/li&amp;gt;   &amp;lt;/ul&amp;gt;   &amp;lt;button @click=&amp;quot;handleChangeName&amp;quot;&amp;gt;修改名称&amp;lt;/button&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   export default {     data() {       return {         list: ['张三', '李四']       }     },     methods: {       // 修改用户名称       handleChangeName() {         this.list[0] = '王五'       }     }   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;上面的代码希望将张三的名字修改为王五，实际上这个修改并不能生效，这是因为&lt;code&gt;Vue&lt;/code&gt;不能检测到以下变动的数组:&lt;/p&gt; &lt;ol&gt; &lt;li&gt;当你利用索引直接设置一个项时，例如: &lt;code&gt;this.list[index] = newValue&lt;/code&gt;&lt;/li&gt; &lt;li&gt;修改数组的&lt;code&gt;length&lt;/code&gt;属性,例如: &lt;code&gt;this.list.length = 0&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;p&gt;所以在上例中通过&lt;code&gt;this.list[0] = '王五'&lt;/code&gt; 是无法触发数据响应的，那应该怎么办呢？像上面提到的&lt;code&gt;Vue.set&lt;/code&gt;和&lt;code&gt;$forceUpdate&lt;/code&gt;都可以解决这个问题，比如&lt;code&gt;Vue.set&lt;/code&gt;可以这样写&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;Vue.set(this.list,0,'王五') &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;除了那些方法之外，&lt;code&gt;Vue&lt;/code&gt;还针对数组提供了变异方法&lt;/p&gt; &lt;p&gt;在操作数组的时候，我们一般会用到数据提供的许多方法，比如&lt;code&gt;push&lt;/code&gt;,&lt;code&gt;pop&lt;/code&gt;,&lt;code&gt;splice&lt;/code&gt;等等，在&lt;code&gt;Vue&lt;/code&gt;中调用数组上面提供的这些方法修改数组的值是可以触发数据响应的，比如上面的代码改为以下代码即可触发数据响应&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;this.list.splice(0,1,'王五') &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;实际上，如果&lt;code&gt;Vue&lt;/code&gt;仅仅依赖&lt;code&gt;getter&lt;/code&gt;与&lt;code&gt;setter&lt;/code&gt;，是无法做到在数组调用&lt;code&gt;push&lt;/code&gt;,&lt;code&gt;pop&lt;/code&gt;等方法时候触发数据响应的，因此&lt;code&gt;Vue&lt;/code&gt;实际上是通过劫持这些方法，对这些方法进行包装变异来实现的。&lt;/p&gt; &lt;p&gt;&lt;code&gt;Vue&lt;/code&gt;对数组的以下方法进行的包装变异:&lt;/p&gt; &lt;ul&gt; &lt;li&gt;push&lt;/li&gt; &lt;li&gt;pop&lt;/li&gt; &lt;li&gt;shift&lt;/li&gt; &lt;li&gt;unshift&lt;/li&gt; &lt;li&gt;splice&lt;/li&gt; &lt;li&gt;sort&lt;/li&gt; &lt;li&gt;reverse&lt;/li&gt; &lt;/ul&gt; &lt;p&gt;所以在操作数组的时候，调用上面这些方法是可以保证数据可以正常响应，下面是&lt;code&gt;Vue&lt;/code&gt;源码中包装数组方法的代码:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;var original = arrayProto[method]; def(arrayMethods, method, function mutator () {   // 将 arguments 转换为数组   var args = [], len = arguments.length;   while ( len-- ) args[ len ] = arguments[ len ];   var result = original.apply(this, args);   // 这儿的用法同dependArray(value)，就是为了取得dep   var ob = this.__ob__;   var inserted;   switch (method) {     case 'push':     case 'unshift':       inserted = args;       break     case 'splice':       inserted = args.slice(2);       break   }   // 如果有新的数据插入，则插入的数据也要进行一个响应式   if (inserted) { ob.observeArray(inserted); }   // 通知依赖进行更新   ob.dep.notify();   return result }); &lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;文本格式化，&lt;code&gt;filter&lt;/code&gt;更简单&lt;/h2&gt; &lt;h3&gt;使用&lt;code&gt;filter&lt;/code&gt; 简化逻辑&lt;/h3&gt; &lt;p&gt;我想把时间戳显示成&lt;code&gt;yyyy-MM-DD HH🇲🇲ss&lt;/code&gt;的格式怎么办？是需要在代码中先将日期格式化之后，再渲染到模板吗？就像下面这样&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;div&amp;gt;   {{ dateStr }}   &amp;lt;ul&amp;gt;     &amp;lt;li v-for=&amp;quot;(item, index) in getList&amp;quot; :key=&amp;quot;index&amp;quot;&amp;gt;       {{ item.date }}   &amp;lt;/li&amp;gt;   &amp;lt;/ul&amp;gt;   &amp;lt;/div&amp;gt; &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   import { format } from '@/utils/date'   export default {     data() {       return {         date: Date.now(),         list: [           {             date: Date.now()           }         ]       }     },     computed: {       dateStr() {         return format(this.date, 'yyyy-MM-DD HH🇲🇲ss')       },       getList() {         return this.list.map(item =&amp;gt; {           return {             ...item,             date: format(item.date, 'yyyy-MM-DD HH🇲🇲ss')           }         })       }     }   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;像上面的写法，针对每一个日期字段都需要调用&lt;code&gt;format&lt;/code&gt;，然后通过计算属性进行转换？这时候可以考虑使用&lt;code&gt;Vue&lt;/code&gt;提供的&lt;code&gt;filter&lt;/code&gt;去简化&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;div&amp;gt;   &amp;lt;!--使用过滤器--&amp;gt;   {{ date | formatDate }}   &amp;lt;ul&amp;gt;     &amp;lt;li v-for=&amp;quot;(item, index) in list&amp;quot; :key=&amp;quot;index&amp;quot;&amp;gt;       &amp;lt;!--在v-for中使用过滤器--&amp;gt;       {{ item.date | formatDate }}   &amp;lt;/li&amp;gt;   &amp;lt;/ul&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   import { format } from '@/utils/date'   export default {     filters: {       formatDate(value) {         return format(value, 'yyyy-MM-DD HH🇲🇲ss')       }     },     data() {       return {         date: Date.now(),         list: [           {             date: Date.now()           }         ]       }     }   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;通过上面的修改是不是就简单多了&lt;/p&gt; &lt;h3&gt;注册全局&lt;code&gt;filter&lt;/code&gt;&lt;/h3&gt; &lt;p&gt;有些过滤器使用的很频繁，比如上面提到的日期过滤器，在很多地方都要使用，这时候如果在每一个要用到的组件里面都去定义一遍，就显得有些多余了，这时候就可以考虑&lt;code&gt;Vue.filter&lt;/code&gt;注册全局过滤器&lt;/p&gt; &lt;p&gt;对于全局过滤器，一般建议在项目里面添加&lt;code&gt;filters&lt;/code&gt;目录，然后在filters目录里面添加&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;// filters\index.js import Vue from 'vue' import { format } from '@/utils/date'  Vue.filter('formatDate', value =&amp;gt; {   return format(value, 'yyyy-MM-DD HH🇲🇲ss') }) &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;然后将&lt;code&gt;filters&lt;/code&gt;里面的文件引入到&lt;code&gt;main.js&lt;/code&gt;里面，这时候就可以在组件里面直接用了，比如将前面的代码可以修改为&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;div&amp;gt;   &amp;lt;!--使用过滤器--&amp;gt;   {{ date | formatDate }}   &amp;lt;ul&amp;gt;     &amp;lt;li v-for=&amp;quot;(item, index) in list&amp;quot; :key=&amp;quot;index&amp;quot;&amp;gt;       &amp;lt;!--在v-for中使用过滤器--&amp;gt;       {{ item.date | formatDate }}   &amp;lt;/li&amp;gt;   &amp;lt;/ul&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   export default {     data() {       return {         date: Date.now(),         list: [           {             date: Date.now()           }         ]       }     }   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;是不是更简单了&lt;/p&gt; &lt;h2&gt;开发了插件库，来安装一下&lt;/h2&gt; &lt;p&gt;在使用一些&lt;code&gt;UI&lt;/code&gt;框架的时候，经常需要使用&lt;code&gt;Vue.use&lt;/code&gt;来安装， 比如使用&lt;code&gt;element-ui&lt;/code&gt;时候，经常会这样写:&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;import Vue from 'vue'; import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI,{size: 'small'}); &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;使用了&lt;code&gt;Vue.use&lt;/code&gt;之后，&lt;code&gt;element-ui&lt;/code&gt;就可以直接在组件里面使用了，好神奇哦（呸，娘炮）。接下来我们实现一个简化版的&lt;code&gt;element&lt;/code&gt;来看如何去安装。&lt;/p&gt; &lt;h3&gt;了解&lt;code&gt;Vue.use&lt;/code&gt;的用法&lt;/h3&gt; &lt;p&gt;&lt;code&gt;Vue.use&lt;/code&gt;是一个全局的方法，它需要在你调用 &lt;code&gt;new Vue()&lt;/code&gt; 启动应用之前完成，&lt;code&gt;Vue.use&lt;/code&gt;的参数如下&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;/** * plugin: 要安装的插件 如 ElementUI * options: 插件的配置信息 如 {size: 'small'} */ Vue.use(plugin, options) &lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;模拟&lt;code&gt;element-ui&lt;/code&gt;的安装逻辑&lt;/h3&gt; &lt;p&gt;想一下，使用&lt;code&gt;Vue.use(ElementUI,{size: 'small'})&lt;/code&gt; 之后我们可以用到哪些&lt;code&gt;element-ui&lt;/code&gt;提供的东西&lt;/p&gt; &lt;ol&gt; &lt;li&gt;可以直接在组件里面用&lt;code&gt;element-ui&lt;/code&gt;的组件，不需要再&lt;code&gt;import&lt;/code&gt;&lt;/li&gt; &lt;li&gt;可以直接使用&lt;code&gt;v-loading&lt;/code&gt;指令&lt;/li&gt; &lt;li&gt;通过&lt;code&gt;this.$loading&lt;/code&gt;在组件里面显示&lt;code&gt;loading&lt;/code&gt;&lt;/li&gt; &lt;li&gt;其他..&lt;/li&gt; &lt;/ol&gt; &lt;pre&gt;&lt;code class="language-js"&gt;// 这个是一个按钮组件 import Button from '@/components/button'  // loading 指令 import loadingDirective from '@/components/loading/directive'  // loading 方法 import loadingMethod from '@/components/loading'  export default {   /**    * Vue.use 需要插件提供一个install方法    * @param {*} Vue Vue    * @param {*} options 插件配置信息    */   install(Vue, options) {     console.log(options)     // 将组件通过Vue.components 进行注册     Vue.components(Button.name, Button)      // 注册全局指令     Vue.directive('loading', loadingDirective)      // 将loadingMethod 挂载到 Vue原型链上面，方便调用     Vue.prototype.$loading = loadingMethod   } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;通过上面的代码，已经实现了一个丐版的&lt;code&gt;element-ui&lt;/code&gt;插件，这时候就可以在&lt;code&gt;main.js&lt;/code&gt;里面通过&lt;code&gt;Vue.use&lt;/code&gt;进行插件安装了。大家可能会有疑问，为什么我要用这种写法，不用这种写法我照样可以实现功能啊。小编认为这种写法有两个优势&lt;/p&gt; &lt;ol&gt; &lt;li&gt;标准化，通过提供一种统一的开发模式，无论对插件开发者还是使用者来说，都有一个规范去遵循。&lt;/li&gt; &lt;li&gt;插件缓存，&lt;code&gt;Vue.use&lt;/code&gt; 在安装插件的时候，会对插件进行缓存，即一个插件如果安装多次，实际上只会在第一次安装时生效。&lt;/li&gt; &lt;/ol&gt; &lt;h3&gt;插件的应用场景&lt;/h3&gt; &lt;ol&gt; &lt;li&gt;添加全局方法或者 property。&lt;/li&gt; &lt;li&gt;添加全局资源：指令/过滤器/过渡等。&lt;/li&gt; &lt;li&gt;通过全局混入来添加一些组件选项。&lt;/li&gt; &lt;li&gt;添加 Vue 实例方法，通过把它们添加到 &lt;code&gt;Vue.prototype&lt;/code&gt; 上实现。&lt;/li&gt; &lt;li&gt;一个库，提供自己的 API，同时提供上面提到的一个或多个功能。如&lt;code&gt;element-ui&lt;/code&gt;&lt;/li&gt; &lt;/ol&gt; &lt;h2&gt;提高Vue渲染性能，了解一下Object.freeze&lt;/h2&gt; &lt;p&gt;当一个 Vue 实例被创建时，它将 &lt;code&gt;data&lt;/code&gt; 对象中的所有的 property 加入到 Vue 的&lt;strong&gt;响应式系统&lt;/strong&gt;中。当这些 property 的值发生改变时，视图将会产生“响应”，即匹配更新为新的值。但是这个过程实际上是比较消耗性能的，所以对于一些有大量数据但只是展示的界面来说，并不需要将&lt;code&gt;property&lt;/code&gt;加入到响应式系统中，这样可以提高渲染性能，怎么做呢，你需要了解一下&lt;code&gt;Object.freeze&lt;/code&gt;。&lt;/p&gt; &lt;p&gt;在&lt;code&gt;Vue&lt;/code&gt;官网中，有这样一段话：这里唯一的例外是使用 &lt;code&gt;Object.freeze()&lt;/code&gt;，这会阻止修改现有的 property，也意味着响应系统无法再_追踪_变化。这段话的意思是，如果我们的数据使用了&lt;code&gt;Object.freeze&lt;/code&gt;，就可以让数据脱离响应式系统，那么该如何做呢?&lt;/p&gt; &lt;p&gt;比如下面这个表格，因为只是渲染数据，这时候我们就可以通过&lt;code&gt;Object.freeze&lt;/code&gt;来优化性能&lt;/p&gt; &lt;pre&gt;&lt;code class="language-vue"&gt;&amp;lt;template&amp;gt; &amp;lt;el-table :data=&amp;quot;tableData&amp;quot; style=&amp;quot;width: 100%&amp;quot;&amp;gt;   &amp;lt;el-table-column prop=&amp;quot;date&amp;quot; label=&amp;quot;日期&amp;quot; width=&amp;quot;180&amp;quot; /&amp;gt;   &amp;lt;el-table-column prop=&amp;quot;name&amp;quot; label=&amp;quot;姓名&amp;quot; width=&amp;quot;180&amp;quot; /&amp;gt;   &amp;lt;el-table-column prop=&amp;quot;address&amp;quot; label=&amp;quot;地址&amp;quot; /&amp;gt;   &amp;lt;/el-table&amp;gt; &amp;lt;/template&amp;gt; &amp;lt;script&amp;gt;   export default {     data() {       const data = Array(1000)       .fill(1)       .map((item, index) =&amp;gt; {         return {           date: '2020-07-11',           name: `子君${index}`,           address: '大西安'         }       });       return {         // 在这里我们用了Object.freeze         tableData: Object.freeze(data)       }     }   } &amp;lt;/script&amp;gt; &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;有的同学可能会有疑问，如果我这个表格的数据是滚动加载的，你这样写我不就没法再给&lt;code&gt;tableData&lt;/code&gt;添加数据了吗？是，确实没办法去添加数据了，但还是有办法解决的，比如像下面这样&lt;/p&gt; &lt;pre&gt;&lt;code class="language-js"&gt;export default {   data() {     return {       tableData: []     }   },   created() {     setInterval(() =&amp;gt; {       const data = Array(1000)       .fill(1)       .map((item, index) =&amp;gt; {         // 虽然不能冻结整个数组，但是可以冻结每一项数据         return Object.freeze({           date: '2020-07-11',           name: `子君${index}`,           address: '大西安'         })       })       this.tableData = this.tableData.concat(data)     }, 2000)   } } &lt;/code&gt;&lt;/pre&gt; &lt;p&gt;合理的使用&lt;code&gt;Object.freeze&lt;/code&gt;，是可以节省不少渲染性能，特别对于IE浏览器，效果还是很明显的，赶快去试试吧。&lt;/p&gt; &lt;p&gt;最后如果你现在需要开发移动端项目，可以了解一下小编整理的一个开箱即用框架 vue-vant-base,也许可以帮到你哦&lt;/p&gt;</content:encoded>
      <pubDate>Fri, 31 Dec 2021 08:30:04 GMT</pubDate>
    </item>
  </channel>
</rss>

