<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ SHERlocked93 - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ SHERlocked93 - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 24 May 2026 19:37:54 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/sherlocked93/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Git 常用命令总结 ]]>
                </title>
                <description>
                    <![CDATA[ 最近公司的代码管理工具要从 SVN 转到 Git 上，因此虽然之前用过 Git，但是都是一些简单的推送提交，因此还是有必要进行一些系统的学习，这里做一下笔记，以备后询，且不定期更新。 关于 SVN 和 Git 的比较已经有很多文章说过了，就不再赘述。本文的重点是如何使用常用的 Git 命令进行操作，冷门的就不说了，且比较零散，系统的学习推介廖雪峰的 Git 教程 [https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000] 。 > 声明：下面用户名都为SHERlocked93，请自行修改成自己的用户名 1. 概览  * 工作区 Workspace  * 暂存区 Stage / Index  * 本地仓库 Repository  * 远程仓库 Remote 2. 修改 2.1 暂存修改 操作一览 如果在工作的时候出现了临时需要解决的问题，而你又不希望提交，那么有个stash功能 git stash 在暂存后工作区会回退到最近的一个 commit 的状态，以便开建新分支 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/collection-of-useful-git-commands/</link>
                <guid isPermaLink="false">5d9dddcefbfdee429dc5ff91</guid>
                
                    <category>
                        <![CDATA[ Git ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 命令行 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 开发技能 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Fri, 06 Nov 2020 04:43:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1556075798-4825dfaaf498.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近公司的代码管理工具要从 SVN 转到 Git 上，因此虽然之前用过 Git，但是都是一些简单的推送提交，因此还是有必要进行一些系统的学习，这里做一下笔记，以备后询，且不定期更新。</p><p>关于 SVN 和 Git 的比较已经有很多文章说过了，就不再赘述。本文的重点是如何使用<strong><strong>常用</strong></strong>的 Git 命令进行操作，冷门的就不说了，且比较零散，系统的学习推介廖雪峰的 <a href="https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000" rel="nofollow noreferrer">Git 教程</a>。</p><blockquote>声明：下面用户名都为<code>SHERlocked93</code>，请自行修改成自己的用户名</blockquote><h2 id="1-">1. 概览</h2><figure class="kg-card kg-image-card"><img src="https://i.loli.net/2018/09/29/5baf2af732fe1.png" class="kg-image" alt="5baf2af732fe1" width="600" height="400" loading="lazy"></figure><ul><li>工作区 Workspace</li><li>暂存区 Stage / Index</li><li>本地仓库 Repository</li><li>远程仓库 Remote</li></ul><h2 id="2-">2. 修改</h2><h3 id="2-1-">2.1 暂存修改</h3><p>操作一览</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-2.png" class="kg-image" alt="image-2" width="600" height="400" loading="lazy"></figure><p>如果在工作的时候出现了临时需要解决的问题，而你又不希望提交，那么有个<code>stash</code>功能</p><pre><code>git stash</code></pre><p>在暂存后工作区会回退到最近的一个 commit 的状态，以便开建新分支；比如我们修复 bug 时，我们会通过创建新的 bug 分支进行修复，然后合并，最后删除；</p><p>当手头工作没有完成时，先把工作现场<code>git stash</code>一下，然后去修复 bug，修复后，再<code>git stash pop</code>，回到工作现场。</p><h3 id="2-2-">2.2 撤销修改</h3><h4 id="-">还未提交到暂存区</h4><p>当修改还没有被<code>add</code>的时候，可以使用</p><pre><code>git checkout -- filename.txt</code></pre><p>来丢弃工作区某文件的修改，当然也可以把后面的文件改成<code>*</code>来撤销所有文件的修改。这是用仓库的文件覆盖工作区的文件。</p><p>注意这里用的是<code>--</code>，如果没有这个<code>--</code>的话就变成切换分支了。</p><h4 id="--1">还未提交到仓库</h4><p>如果你的修改已经被<code>add</code>到了暂存区，但是还没有被<code>commit</code>，那么可以使用</p><pre><code>git reset HEAD filename.txt
git checkout -- filename.txt</code></pre><p>首先用<code>reset</code>来把修改撤回到工作区，再使用上面的<code>checkout</code>命令撤回工作区的修改。这里的<code>reset</code>相当于<code>add</code>的反操作。</p><h4 id="--2">已经提交到仓库</h4><p>则可以版本回退</p><pre><code>git reset --hard 15zdx2s</code></pre><p>这里的<code>--hard</code>表示强制回退，丢弃本地的修改。这个回退比较野蛮，该版本号之后的提交都将不可见。</p><h4 id="--3">撤销之前某一个提交</h4><p><code>git revert</code>撤销一个提交的同时会创建一个新的提交，这是一个安全的方法，因为它不会重写提交历史。但实现上和reset是完全不同的。它撤销这个提交引入的更改，然后在最后加上一个撤销了更改的新提交，而不是从项目历史中移除这个提交。</p><pre><code>git revert 46af7z6</code></pre><p>相较于<code>reset</code> ，<code>revert</code>不会改变项目历史，对那些已经发布到共享仓库的提交来说这是一个安全的操作。其次<code>git revert</code> 可以将提交历史中的任何一个提交撤销、而<code>reset</code>会把历史上某个提交及之后所有的提交都移除掉，这太野蛮了。</p><p>相比 <code>reset</code>，它不会改变现在的提交历史。因此，<code>revert</code> 可以用在公共分支上，<code>reset</code> 应该用在私有分支上。</p><h4 id="-commit">合并 commit</h4><p>如果已经<code>commit</code>了怎么办，如果要撤回目前的<code>commit</code>，可以把它合并到上一个<code>commit</code>中</p><pre><code>git rebase -i HEAD~~</code></pre><p>在出现的两个提交信息的<code>pick</code>改为<code>fixup</code></p><h2 id="3-">3. 分支操作</h2><h3 id="3-1-">3.1 创建/查看/合并分支</h3><p>操作一览</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-3.png" class="kg-image" alt="image-3" width="600" height="400" loading="lazy"></figure><p>创建分支</p><pre><code># 创建新分支
git branch bug-fix
# 查看分支，-a查看本地和远程的分支，-r查看远程分支，-l或没有只查看本地
git branch -a
# 切换到刚刚创建的分支
git checkout bug-fix</code></pre><p>上面两个步骤可以合并为</p><pre><code># 创建并切换到分支
git checkout -b bug-fix</code></pre><p>如果修改一下本地文件之后在这个分支继续培育一个版本之后，怎么去合并到主分支呢？</p><pre><code>git add *
git commit -m "some change"
# 切换到主分支
git checkout master
# 合并分支
git merge bug-fix
# 删除分支 (可选)
git branch -d bug-fix</code></pre><p>如果 master 分支和新的分支都各自培育了版本，那么自动合并通常会失败，发生冲突<code>conflict</code>，此时需要打开文件解决冲突之后<code>commit</code>一个版本以完成合并</p><pre><code>git add *
git commit -m "branch merge"</code></pre><p>这里提一下，<code>merge</code>的时候有几个主要模式，<code>--no-ff</code>、<code>fast-forward</code>，其中<code>fast-forward</code>是默认的</p><ol><li><code>fast-forward</code>：在 master 开始的新分支前进了几个版本之后如果需要 merge 回来，此时 master 并没有前进，那么这个模式就是把 HEAD 与 master 指针指向新分支上，完成合并。这种情况如果删除分支，则会丢失分支信息，因为在这个过程中并没有创建 commit。</li><li><code>--no-ff</code>：关闭默认的<code>fast-forward</code>模式，也就是在 merge 的时候生成一个新的 commit，这样在分支历史上就可以看出分支信息。</li></ol><h3 id="3-2-">3.2 远程仓库操作</h3><p>操作一览</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-4.png" class="kg-image" alt="image-4" width="600" height="400" loading="lazy"></figure><p>关于各个分支，哪些需要推送呢？</p><ol><li><code>master</code>分支是主分支，因此要时刻与远程同步；</li><li><code>dev</code>分支是开发分支，团队所有成员都需要在上面工作，所以也需要与远程同步；</li><li><code>bug</code>分支只用于在本地修复 bug，就没必要推到远程了，除非老板要看看你每周到底修复了几个 bug；</li><li><code>feature</code>分支是否推到远程，取决于你是否和你的小伙伴合作在上面开发。</li></ol><h4 id="-clone">直接<code>clone</code></h4><p>在 GitHub 上创建一个新的项目之后，比如叫<code>learn-git</code>，那么可以直接<code>clone</code>下来，注意创建的时候不要选择 <code>Initialize this repository with a README</code>，我们要的是一个空的仓库。</p><pre><code>git&nbsp;clone&nbsp;https://github.com/SHERlocked93/learn-git.git</code></pre><p>这样在本地就直接创建了一个空的文件夹<code>learn-git</code>，当然里面有<code>.git</code>文件夹。<br>也可以使用 SSH 地址来 clone，速度会快一些，也不用每次推送都输入口令，推介使用这种：</p><pre><code>git&nbsp;clone&nbsp;git@github.com:SHERlocked93/learn-git.git</code></pre><p>添加一个文件<code>filename.txt</code>之后</p><pre><code>git add filename.txt
git commit -m "add filename.txt"
git push -u origin master</code></pre><p>这样就把本地新建的文件 push 到了远程仓库</p><h4 id="--4">本地与远程建立关联</h4><p>如果已经有了本地工程文件夹，如何分享到 GitHub 远程仓库呢？当然此时我们已经在 GitHub 上创建了一个新的空白项目，还是叫<code>learn-git</code>，在本地文件夹中</p><pre><code>git init
# 关联远程库
git remote add origin git@github.com:SHERlocked93/learn-git.git
git push -u origin master</code></pre><p>就可以了，如果你的远程仓库已经有了提交，那么在<code>push</code>之前需要</p><pre><code># 允许不想干库合并
git pull origin master --allow-unrelated-histories
git push -u origin master</code></pre><p>先拉取远程分支，注意这里<code>--allow-unrelated-histories</code>允许两个不想干的分支强行合并，再<code>push</code>；这样在 GitHub 的网站上还能看到 commit 记录。</p><p>也可以强硬一点直接强行推送</p><pre><code># -f 强行推送
git push -u origin master -f</code></pre><p>这样本地仓库就直接把远程仓库覆盖了，且 GitHub 上也看不到历史<code>commit</code>了，如果不想被同事枪击的话，还是推介上一种做法。</p><h4 id="--5">同步远程仓库</h4><p>那么已经 clone 的仓库如果希望同步原仓库新的提交怎么办？</p><pre><code># 从远程分支拉取代码到本地
git pull upstream master
# push到自己的库里
git push  origin master</code></pre><h3 id="3-3-">3.3 多人协作</h3><p>多人协作的工作模式通常是这样：</p><ol><li>首先，可以试图用<code>git push origin &lt;branch-name&gt;</code>推送自己的修改</li><li>如果推送失败，则因为远程分支比你的本地更新，需要先用git pull试图合并</li><li>如果合并有冲突，则解决冲突，并在本地提交</li><li>没有冲突或者解决掉冲突后，再用<code>git push origin &lt;branch-name&gt;</code>推送就能成功</li></ol><p>从远程抓取分支，使用<code>git pull</code>，如果有冲突，要先处理冲突，<code>add-&gt;commit-&gt;push</code>。如果<code>git pull</code>提示 no tracking information，则说明本地分支和远程分支的链接关系没有创建，用命令<code>git branch --set-upstream-to &lt;branch-name&gt; origin/&lt;branch-name&gt;</code>。</p><h2 id="4-">4. 标签操作</h2><p>操作一览</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-5.png" class="kg-image" alt="image-5" width="600" height="400" loading="lazy"></figure><p>如果要删除远程分支，需要</p><pre><code># 首先删除本地tag，假如tag是v0.9
git tag -d v0.9
# 再从远程删除
git push origin :refs/tags/v0.9</code></pre><h2 id="5-">5. 提交格式</h2><p>type：</p><ul><li>feat: 新特性，添加功能</li><li>fix: 修改 bug</li><li>refactor: 代码重构</li><li>docs: 文档修改</li><li>style: 代码格式修改, 注意不是 css 修改</li><li>test: 测试用例修改</li><li>chore: 其他修改, 比如构建流程, 依赖管理</li></ul><p>附件：Git常用命令速查表</p><figure class="kg-card kg-image-card"><img src="https://i.loli.net/2018/09/30/5bb07b7d2753b.png" class="kg-image" alt="5bb07b7d2753b" width="600" height="400" loading="lazy"></figure> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ CentOS 入门必备基础知识 ]]>
                </title>
                <description>
                    <![CDATA[ 这里记录一下我的学习过程，相当于自己记个笔记，同时分享出来，如果有同学刚好有需要而这个文章帮助到了你的话，在下也会十分开心。 文章最后推介了几个免费视频，B 站和慕课上的免费学习视频挺多，而且有些质量还是不错的。 1. CentOS 中的文件管理 1.1 CentOS 中根目录下的各子目录 当 cd / 进入到根目录，ls 可以看到一大堆子目录，如下图： 这些文件是有颜色的：  * 蓝色 表示文件夹；  * 灰色 表示普通文件；  * 绿色 表示可执行文件；  * 红色 表示压缩文件；  * 天蓝色 表示链接文件（快捷方式）； 常用目录的作用如下：  * bin： 存放普通用户可执行的指令，普通用户也可以执行；  * dev ： 设备目录，所有的硬件设备及周边均放置在这个设备目录中；  * boot ： 开机引导目录，包括 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/centos-basics/</link>
                <guid isPermaLink="false">5f9de9dc5f583f0565090c94</guid>
                
                    <category>
                        <![CDATA[ Linux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Sat, 31 Oct 2020 08:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1603979135031-0e51f9e9e9f2.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>这里记录一下我的学习过程，相当于自己记个笔记，同时分享出来，如果有同学刚好有需要而这个文章帮助到了你的话，在下也会十分开心。</p><p>文章最后推介了几个免费视频，B 站和慕课上的免费学习视频挺多，而且有些质量还是不错的。</p><h2 id="1-centos-">1. CentOS 中的文件管理</h2><h3 id="1-1-centos-">1.1 CentOS 中根目录下的各子目录</h3><p>当 <code>cd /</code> 进入到根目录，<code>ls</code> 可以看到一大堆子目录，如下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-10.png" class="kg-image" alt="image-10" width="600" height="400" loading="lazy"></figure><p>这些文件是有颜色的：</p><ul><li>蓝色 表示文件夹；</li><li>灰色 表示普通文件；</li><li>绿色 表示可执行文件；</li><li>红色 表示压缩文件；</li><li>天蓝色 表示链接文件（快捷方式）；</li></ul><p>常用目录的作用如下：</p><ul><li><strong>bin：</strong> 存放普通用户可执行的指令，普通用户也可以执行；</li><li><strong>dev ：</strong> 设备目录，所有的硬件设备及周边均放置在这个设备目录中；</li><li><strong>boot ：</strong> 开机引导目录，包括 Linux 内核文件与开机所需要的文件；</li><li><strong>home：</strong> 这里主要存放你的个人数据，具体每个用户的设置文件，用户的桌面文件夹，还有用户的数据都放在这里。每个用户都有自己的用户目录，位置为：<code>/home/用户名</code>。当然，root 用户除外；</li><li><strong>usr：</strong> 应用程序放置目录；</li><li><strong>lib：</strong> 开机时常用的动态链接库，bin 及 sbin 指令也会调用对应的 lib 库；</li><li><strong>tmp：</strong> 临时文件存放目录 ；</li><li><strong>etc：</strong> 各种配置文件目录，大部分配置属性均存放在这里；</li></ul><p>其他更详细的目录作用参考 <a href="https://zhuanlan.zhihu.com/p/46279950" rel="nofollow noopener noreferrer">&lt;CentOS根目录下各目录介绍 - 知乎&gt;</a></p><h3 id="1-2-">1.2 目录相关命令</h3><p>一些常用的命令见下：</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>作用</th>
<th>命令</th>
</tr>
</thead>
<tbody>
<tr>
<td>切换目录</td>
<td><code>cd</code></td>
</tr>
<tr>
<td>显示当前目录完整路径</td>
<td><code>pwd</code></td>
</tr>
<tr>
<td>查看目录下的信息（包括隐藏文件）</td>
<td><code>ls</code>（<code>ls -a</code>）</td>
</tr>
<tr>
<td>列出目录下的文件和详细信息</td>
<td><code>ls-l</code> （<code>ll</code>）</td>
</tr>
<tr>
<td>创建目录</td>
<td><code>mkdir</code></td>
</tr>
<tr>
<td>创建文件</td>
<td><code>touch</code></td>
</tr>
<tr>
<td>复制文件（文件夹）</td>
<td><code>cp</code>（<code>cp -r</code>）</td>
</tr>
<tr>
<td>移动/重命名文件夹和目录</td>
<td><code>mv</code></td>
</tr>
<tr>
<td>删除文件（目录）</td>
<td><code>rm</code>（<code>rm -rf</code>）</td>
</tr>
<tr>
<td>删除空文件夹</td>
<td><code>rmdir</code></td>
</tr>
<tr>
<td>查找文件</td>
<td><code>find</code></td>
</tr>
<tr>
<td>获取帮助</td>
<td><code>man</code> / <code>info</code></td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>还有几个常用的快捷键：</p>
<table>
<thead>
<tr>
<th>作用</th>
<th>快捷键</th>
</tr>
</thead>
<tbody>
<tr>
<td>清空至行首</td>
<td>Ctrl + U</td>
</tr>
<tr>
<td>清空至行尾</td>
<td>Ctrl + K</td>
</tr>
<tr>
<td>清屏</td>
<td>Ctrl + L</td>
</tr>
<tr>
<td>终止执行的命令</td>
<td>Ctrl + C</td>
</tr>
</tbody>
</table>
<p>值得一提的是，这些命令在其他系统也可以使用。</p>
<!--kg-card-end: markdown--><h3 id="1-3-tree-">1.3 tree 命令查看目录树</h3><p>我们可以使用 <code>tree</code> 命令方便地查看目录树，但是系统本身却并没有安装 <code>tree</code> 命令，所以我们要首先安装一下 <code>sudo yum -y install tree</code>，然后我们就可以快乐使用了：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-11.png" class="kg-image" alt="image-11" width="600" height="400" loading="lazy"></figure><h2 id="2-vim-">2. vim 编辑器使用方法</h2><p>vim 编辑器是 CentOS 系统中使用频率比较高的编辑器，掌握基本使用方法对以后的工作有很大帮助。</p><p>通过 <code>vim &lt;文件名&gt;</code> 的方式可以编辑某文档，如果文档名不存在，那么会新建一个文档来进行编辑。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-12.png" class="kg-image" alt="image-12" width="600" height="400" loading="lazy"></figure><p>vim 共分为三种模式，分别是<strong>命令模式（Command mode）</strong>，<strong>输入模式（Insert mode）<strong>和</strong>底线命令模式（Last line mode）</strong>。</p><h3 id="2-1-">2.1 命令模式</h3><p>启动 vim 后就进入了命令模式，此状态下敲击键盘动作会被认为是命令，而非输入字符。常用的几个命令：</p><ul><li><strong>i</strong> 切换到输入模式，以输入字符；</li><li><strong>:</strong> 切换到底线命令模式，以在最底一行输入命令；</li><li><strong>a</strong> 切换到输入文字模式；</li></ul><p>命令模式只有一些最基本的命令，要依靠底线命令模式输入更多命令。</p><h3 id="2-2-">2.2 输入模式</h3><!--kg-card-begin: markdown--><p>在命令模式下按下 <code>i</code> 就进入了输入模式。在输入模式中，可以使用以下按键：</p>
<table>
<thead>
<tr>
<th>功能</th>
<th>命令</th>
</tr>
</thead>
<tbody>
<tr>
<td>向上翻页</td>
<td>PageDown / Ctrl + F</td>
</tr>
<tr>
<td>向下翻页</td>
<td>PageUp / Ctrl + B</td>
</tr>
<tr>
<td>跳转到文件首行</td>
<td><code>1G</code> / <code>gg</code></td>
</tr>
<tr>
<td>跳转到末尾行</td>
<td><code>G</code></td>
</tr>
<tr>
<td>跳转到第 # 行</td>
<td><code>#G</code></td>
</tr>
<tr>
<td>行号显示</td>
<td><code>:set nu</code></td>
</tr>
<tr>
<td>行号显示取消</td>
<td><code>:set nonu</code></td>
</tr>
<tr>
<td>插入</td>
<td><code>d</code> / Del</td>
</tr>
<tr>
<td>删除当前行</td>
<td><code>dd</code></td>
</tr>
<tr>
<td>复制</td>
<td><code>yy</code></td>
</tr>
<tr>
<td>将缓冲区中的内容粘贴到光标位置处之后</td>
<td><code>p</code></td>
</tr>
</tbody>
</table>
<p>还有一些其他命令，比如删除从光标处开始的 # 行内容 <code>#dd</code>，复制从光标处开始的 # 行内容 <code>#yy</code> 等，可以看文档 <a href="https://www.runoob.com/linux/linux-vim.html">&lt;Linux vi/vim | 菜鸟教程&gt;</a> 一文。</p>
<!--kg-card-end: markdown--><h3 id="2-3-">2.3 底线命令模式</h3><p>在命令模式下按下:（英文冒号）就进入了底线命令模式。</p><p>底线命令模式可以输入单个或多个字符的命令，可用的命令非常多。</p><p>在底线命令模式中，基本的命令有（已经省略了冒号）：</p><ul><li><code>:q</code> 退出程序；</li><li><code>:q!</code> 放弃对文件内容的修改并退出；</li><li><code>:w</code> 保存文件；</li><li><code>:w /root/xx</code> 另存为；</li><li><code>:wq</code> 保存文件并退出；</li></ul><h3 id="2-4-">2.4 查看文件内容</h3><!--kg-card-begin: markdown--><p>一些常见查看文件内容的命令：</p>
<table>
<thead>
<tr>
<th>功能</th>
<th>命令</th>
</tr>
</thead>
<tbody>
<tr>
<td>浏览文件全部内容</td>
<td><code>more</code> / <code>less</code></td>
</tr>
<tr>
<td>查看文件内容（显示行号）</td>
<td><code>cat</code> (<code>cat -n</code>)</td>
</tr>
<tr>
<td>在文本文件中查找字符串（显示行号）</td>
<td><code>grep</code> （<code>grep &lt;关键字&gt; &lt;要查找的文件&gt; -n</code>）</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h3 id="2-5-">2.5 管道符</h3><p><strong>管道符</strong> 将一个命令的执行结果作为另一个命令的输入来执行，格式 <code>cmd1 | cmd2 ... | cmdn</code></p><p>比如，将 <code>/etc</code> 目录中的文件名以 pass 开头的文件列举出来 <code>ls /etc | grep pass*</code></p><p>再比如，查看 <code>/etc</code> 目录下的内容，并使用 less 的形式浏览 <code>ls /etc | less</code></p><h3 id="2-6-">2.6 重定向</h3><p>功能命令输出重定向，已有原来的文件则替换<code>&gt;</code>输出重定向，如果原来的文件存在则追加在原来的内容之前<code>&gt;&gt;</code>输入重定向，即命令的输入不通过键盘来完成，而通过其他的方式<code>&lt;</code>错误重定向<code>2&gt;</code>输出重定向与错误重定向同时实现<code>&amp;&gt;</code></p><p>比如，将 <code>ls /</code> 命令执行的结果输出到 <code>2.txt</code> 中 <code>ls / &gt; 2.txt</code></p><h2 id="3-">3. 进程管理</h2><h3 id="2-1--1">2.1 进程管理</h3><p><strong>进程：</strong> 是正在执行的一个程序或命令，每一个进程都是一个运行的实体，都有自己的地址空间，并占用一定的系统资源。</p><p>进程管理最重要的就是 <code>ps</code> 命令：</p><ul><li><code>ps aux</code> ，查看系统中所有进程，使用 BSD 操作系统格式；</li><li><code>ps -le</code> ，查看系统中所有进程，使用 Linux 标准命令格式；</li></ul><p><code>ps</code> 命令输出大约如下图所示：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-13.png" class="kg-image" alt="image-13" width="600" height="400" loading="lazy"></figure><p>输出的格式含义：</p><ul><li>USER：该进程由哪个用户产生的；</li><li>PID：进程的 ID；</li><li>%CPU：进程占用 CPU 资源的百分比；</li><li>%MEM：进程占用物理内存的百分比；</li><li>VSZ：进程占用虚拟内存的大小，单位 KB；</li><li>RSS：进程占用实际物理内存的大小，单位 KB；</li><li>TTY：进程在哪个终端运行的，tty1-tty7 代表本地控制台终端，tty1-tty6 是本地的字符界面终端，tty7 是图形终端，pts/0-255 代表虚拟终端，如果是 ? 则代表是系统进程；</li><li>STAT：进程状态，R-运行，S-睡眠，T-停止，s-包含子进程，+-位于后台；</li><li>START：进程启动时间；</li><li>TIME：进程占用 CPU 的运算时间，注意不是系统时间；</li><li>COMMAND，产生此进程的命令名；</li></ul><p>还有个命令 <code>pstree</code>，显示进程树：</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2020/3/4/170a3646f144f91c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="image-20200301191720755" width="600" height="400" loading="lazy"></figure><p><code>top</code> 命令可以查看系统健康状态，和 Windows 系统中的系统管理器类似。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-14.png" class="kg-image" alt="image-14" width="600" height="400" loading="lazy"></figure><p><code>top</code> 命令的交互模式中可以执行下面命令：</p><ul><li><code>?</code>/<code>h</code> ： 显示交互模式的帮助；</li><li><code>P</code> ：以 CPU 使用率排序，默认就是此项；</li><li><code>M</code>：以内存的使用率排序；</li><li><code>N</code> ：以 PID 排序；</li><li><code>q</code> ：退出 <code>top</code>；</li></ul><p>load average 后面的三个数字的意思，分别为系统在之前 1 分钟，5 分钟，15分钟的平均负载。一般认为小于 1 时，负载较小。如果大于 1，系统已经超出负荷。如果是多核 CPU，那么这个数字应该不大于你的 CPU 核心数，比如双核 CPU 时应该不大于 2。</p><p>Tasks 后面的 zombie，意思为僵尸进程，一般是进程无法正常运行，也没有正常退出卡住了，也有可能这个进程正在终止过程中，如果稍微等待一下还有，那么就需要手工检查一下。</p><p>%CPU（s） 的 id 是主要需要看的，意为空闲 CPU 的百分比，如果低于 20，那么系统的状态就比较卡了。</p><!--kg-card-begin: markdown--><h3 id="32">3.2 杀死进程</h3>
<p>杀死进程主要有下面几个命令：</p>
<table>
<thead>
<tr>
<th>功能</th>
<th>命令</th>
</tr>
</thead>
<tbody>
<tr>
<td>杀死某个进程</td>
<td><code>kill</code></td>
</tr>
<tr>
<td>按照进程名杀死进程</td>
<td><code>killall</code></td>
</tr>
<tr>
<td>按照进程名杀死进程，加 <code>-t</code> 可以按照终端号踢出用户</td>
<td><code>pkill</code></td>
</tr>
</tbody>
</table>
<p>杀死进程时，可以跟信号，信号很多，常用信号：</p>
<table>
<thead>
<tr>
<th>信号代号</th>
<th>信号名称</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>SIGHUP</td>
<td>让进程立即关闭，然后重新读取配置文件之后重启，平滑重启</td>
</tr>
<tr>
<td>2</td>
<td>SIGINT</td>
<td>程序终止信号，用于终止前台进程，相当于 ctrl + c 快捷键</td>
</tr>
<tr>
<td>9</td>
<td>SIGKILL</td>
<td>强制终止，用来立即结束程序的运行，本信号不能被阻塞、处理和忽略</td>
</tr>
<tr>
<td>15</td>
<td>SIGTERM</td>
<td>正常结束的信号，kill 命令默认就是这个信号，有时候进程已经发生问题，正常无法终止，此时会使用 -9 信号</td>
</tr>
</tbody>
</table>
<p>所以常用杀死进程的命令：正常杀死 <code>kill -1 2235</code> 或者强制杀死 <code>kill -9 2235</code></p>
<h3 id="33">3.3 修改进程优先级</h3>
<p>我们可以 <code>ps -le | more</code> 来查看进程优先级：</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-15.png" class="kg-image" alt="image-15" width="600" height="400" loading="lazy"></figure><p>PRI 代表 Priority ， NI 代表 Nice，这两个值都是优先级，数字越小代表该进程优先级越高。用户只能修改 NI，不能直接修改 PRI，但系统最终取 PRI + NI 的值。NI 值的范围是 -20 到 19，普通用户调整 NI 值的范围是 0-19，而且只能调整自己的进程，root 用户才能设定进程 NI 值为负值。</p><p>可以使用 <code>nice</code> 命令来修改优先级，<code>nice &lt;选项&gt; 命令</code>，nice 命令可以给新执行的命令直接赋予 NI 值，但是不能修改已经存在进程的 NI 值。选项 <code>-n 值</code> 给命令赋予 NI 值。</p><p>比如修改 apache 的进程优先级 <code>nice -n -5 service httpd start</code></p><p>如果要修改已存在的进程的优先级，需要使用 <code>renice</code> 命令，<code>renice &lt;优先级&gt; PDID</code>，PID 为某一个进程的 ID。</p><p>比如 <code>renice -10 2125</code> 修改 ID 2125 的进程 NI 值为 -10。</p><h2 id="4-">4. 工作管理</h2><ol><li>当前的登录终端，只能管理当前终端的工作，而不能管理其他登录终端的工作；</li><li>放入后台的命令必须可以持续运行一段时间，这样我们才能扑捉和操作这个工作；</li><li>放入后台执行的命令不能和前台用户有交互或需要前台输入，否则放入后台只能暂停，而不能执行；</li></ol><p>把进程放入后台有两个主要命令：</p><ol><li><code>&lt;命令&gt; &amp;</code> 把命令放入后台，并在后台执行</li><li><code>&lt;命令&gt;</code> 执行后按下 ctrl + z 快捷键，放在后台暂停</li></ol><p>查看正在后台的工作，可以使用 <code>jobs [-l]</code> 命令，<code>-l</code> 是显示工作的 PID。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-16.png" class="kg-image" alt="image-16" width="600" height="400" loading="lazy"></figure><p><code>+</code> 代表最近一个放入后台的工作，也是工作恢复时，默认恢复的工作，<code>-</code> 代表倒数第二个放入后台的工作。</p><p>恢复到前台：</p><ol><li><code>fg %工作号</code> 将后台暂停的工作恢复到前台执行，这里的 % 可以省略，注意工作号和 PID 的区别；</li><li><code>bg %工作号</code> 将后台暂停的工作恢复到后台执行，后台恢复执行的命令，是不能和前台有交互的，否则不能恢复到后台执行；</li></ol><p>后台命令脱离登陆终端执行的方法：</p><ol><li>第一种方法是把需要后台执行的命令加入 <code>/etc/rc.local</code> 文件；</li><li>第二种方法是使用系统定时任务，让系统在指定的时间执行某个后台命令；</li><li>第三种方法是使用 <code>nohup</code> 命令；</li></ol><p><code>nohup</code> 命令的使用方法 <code>nohup &lt;命令&gt; &amp;</code></p><h2 id="5-ssh-">5. SSH 操作</h2><p>Secure Shell(SSH)是建立在应用层基础上的安全网络协议，是专为远程登录会话和其他网络服务提供安全性的协议，可有效弥补网络中的漏洞。通过 SSH，可以把所有传输的数据进行加密，也能够防止 DNS 欺骗和 IP 欺骗。还有一个额外的好处就是传输的数据是经过压缩的，所以可以加快传输的速度，已经成为Linux系统的标准配置。</p><h3 id="5-1-ssh-">5.1 SSH 登陆服务器</h3><pre><code>ssh -p port &lt;username&gt;@&lt;hostname or IP address&gt;</code></pre><p>比如我这里购买的腾讯云服务器就可以使用 <code>ssh root@&lt;公网IP/域名&gt;</code> 连接，如果你设置过域名对 IP 的映射，那么 <code>@</code> 后面写你的域名也可以，比如我就可以 <code>ssh root@sherlocked93</code> 连接服务器。</p><p>然后就是输入密码，就可以进入 CentOS 系统了，但是每次登陆都需要密码，挺蠢的也不安全，我们可以设置使用 SSH 密钥的方式，密钥登陆的方式可以百度一下。</p><p>连接系统之后，可以通过 Ctrl+D 或者 <code>exit</code> 命令退出远程登录。</p><p>连接上 CentOS 之后，命令行左侧的命令提示符含义如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-17.png" class="kg-image" alt="image-17" width="600" height="400" loading="lazy"></figure><h3 id="5-2-ssh-">5.2 SSH 上传/下载文件</h3><p>SSH 可以通过 scp 命令来上传文件，是 Linux 系统下基于 SSH 登陆进行安全的远程文件拷贝命令，scp 是 secure copy 的简写，可以使用它上传本地文件夹到远程服务器，也可以从远程服务器上下载文件夹到本地：</p><pre><code># 上传文件夹到远程服务器
scp -P port -r /local/dir username@servername:/remote/dir
# scp -p 2333 -r /test/a root@192.168.0.101:/var/b

# 从远程服务器下载文件夹
scp -P port -r username@servername:/remote/dir/ /local/dir
# scp -p 2333 -r root@192.168.0.101:/var/b /test/a</code></pre><p><code>-r</code> 参数表示递归复制，即复制该目录下面的文件和目录，如果要上传单个文件，只要把 <code>-r</code> 删除。大写的 <code>P</code> 表示的是端口，如果还是默认的 SSH 端口 22 没有更改，则不需要 <code>-P</code>。</p><h3 id="5-3-ssh-">5.3 SSH 设置超时断开</h3><p>SSH 在使用时，经常会因为闲置时间过长而倍服务器自动断开，然后又要重新连接，比较麻烦，可以设置一下防止经常被服务器踢出。</p><p>一种方法就是修改服务器设置，找到所在用户的 <code>.ssh</code> 目录，如 root 用户该目录在：<code>/root/.ssh/</code>，在该目录创建 config 文件</p><pre><code>vim /root/.ssh/config</code></pre><p>加入下面一句：</p><pre><code>ServerAliveInterval 60</code></pre><p>然后 ESC 再 <code>:wq</code> 保存退出，重新开启 root 用户的shell，则再 SSH 远程服务器的时候，不会因为长时间操作断开。</p><p>还有种方法设置 <code>$TMOUT</code> 系统环境变量</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/10/image-18.png" class="kg-image" alt="image-18" width="600" height="400" loading="lazy"></figure><p><code>vim /etc/profile</code> 在最后一行加上：</p><pre><code>export TMOUT=0</code></pre><p>设置 <code>TMOUT</code> 参数为 0 的意思就是设置不超时，然后 ESC 再 <code>:wq</code> 保存退出，再 <code>source /etc/profile</code> 让配置立即生效。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 前端 Web Workers 到底是什么 ]]>
                </title>
                <description>
                    <![CDATA[ 以前我们总说，JS 是单线程没有多线程，当 JS 在页面中运行长耗时同步任务的时候就会导致页面假死影响用户体验，从而需要设置把任务放在任务队列中；执行任务队列中的任务也并非多线程进行的，然而现在 HTML5 提供了我们前端开发这样的能力 - Web Workers API [https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API]，我们一起来看一看 Web Worker 是什么，怎么去使用它，在实际生产中如何去用它来进行产出。 1. 概述 Web Workers 使得一个 Web 应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个单独的线程中执行费时的处理任务，从而允许主（通常是 UI）线程运行而不被阻塞。 它的作用就是给 JS 创造多线程运行环境，允许主线程创建 worker 线程，分配任务给后者，主线程运行的同时 worker 线程也在运行，相互不干扰，在 worker 线程运行结束后把结果返回给主线程。这样做的好处是主线程可以把计算密集型或高延迟的任务交给 worker ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/general-introduction-to-web-worker/</link>
                <guid isPermaLink="false">5d975432fbfdee429dc5ff15</guid>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Mon, 06 Jul 2020 02:40:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/10/v2-94f939f3e438615eeab66bb4e30ea182_hd.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>以前我们总说，JS 是单线程没有多线程，当 JS 在页面中运行长耗时同步任务的时候就会导致页面假死影响用户体验，从而需要设置把任务放在任务队列中；执行任务队列中的任务也并非多线程进行的，然而现在 HTML5 提供了我们前端开发这样的能力 - <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API">Web Workers API</a>，我们一起来看一看 Web Worker 是什么，怎么去使用它，在实际生产中如何去用它来进行产出。</p><h2 id="1-">1. 概述</h2><p>Web Workers 使得一个 Web 应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个单独的线程中执行费时的处理任务，从而允许主（通常是 UI）线程运行而不被阻塞。</p><p>它的作用就是给 JS 创造多线程运行环境，允许主线程创建 worker 线程，分配任务给后者，主线程运行的同时 worker 线程也在运行，相互不干扰，在 worker 线程运行结束后把结果返回给主线程。这样做的好处是主线程可以把计算密集型或高延迟的任务交给 worker 线程执行，这样主线程就会变得轻松，不会被阻塞或拖慢。这并不意味着 JS 语言本身支持了多线程能力，而是浏览器作为宿主环境提供了 JS 一个多线程运行的环境。</p><p>不过因为 worker 一旦新建，就会一直运行，不会被主线程的活动打断，这样有利于随时响应主线程的通性，但是也会造成资源的浪费，所以不应过度使用，用完注意关闭。或者说：如果 worker 无实例引用，该 worker 空闲后立即会被关闭；如果 worker 实列引用不为 0，该 worker 空闲也不会被关闭。</p><p>看一看它的<a href="https://caniuse.com/#search=webworker">兼容性</a></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image.png" class="kg-image" alt="image" width="665" height="118" loading="lazy"></figure><h2 id="2-">2. 使用</h2><h3 id="2-1-">2.1 限制</h3><p>worker 线程的使用有一些注意点：</p><ol><li>同源限制 worker 线程执行的脚本文件必须和主线程的脚本文件同源，这是当然的了，总不能允许 worker 线程到别人电脑上到处读文件吧</li><li>文件限制 为了安全，worker 线程无法读取本地文件，它所加载的脚本必须来自网络，且需要与主线程的脚本同源</li><li>DOM 操作限制 worker 线程在与主线程的 window 不同的另一个全局上下文中运行，其中无法读取主线程所在网页的 DOM 对象，也不能获取 <code>document</code>、<code>window</code>等对象，但是可以获取<code>navigator</code>、<code>location(只读)</code>、<code>XMLHttpRequest</code>、<code>setTimeout族</code>等浏览器API</li><li>通信限制 worker 线程与主线程不在同一个上下文，不能直接通信，需要通过<code>postMessage</code>方法来通信</li><li>脚本限制 worker 线程不能执行<code>alert</code>、<code>confirm</code>，但可以使用 <code>XMLHttpRequest</code> 对象发出 ajax 请求</li></ol><h3 id="2-2-">2.2 例子</h3><p>在主线程中生成 Worker 线程很容易：</p><pre><code>var myWorker = new Worker(jsUrl, options)</code></pre><p>Worker() 构造函数，第一个参数是脚本的网址（必须遵守同源政策），该参数是必需的，且只能加载 JS 脚本，否则报错。第二个参数是配置对象，该对象可选。它的一个作用就是指定 Worker 的名称，用来区分多个 Worker 线程。</p><pre><code>// 主线程

var myWorker = new Worker('worker.js', { name : 'myWorker' });

// Worker 线程
self.name // myWorker</code></pre><p>关于 api 什么的，直接上例子大概就能明白了，首先是 worker 线程的 js 文件：</p><pre><code>// workerThread1.js

let i = 1

function simpleCount() {
  i++
  self.postMessage(i)
  setTimeout(simpleCount, 1000)
}

simpleCount()

self.onmessage = ev =&gt; {
  postMessage(ev.data + ' 呵呵~')
}</code></pre><p>在 HTML 文件中的 body 中：</p><pre><code>&lt;!--主线程，HTML文件的body标签中--&gt;

&lt;div&gt;
  Worker 输出内容：&lt;span id='app'&gt;&lt;/span&gt;
  &lt;input type='text' title='' id='msg'&gt;
  &lt;button onclick='sendMessage()'&gt;发送&lt;/button&gt;
  &lt;button onclick='stopWorker()'&gt;stop!&lt;/button&gt;
&lt;/div&gt;

&lt;script type='text/javascript'&gt;
  if (typeof(Worker) === 'undefined')	// 使用Worker前检查一下浏览器是否支持
    document.writeln(' Sorry! No Web Worker support.. ')
  else {
    window.w = new Worker('workerThread1.js')
    window.w.onmessage = ev =&gt; {
      document.getElementById('app').innerHTML = ev.data
    }
    
    window.w.onerror = err =&gt; {
      w.terminate()
      console.log(error.filename, error.lineno, error.message) // 发生错误的文件名、行号、错误内容
    }
    
    function sendMessage() {
      const msg = document.getElementById('msg')
      window.w.postMessage(msg.value)
    }
    
    function stopWorker() {
      window.w.terminate()
    }
  }
&lt;/script&gt;</code></pre><p>可以自己运行一下看看效果，上面用到了一些常用的 api。</p><p>主线程中的api，<code>worker</code>表示是 Worker 的实例：</p><ul><li><code>worker.postMessage</code>: 主线程往 worker 线程发消息，消息可以是任意类型数据，包括二进制数据</li><li><code>worker.terminate</code>: 主线程关闭 worker 线程</li><li><code>worker.onmessage</code>: 指定 worker 线程发消息时的回调，也可以通过<code>worker.addEventListener('message',cb)</code>的方式</li><li><code>worker.onerror</code>: 指定 worker 线程发生错误时的回调，也可以 <code>worker.addEventListener('error',cb)</code></li></ul><p>Worker 线程中全局对象为 <code>self</code>，代表子线程自身，这时 <code>this</code>指向<code>self</code>，其上有一些 api：</p><ul><li><code>self.postMessage</code>: worker 线程往主线程发消息，消息可以是任意类型数据，包括二进制数据</li><li><code>self.close</code>: worker 线程关闭自己</li><li><code>self.onmessage</code>: 指定主线程发 worker 线程消息时的回调，也可以<code>self.addEventListener('message',cb)</code></li><li><code>self.onerror</code>: 指定 worker 线程发生错误时的回调，也可以 <code>self.addEventListener('error',cb)</code></li></ul><p>注意，<code>w.postMessage(aMessage, transferList)</code> 方法接受两个参数，<code>aMessage</code> 是可以传递任何类型数据的，包括对象，这种通信是拷贝关系，即是传值而不是传址，Worker 对通信内容的修改，不会影响到主线程。事实上，浏览器内部的运行机制是，先将通信内容串行化，然后把串行化后的字符串发给 Worker，后者再将它还原。一个可选的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Transferable">Transferable</a> 对象的数组，用于传递所有权。如果一个对象的所有权被转移，在发送它的上下文中将变为不可用（中止），并且只有在它被发送到的 worker 中可用。可转移对象是如 ArrayBuffer，MessagePort 或 ImageBitmap 的实例对象，<code>transferList</code>数组中不可传入 null。</p><p>更详细的 API 参见 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/WorkerGlobalScope">MDN - WorkerGlobalScope</a>。</p><p>worker 线程中加载脚本的 api：</p><pre><code>importScripts('script1.js')	// 加载单个脚本
importScripts('script1.js', 'script2.js')	// 加载多个脚本</code></pre><h2 id="3-">3. 实战场景</h2><p>个人觉得，Web Worker 我们可以当做计算器来用，需要用的时候掏出来摁一摁，不用的时候一定要收起来。</p><p>加密数据 有些加解密的算法比较复杂，或者在加解密很多数据的时候，这会非常耗费计算资源，导致 UI 线程无响应，因此这是使用 Web Worker 的好时机，使用 Worker 线程可以让用户更加无缝的操作 UI。</p><p>预取数据 有时候为了提升数据加载速度，可以提前使用 Worker 线程获取数据，因为 Worker 线程是可以是用 <code>XMLHttpRequest</code> 的。</p><p>预渲染 在某些渲染场景下，比如渲染复杂的 canvas 的时候需要计算的效果比如反射、折射、光影、材料等，这些计算的逻辑可以使用 Worker 线程来执行，也可以使用多个 Worker 线程，这里有个<a href="https://nerget.com/rayjs-mt/rayjs.html">射线追踪的示例</a>。</p><p>复杂数据处理场景 某些检索、排序、过滤、分析会非常耗费时间，这时可以使用 Web Worker 来进行，不占用主线程。</p><p>预加载图片 有时候一个页面有很多图片，或者有几个很大的图片的时候，如果业务限制不考虑懒加载，也可以使用 Web Worker 来加载图片，可以参考一下<a href="https://juejin.im/post/5a0875fcf265da431f4a8ddc" rel="">这篇文章的探索</a>，这里简单提要一下。</p><pre><code>// 主线程
let w = new Worker("js/workers.js");
w.onmessage = function (event) {
  var img = document.createElement("img");
  img.src = window.URL.createObjectURL(event.data);
  document.querySelector('#result').appendChild(img)
}

// worker线程
let arr = [...好多图片路径];
for (let i = 0, len = arr.length; i &lt; len; i++) {
    let req = new XMLHttpRequest();
    req.open('GET', arr[i], true);
    req.responseType = "blob";
    req.setRequestHeader("client_type", "DESKTOP_WEB");
    req.onreadystatechange = () =&gt; {
      if (req.readyState == 4) {
      postMessage(req.response);
    }
  }
  req.send(null);
}</code></pre><p>在实战的时候注意</p><ul><li>虽然使用 worker 线程不会占用主线程，但是启动 worker 会比较耗费资源</li><li>主线程中使用 XMLHttpRequest 在请求过程中浏览器另开了一个异步 http 请求线程，但是交互过程中还是要消耗主线程资源</li></ul><p>在 Webpack 项目里面使用 Web Worker 请参照：<a href="https://juejin.im/post/5acf348151882579ef4f5a77" rel="">怎么在 ES6+Webpack 下使用 Web Worker</a></p><p>至于还有 Shared Worker、Service Worker 什么的，我们就不看了，IE 不喜欢。</p><hr><p>网上的帖子大多深浅不一，甚至有些前后矛盾，在下的文章都是学习过程中的总结，如果发现错误，欢迎留言指出。</p><p>参考：</p><ol><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API">MDN - Web Workers 概念与用法</a></li><li><a href="http://www.ruanyifeng.com/blog/2018/07/web-worker.html">阮一峰 - Web Worker 使用教程</a></li><li><a href="https://segmentfault.com/a/1190000014938305">JavaScript 工作原理之七－Web Workers 分类及 5 个使用场景</a></li><li><a href="https://juejin.im/post/5a0875fcf265da431f4a8ddc" rel="">Web Worker 在项目中的妙用</a></li><li><a href="https://juejin.im/post/5acf348151882579ef4f5a77" rel="">怎么在 ES6+Webpack 下使用 Web Worker</a></li></ol> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 编程基础知识之浏览器缓存机制 ]]>
                </title>
                <description>
                    <![CDATA[ 最近在项目中遇到了 IE 浏览器因缓存问题未能成功向后端发送 GET类型请求 的 bug，然后顺藤摸瓜顺便看了看缓存的知识，觉得有必要总结跟大家分享一下。 在前端开发中，性能一直都是被大家所重视的一点，然而判断一个网站的性能最直观的就是看网页打开的速度。其中提高网页反应速度的一个方式就是使用缓存。一个优秀的缓存策略可以缩短网页请求资源的距离，减少延迟，并且由于缓存文件可以重复利用，还可以减少带宽，降低网络负荷。 1. 介绍 Web 缓存是指一个 Web 资源（如 html 页面，图片，js，数据等）存在于 Web 服务器和客户端（浏览器）之间的副本。 缓存会根据进来的请求保存输出内容的副本；当下一个请求来到的时候，如果是相同的 URL，缓存会根据缓存机制决定是直接使用副本响应访问请求，还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页，当再次访问这个 URL 地址的时候，如果网页没有更新，就不会再次下载网页，而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新，浏览器才会再次下载网页。至于浏览器和网站服务器是如何标识网站页面是否更新的机制，将在后面介绍。 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/an-introduction-to-the-browser-cache-mechanism/</link>
                <guid isPermaLink="false">5d764153fbfdee429dc5fb4b</guid>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Thu, 05 Mar 2020 02:13:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/09/1_LD9BykawGFTJyJGdv6NUZQ.gif" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近在项目中遇到了 IE 浏览器因缓存问题未能成功向后端发送 <code>GET</code>类型请求 的 bug，然后顺藤摸瓜顺便看了看缓存的知识，觉得有必要总结跟大家分享一下。</p><p>在前端开发中，性能一直都是被大家所重视的一点，然而判断一个网站的性能最直观的就是看网页打开的速度。其中提高网页反应速度的一个方式就是使用缓存。一个优秀的缓存策略可以缩短网页请求资源的距离，减少延迟，并且由于缓存文件可以重复利用，还可以减少带宽，降低网络负荷。</p><h2 id="1-">1. 介绍</h2><p><strong>Web 缓存</strong>是指一个 Web 资源（如 html 页面，图片，js，数据等）存在于 Web 服务器和客户端（浏览器）之间的副本。</p><p>缓存会根据进来的请求保存输出内容的副本；当下一个请求来到的时候，如果是相同的 URL，缓存会根据缓存机制决定是直接使用副本响应访问请求，还是向源服务器再次发送请求。比较常见的就是浏览器会缓存访问过网站的网页，当再次访问这个 URL 地址的时候，如果网页没有更新，就不会再次下载网页，而是直接使用本地缓存的网页。只有当网站明确标识资源已经更新，浏览器才会再次下载网页。至于浏览器和网站服务器是如何标识网站页面是否更新的机制，将在后面介绍。</p><h3 id="1-1-web-">1.1 Web 缓存的作用</h3><p>Web 缓存的<strong>作用</strong>显而易见：</p><ul><li><strong>减少网络带宽消耗</strong>：无论对于网站运营者或者用户，带宽都代表着金钱，过多的带宽消耗，只会便宜了网络运营商。当 Web 缓存副本被使用时，只会产生极小的网络流量，可以有效的降低运营成本。</li><li><strong>降低服务器压力</strong>：给网络资源设定有效期之后，用户可以重复使用本地的缓存，减少对源服务器的请求，间接降低服务器的压力。同时，搜索引擎的爬虫机器人也能根据过期机制降低爬取的频率，也能有效降低服务器的压力。</li><li><strong>减少网络延迟，加快页面打开速度</strong>：带宽对于个人网站运营者来说是十分重要，而对于大型的互联网公司来说，可能有时因为钱多而真的不在乎。那 Web 缓存还有作用吗？答案是肯定的，对于最终用户，缓存的使用能够明显加快页面打开速度，达到更好的体验。</li></ul><h3 id="1-2-web-">1.2 Web 缓存的类型</h3><p>Web 缓存大致可以分为以下几种类型 详细内容：</p><ul><li>数据库数据缓存</li><li>服务器端缓存</li><li>浏览器端缓存</li><li>Web 应用层缓存</li></ul><p>浏览器通过代理服务器向源服务器发起请求的原理如下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-24.png" class="kg-image" alt="image-24" width="554" height="125" loading="lazy"></figure><p>浏览器先向代理服务器发起 Web 请求，再将请求转发到源服务器。它属于共享缓存，所以很多地方都可以使用其缓存资源，因此对于节省流量有很大作用。</p><p>浏览器缓存是将文件保存在客户端，在同一个会话过程中会检查缓存的副本是否足够新，在后退网页时，访问过的资源可以从浏览器缓存中拿出使用。通过减少服务器处理请求的数量，用户将获得更快的体验</p><p>下面着重关注一下浏览器缓存。</p><h2 id="2-web-">2. Web 缓存的工作原理</h2><p>所有的缓存都是基于一套规则来帮助他们决定什么时候使用缓存中的副本提供服务（假设有副本可用的情况下，未被销毁回收或者未被删除修改）。这些规则有的在协议中有定义（如 HTTP 协议1.0和1.1），有的则是由缓存的管理员设置（如 DBA、浏览器的用户、代理服务器管理员或者应用开发者）。</p><h3 id="2-1-">2.1 浏览器端的缓存规则</h3><p>对于浏览器端的缓存来讲，这些规则是在 HTTP 协议头和 HTML 页面的 <code>Meta</code>标签中定义的。他们分别从<strong>新鲜度</strong>和<strong>校验值</strong>两个维度来规定浏览器是直接使用缓存中的副本，还是需要去源服务器获取更新的版本。</p><p><strong>新鲜度</strong>（过期机制）：也就是缓存副本有效期。一个缓存副本必须满足以下任一条件，浏览器会认为它是有效的，足够新的，而直接从缓存中获取副本并渲染：</p><ul><li>含有完整的过期时间控制头信息（HTTP 协议报头），并且仍在有效期内</li><li>浏览器已经使用过这个缓存副本，并且在一个会话中已经检查过新鲜度</li></ul><p><strong>校验值</strong>（验证机制）：服务器返回资源的时候有时在控制头信息带上这个资源的实体标签 Etag（Entity Tag），它可以用来作为浏览器再次请求过程的校验标识。如过发现校验标识不匹配，说明资源已经被修改或过期，浏览器需求重新获取资源内容。</p><h3 id="2-2-">2.2 浏览器缓存的控制</h3><h4 id="2-2-1-html-meta-">2.2.1 使用 HTML 的 <code>Meta</code> 标签</h4><p><code>&lt;META HTTP-EQUIV="Pragma" CONTENT="no-cache"&gt;</code></p><p>上述代码的作用是告诉浏览器当前页面不被缓存，每次访问都需要去服务器拉取。使用上很简单，但只有部分浏览器可以支持，而且所有缓存代理服务器都不支持，因为代理不解析 HTML 内容本身。可以通过这个页面测试你的浏览器是否支持：[Pragma No-Cache Test] (http://www.procata.com/cachetest/tests/pragma/index.php)。</p><h4 id="2-2-2-http-">2.2.2 使用缓存有关的 HTTP 消息报头</h4><p>一个 URI 的完整 HTTP 协议交互过程是由 HTTP 请求和 HTTP 响应组成的。有关 HTTP 详细内容可参考《Hypertext Transfer Protocol — HTTP/1.1》、《HTTP 协议详解》等。</p><p>在 HTTP 请求和响应的消息报头中，常见的与缓存有关的消息报头有：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-15.png" class="kg-image" alt="image-15" width="597" height="596" loading="lazy"></figure><p>稍微解释一下：</p><p><strong>(1) Cache-Control</strong></p><p><strong>max-age</strong>（单位为 s）指定设置缓存最大的有效时间，定义的是时间长短。当浏览器向服务器发送请求后，在 max-age 这段时间里浏览器就不会再向服务器发送请求了。我们来找个资源看下。比如 QQ 推广上的 css 资源，max-age=3600，也就是说缓存有效期为 3600 秒（也就是 1h）。于是在1天内都会使用这个版本的资源，即使服务器上的资源发生了变化，浏览器也不会得到通知。max-age 会覆盖掉 Expires，后面会有讨论。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-16.png" class="kg-image" alt="image-16" width="386" height="166" loading="lazy"></figure><p><br><strong>s-maxage</strong>（单位为 s）同 max-age，只用于共享缓存（比如 CDN 缓存）。比如，当 s-maxage=60 时，在这60秒中，即使更新了 CDN 的内容，浏览器也不会进行请求。也就是说 max-age 用于普通缓存，而 s-maxage 用于代理缓存。如果存在 s-maxage，则会覆盖掉 max-age 和 Expires header。</p><p><strong>public</strong> 指定响应会被缓存，并且在多用户间共享。也就是下图的意思。如果没有指定 public 还是 private，则默认为 public。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-17.png" class="kg-image" alt="image-17" width="541" height="292" loading="lazy"></figure><p><strong>private</strong> 响应只作为私有的缓存（见下图），不能在用户间共享。如果要求 HTTP 认证，响应会自动设置为 private。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-18.png" class="kg-image" alt="image-18" width="503" height="128" loading="lazy"></figure><p><strong>no-cache</strong> 指定不缓存响应，表明资源不进行缓存，但是设置了 no-cache 之后并不代表浏览器不缓存，而是在获取缓存前要向服务器确认资源是否被更改。因此有的时候只设置 no-cache 防止缓存还是不够保险，还可以加上 private 指令，将过期时间设为过去的时间。</p><p><strong>no-store</strong> 绝对禁止缓存，一看就知道如果用了这个命令当然就是不会进行缓存啦～每次请求资源都要从服务器重新获取。</p><p><strong>must-revalidate</strong> 指定如果页面是过期的，则去服务器进行获取。这个指令并不常用，就不做过多的讨论了。</p><p><strong>cache-control</strong>的种类这么多，然而怎么使用它们呢，参看下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-19.png" class="kg-image" alt="image-19" width="554" height="705" loading="lazy"></figure><p><strong>(2) Expires</strong></p><p>缓存过期时间，用来指定资源到期的时间，是服务器端的具体的时间点。也就是说， &nbsp;<strong>Expires=max-age + 请求时间</strong> ，需要和 Last-modified 结合使用。但在上面我们提到过，cache-control 的优先级更高。Expires 是 Web 服务器响应消息头字段，在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据，而无需再次请求。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-20.png" class="kg-image" alt="image-20" width="382" height="169" loading="lazy"></figure><h5 id=""><br></h5><p><strong>(3) Last-modified &amp; If-modified-since</strong></p><p>服务器端文件的最后修改时间，需要和 cache-control 共同使用，是检查服务器端资源是否更新的一种方式。当浏览器再次进行请求时，会向服务器传送If-Modified-Since报头，询问 Last-Modified 时间点之后资源是否被修改过。如果没有修改，则返回码为304，使用缓存；如果修改过，则再次去服务器请求资源，返回码和首次请求相同为200，资源为服务器最新资源。</p><p><strong>(4) Etag &amp; &amp; If-None-Match</strong></p><p>根据实体内容生成一段 hash 字符串，标识资源的状态，由服务端产生。浏览器会将这串字符串传回服务器，验证资源是否已经修改，如果没有修改，过程如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-21.png" class="kg-image" alt="image-21" width="570" height="292" loading="lazy"></figure><h4 id="2-2-3-">2.2.3 缓存报头种类与优先级</h4><p><strong>(1) Cache-Control与Expires</strong></p><p><code>Cache-Control</code>与 <code>Expires</code>的作用一致，都是指明当前资源的有效期，控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。只不过 <code>Cache-Control</code>的选择更多，设置更细致，如果同时设置的话，其优先级高于 <code>Expires</code>。</p><p><strong>(2) Last-Modified 与 ETag</strong></p><p>你可能会觉得使用 <code>Last-Modified</code> 已经足以让浏览器知道本地的缓存副本是否足够新，为什么还需要 <code>Etag</code>（实体标识）呢？HTTP1.1 中 Etag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题：</p><ul><li>Last-Modified 标注的最后修改只能精确到<strong>秒</strong>级，如果某些文件在1秒钟以内，被修改多次的话，它将不能准确标注文件的新鲜度</li><li>如果某些文件会被定期生成，当有时内容并没有任何变化，但 Last-Modified 却改变了，导致文件没法使用缓存</li><li>有可能存在服务器没有准确获取文件修改时间，或者与代理服务器时间不一致等情形</li></ul><p>Etag 是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符，能够更加准确的控制缓存。Last-Modified 与 ETag 是可以一起使用的，<strong>服务器会优先验证 ETag</strong>，一致的情况下，才会继续比对 Last-Modified，最后才决定是否返回304。Etag 的服务器生成规则和强弱 Etag 的相关内容可以参考，《互动百科-Etag》和《HTTP Header definition》，这里不再深入。</p><p><strong>(3) Last-Modified/ETag 与 Cache-Control/Expires</strong></p><p>配置 <code>Last-Modified/ETag</code>的情况下，浏览器再次访问统一 URI 的资源，还是会发送请求到服务器询问文件是否已经修改，如果没有，服务器会只发送一个304回给浏览器，告诉浏览器直接从自己本地的缓存取数据；如果修改过那就整个数据重新发给浏览器；</p><p><code>Cache-Control/Expires</code>则不同，如果检测到本地的缓存还是有效的时间范围内，浏览器直接使用本地副本，不会发送任何请求。两者一起使用时， <code>Cache-Control/Expires</code>的优先级要高，即当本地副本根据 <code>Cache-Control/Expires</code>发现还在有效期内时，则不会再次发送请求去服务器询问修改时间 <code>Last-Modified</code>或实体标识 <code>Etag</code>了。</p><p>一般情况下，两者会配合一起使用，因为即使服务器设置缓存时间, 当用户点击“刷新”按钮时，浏览器会忽略缓存继续向服务器发送请求，这时 <code>Last-Modified/ETag</code>将能够很好利用304，从而减少响应开销。</p><h4 id="2-2-4-">2.2.4 哪些请求不能被缓存？</h4><p>无法被浏览器缓存的请求：</p><ul><li>HTTP 信息头中包含 Cache-Control:no-cache，pragma:no-cache，或 Cache-Control:max-age=0 等告诉浏览器不用缓存的请求</li><li>需要根据 Cookie，认证信息等决定输入内容的动态请求是不能被缓存的</li><li>经过 HTTPS 安全加密的请求（有人也经过测试发现，ie 其实在头部加入 Cache-Control：max-age 信息，firefox 在头部加入 Cache-Control:Public 之后，能够对 HTTPS 的资源进行缓存，参考《HTTPS 的七个误解》)</li><li>POST 请求无法被缓存</li><li>HTTP 响应头中不包含 Last-Modified/Etag，也不包含 Cache-Control/Expires 的请求无法被缓存</li></ul><h2 id="3-">3. 使用缓存流程</h2><p>一个用户发起一个静态资源请求的时候，浏览器会通过以下几步来获取并展示资源：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-22.png" class="kg-image" alt="image-22" width="692" height="1031" loading="lazy"></figure><p>缓存行为主要由缓存策略决定，而缓存策略由内容拥有者设置。这些策略主要通过特定的 HTTP 头部来清晰地表达。</p><p>以上过程也可以被概括为三个阶段：</p><ul><li><strong>本地缓存阶段：</strong>先在本地查找该资源，如果有发现该资源，而且该资源还没有过期，就使用这一个资源，完全不会发送http请求到服务器；</li><li><strong>协商缓存阶段：</strong>如果在本地缓存找到对应的资源，但是不知道该资源是否过期或者已经过期，则发一个 http 请求到服务器，然后服务器判断这个请求，如果请求的资源在服务器上没有改动过，则返回304，让浏览器使用本地找到的那个资源；</li><li><strong>缓存失败阶段：</strong>当服务器发现请求的资源已经修改过，或者这是一个新的请求(在本来没有找到资源)，服务器则返回该资源的数据，并且返回200， 当然这个是指找到资源的情况下，如果服务器上没有这个资源，则返回404。</li></ul><h2 id="4-">4. <strong>用户操作行为与缓存的关系</strong></h2><p>用户在使用浏览器的时候，会有各种操作，比如输入地址后回车，按 F5 刷新等，这些行为会对缓存有什么影响呢？</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-23.png" class="kg-image" alt="image-23" width="490" height="171" loading="lazy"></figure><p>通过上表我们可以看到，当用户在按 <code>F5</code>进行刷新的时候，会忽略 Expires/Cache-Control 的设置，会再次发送请求去服务器请求，而 Last-Modified/Etag 还是有效的，服务器会根据情况判断返回304还是200；<br></p><p>而当用户使用 <code>Ctrl+F5</code>进行强制刷新的时候，只是所有的缓存机制都将失效，重新从服务器拉去资源。</p><ul><li><strong>普通刷新</strong> – 当按下 F5 或者点击刷新按钮来刷新页面的时候，浏览器将绕过本地缓存来发送请求到服务器，此时，协商缓存是有效的</li><li><strong>强制刷新</strong> – 当按下 ctrl+F5 来刷新页面的时候，浏览器将绕过各种缓存(本地缓存和协商缓存)，直接让服务器返回最新的资源</li><li><strong>回车或转向</strong> – 当在地址栏上输入回车或者按下跳转按钮的时候，所有缓存都生效</li></ul><h2 id="5-">5. 如何从缓存角度改善站点</h2><ul><li>同一个资源保证 URL 的稳定性</li><li>给 css、js、图片等资源增加 HTTP 缓存头，并强制入口 html 不被缓存</li><li>减少对 Cookie 的依赖</li><li>多用 Get 方式请求动态 Cgi</li><li>动态 CGI 也是可以被缓存</li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 你的 Mac 用对了吗？推荐一些 Mac 上比较好用的软件 ]]>
                </title>
                <description>
                    <![CDATA[ 作为一个工具控，一直在社区索取别人的营养，今天在下将我搜集的一些应用贡献出来，推介二十几个我常用的软件。一些是其他人反复推介确实经典，另一些是我偶然发现但经过使用感觉非常好用，一并献上，大家可以根据自己的需要，看看是不是正需要这些软件，并解决自己生产生活中的痛点。 下面我将简单介绍一下这些软件，并且附上下载方式和链接。 本文是《那些好用的工具》系列文章之一：  1. 推介几款 windows 下非常好用的工具 [https://juejin.im/post/5c2eca54f265da61171cdc48]  2. 干货满满！推介几款 Mac 下非常好用的软件（上） [https://juejin.im/post/5de664e5f265da33b82bcfce] 1. Alfred 正如 Windows 系统上，Listary [https://www.listary.com/] 必须排在第一个一样，Mac 平台 Alfred 也必须排在第一个。 有的人说 Alfred 是 Mac 上最强大的工具台，不亲自体验一下，你是无法理解 Alfred 的强大之处，比如我，安装并使用 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/great-tools-you-could-try-on-mac/</link>
                <guid isPermaLink="false">5dee218dca1efa04e196a931</guid>
                
                    <category>
                        <![CDATA[ Mac ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Mon, 09 Dec 2019 10:32:58 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1575873896343-84af1dc92fc8.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>作为一个工具控，一直在社区索取别人的营养，今天在下将我搜集的一些应用贡献出来，推介二十几个我常用的软件。一些是其他人反复推介确实经典，另一些是我偶然发现但经过使用感觉非常好用，一并献上，大家可以根据自己的需要，看看是不是正需要这些软件，并解决自己生产生活中的痛点。</p><p>下面我将简单介绍一下这些软件，并且附上下载方式和链接。</p><p>本文是《那些好用的工具》系列文章之一：</p><ol><li><a href="https://juejin.im/post/5c2eca54f265da61171cdc48" rel="">推介几款 windows 下非常好用的工具</a></li><li><a href="https://juejin.im/post/5de664e5f265da33b82bcfce" rel="">干货满满！推介几款 Mac 下非常好用的软件（上）</a></li></ol><h2 id="1-alfred">1. Alfred</h2><p>正如 Windows 系统上，<a href="https://www.listary.com/" rel="nofollow noopener noreferrer">Listary</a> 必须排在第一个一样，Mac 平台 Alfred 也必须排在第一个。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf925d93cea1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>有的人说 Alfred 是 Mac 上最强大的工具台，不亲自体验一下，你是无法理解 Alfred 的强大之处，比如我，安装并使用之后 5 分钟，我就把聚焦的快捷键取消，一年中使用的次数屈指可数，而 Alfred 却基本上成为我使用频率最高的工具。</p><p><strong>快捷搜索</strong> ，快捷搜索是 Listary 也具有的功能，除了可以搜索本地文件、安装的应用、MacOS 设置项等，还可以使用网页搜索，比如输入 <code>gg 我的存款呢？</code> 就可以直接打开默认浏览器在谷歌搜索中搜索，还可以自定义输入其他关键字，只需把 Search URL 中的关键字换成 <code>{query}</code> 即可。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf925350e6e9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p><strong>快捷打开</strong> ，我们也可以使用快捷打开链接的方式，快速打开默认网页，比如我设置 <code>blog</code> 命令为快速打开我的博客 <a href="https://github.com/SHERlocked93/blog%EF%BC%8C%E9%82%A3%E4%B9%88%E5%8F%AA%E8%A6%81%E8%BE%93%E5%85%A5" rel="nofollow noopener noreferrer">github.com/SHERlocked9…</a> <code>blog</code> 并 <code>Enter</code> 即可在默认浏览器完成打开博客地址的功能。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/12/image-13.png" class="kg-image" alt="image-13" width="1280" height="295" loading="lazy"></figure><p><strong>搜索标签</strong> ，我们可以在 Alfred 里快捷访问我们保存在浏览器中的标签，比如你可以把 Vue、vue-router、vuex 等的官网收藏在浏览器的标签，然后在 Alfred 中键入 <code>bm vue</code>，就可以搜索出我们标签中所有含有 vue 标签的网址。</p><p><strong>文字段自动扩展</strong> ，在下会经常使用一些文字段，比如我会经常键入我的手机号，但是我并不很喜欢经常手打这么长的数字，那么我就设置 <code>zsj</code> 自动扩展成我设置好的手机号。</p><p>想设置的同学在 <code>Features -&gt; Snippets</code> 即可设置，下面列举一些我常用的自动扩展，仅供大家参考。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/12/image-14.png" class="kg-image" alt="image-14" width="1280" height="523" loading="lazy"></figure><p>Alfred 还有其他很多优秀的功能，比如自定义 Workflow、LargeType、计算器等等好玩的功能，不亲自试试是无法理解它好玩的地方的。</p><p>快捷键上有的人使用双击 <code>command</code>，个人比较喜欢使用 <code>option + space</code>，具体使用什么快捷键，可以根据自己的喜好来定。</p><p>App Store 里就可以获取 Alfred，不过不便宜，所以你可以购买，当然也可以自己想想办法 🤫。</p><h2 id="2-paste">2. Paste</h2><p>和 Windows 平台的 <a href="https://ditto-cp.sourceforge.io/" rel="nofollow noopener noreferrer">Ditto</a> 对应的剪切板管理工具是 <a href="https://apps.apple.com/cn/app/paste-clipboard-manager/id967805235" rel="nofollow noopener noreferrer">Paste</a>。</p><p>通过 Paste，可以快速访问你曾经复制过的内容，方便你快速地访问，无需担心丢失重要的已复制内容，也可以在过去复制的内容里搜索你需要的信息，或者把一些你常用的文字段落保存到 Paste 的收藏夹里，在下因为经常写一些文档，这些文档有一些比较通用的页尾，比如参考文档、推介阅读链接等等，这时就可以把这些需要经常使用的内容放在收藏夹里，方便快速查找。</p><p>另外，在复制时还可以去掉原复制源的格式信息，把任意内容复制为纯文本。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf9363095a4f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>按住 <code>Shift + left/right</code> 可以一次选择多个剪切板对象复制，我把激活 Paste 的快捷键设置为 <code>Option + ~</code>，就是数字键 1 左边那个，然后把快速粘贴设置为 <code>Option + 1、2...</code> ，这样就可以在打开的之后，左手拇指按着 Option 不用动，就可以快捷选中其他的选项了。</p><p>App Store 上搜索就可以直达，售价 68 块（也可以在网上找到资源 😏）。</p><h2 id="3-picgo">3. PicGo</h2><p>如果你经常使用 Markdown，那么你一定遇到一个问题，图片怎么转化为图床链接，在用过好几款免费或收费图床工具之后，对这款开源的图床工具使用的功能强大、使用简单印象深刻，而且还支持常用的几种图床，所以这里推介给大家。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf93686189ef?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>你可以把图片拖拽到 Menubar 上的小图标上上传，也可以拖拽到软件首页的上传区上传，也可以把图片复制到剪切板中使用快捷键上传。其中第三种方式使用最为方便，也是在下使用最多的一种方式。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf93977a623b?imageslim" class="kg-image" alt="16ecbf93977a623b?imageslim" width="600" height="400" loading="lazy"></figure><p>图床这里我推介使用两个图床，免费图床可以使用 SM.MS 图床，这个图床是免费图床里速度比较靠谱的，只是偶尔会发生上传失败的情况。如果你比较介意的话，可以选择 Github 的图床，这个图床的原理就是在 Github 创建一个仓库，然后把你的图片上传到这个仓库里，缺点是速度比较慢，而且有时候会因为你懂得原因，需要自备科学上网工具 😅。</p><p>快捷键方面，我把快捷上传快捷键设置为 <code>command + control + shift + u</code>，这样在一些截图工具截图之后，所截图片一般会被复制到剪切板，再使用刚刚的快捷上传快捷键，就会把剪切板中的图片上传到你之前设置的图床中，并把上传得到的图片链接重新复制到剪切板，然后你只要直接在 Markdown 中 <code>control + v</code> 即可，惬意 🤪。</p><p>通过 <a href="https://molunerfinn.com/PicGo/" rel="nofollow noopener noreferrer">PicGo</a> 官网可以免费下载，也可以从 Github 的 <a href="https://github.com/Molunerfinn/PicGo/releases" rel="nofollow noopener noreferrer">Release</a> 下载，支持 Windows/Mac 系统。</p><h2 id="4-magnet">4. Magnet</h2><p>有时候你需要并排比较数据，或是要在一个屏幕上排列三个应用的时候，窗口管理工具就显得必不可少了。这里推介一个我用了一下感觉十分好用的窗口管理工具 Magnet，类似的管理工具似乎有好几个，其他的没用不做评价。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf944541f6d2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>把目标窗口进行拖动就可以快速调整窗口位置，比如拖动窗口到边缘，可将窗口大小调整到屏幕的一半。拖动窗口到角落，可将窗口缩小到屏幕的四分之一。将窗口滑动到显示器的底部边缘可创建三等分宽度的窗口。</p><p>Magnet 同时支持键盘快捷方式，可顺利适配所有命令。您可通过菜单栏上的小图标，找到预定设置或创建自己的设置集。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf942b6f1e6e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>也可以使用快捷键的方式，当焦点在某个窗口的时候，可以使用快捷键来快速将窗口进行排列，也可以通过菜单栏上的小图标，来进行自己希望的窗口排列，常用的有居左、居右、最大化、左 2/3、右 2/3，具体如何使用还是看你的个人习惯和屏幕大小了。</p><p>Magnet 在 App Store 上搜索即可获取，买的话也挺便宜的，经常做活动，活动的时候 6 块钱左右就可以到手，当然如果你实在不愿意买，网上也有资源 😅。 </p><p>前面几个软件都是很多工具推介文章推介过的，下面几个有的是我自己偶然发现的，有的是朋友推介的。</p><h2 id="5-text-scanner">5. Text Scanner</h2><p>有时候你手里有一张图片，图片上很多文字，你想把图片上的文字复制到本地，又或者你发现一个网页，但是这个网页是禁止复制的（比如 App Store 上的软件介绍），你可以选择 QQ 上的识图功能，但我这里推介一个更强大的工具 Text Scanner。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf948a8e349e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>在用过五六个识图软件之后，终于选出了识别速度、识别准确率、人机工程最符合我的预期的软件，扫描速度和辨识率都可以称得上惊艳，，除了可以准确识别中文，还可以识别英文、泰语、韩语、俄语、法语等语言，也可以批量识别图片、驾驶证、身份证、表格、证件照等。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbfa33124ceab?imageslim" class="kg-image" alt="16ecbfa33124ceab?imageslim" width="600" height="400" loading="lazy"></figure><p>这款软件也是通过 App Store 来获取，我购买了正版，当然和前面一样，也可以找到资源 🙃，不过这款软件还经常升级，购买的话会省却很多麻烦，推介购买正版。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf953531d9d6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>我的快捷键是 <code>command + shift + 3</code>，跟截图的相对应，仅供参考。</p><h2 id="6-xnip">6. Xnip</h2><p>在试用了五六个截图软件之后，我选择了 Xnip，有了它，QQ截图、微信截图、网页截图都不需要了，可以说是 Mac 上最好用的截图工具，没有之一。</p><ol><li>Xnip 拥有齐全的标注功能，简单易用，可以对截取的图片进行标注，在标注的同时也可以重新调整截图区域的大小；</li><li>滚动截图，允许滚动截取屏幕之外的内容，生成长截图，轻松截取超过一屏的聊天记录、代码、文章等；</li><li>窗口截图功能，截取某一窗口并附带窗口的阴影效果，除此之外，还可以任意组合多个窗口；</li><li>取色器功能，可以获取某一个像素的颜色代码值，也可以精确到像素的选择截图选取；</li><li>多单位切换，使用 pt、px、厘米、英尺四个不同尺寸单位表示截图区域大小，方便测量物体；</li><li>贴图功能，将任何内容贴在屏幕上；</li></ol><p>还有截图高亮工具、马赛克等等功能，在截图的时候在旁边可以选择生成一圈阴影，和 Mac 自带的截图工具一样，甚至更强大，强烈推介。值得一提的是，你可以选择截图的格式 jpg/png，如果你选择 png 格式，那么你的截图阴影部分背景是透明的，十分贴心。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbfa33333f47c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>在 App Store 里就可以搜索到 Xnip，我的快捷键是 <code>Command + Shift + 1</code>，软件是免费的，购买的话可以去除某些功能下的水印，不过免费版的已经足够平常使用了。</p><h2 id="7-switchkey">7. SwitchKey</h2><p>有时候会因为在 QQ、微信上面要拼音输入法，而在 Webstorm、VSCode、Atom、命令行上需要使用英文，在这些应用互相之间切换的时候，输入法的切换就显得琐碎又麻烦。</p><p>SwitchKey 可以根据当前焦点所在应用自动切换指定的输入法，让你无需关注输入法切换这种琐碎的操作。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf95d48ece84?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>确定要为某个软件添加何种输入法很简单，切换好输入法，打开目标软件之后，然后在 Menubar 的软件菜单内点击 Add Current （上图最上方那个按钮）即可将其对应的输入法这项选择配置加入进来，下次打开该软件时就会自动恢复指定输入状态。如果不小心配置错误，选中记录值后按 <code>Delete</code> 键即可，轻松高效。</p><p>你可以在 Github 的 <a href="https://github.com/itsuhane/SwitchKey/releases" rel="nofollow noopener noreferrer">Release</a> 或者<a href="https://jinyu.li/switchkey/" rel="nofollow noopener noreferrer">官网</a>获取下载链接，如果我没记错的话，是免费的 🥳。</p><h2 id="8-pap-er">8. pap.er</h2><p>外观精致设计感十足的 Mac 当然要配上精美的壁纸，<a href="https://paper.meiyuan.in/" rel="nofollow noopener noreferrer">pap.er</a> 就是这样一款可以给你提供精美壁纸的应用，它在 Unsplash 图片站上获取授权的图片，并提供给用户。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf9606e06d95?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>安装好之后，Menubar 上点击 pap.er 的图标，就会弹出壁纸列表，单击「设为桌面」就可以自动下载并使用图片作为壁纸了，在设置中还可以选择每小时或每日自动更换壁纸，也可以在在二级菜单中查看以前下载过的壁纸。</p><p>你可以在<a href="https://paper.meiyuan.in/" rel="nofollow noopener noreferrer">官网</a>上获取安装包，免费下载使用。</p><h2 id="9-findergo">9. FinderGo</h2><p>如果你需要经常使用终端，那么 FinderGo 可以帮助你快速打开终端，并定位到当前文件夹目录下，是日常使用时的一个很实用的小工具。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf96d1138aff?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>具体下载和配置可以百度一下，网上很多教程。</p><p>安装的话你可以到 <a href="https://github.com/onmyway133/FinderGo" rel="nofollow noopener noreferrer">Github</a> 上下载使用，免费的。</p><h2 id="10-gif-brewery-3">10. GIF Brewery 3</h2><p>在使用了多款 GIF 录制工具之后，终于选择了这款功能强大的软件，强烈推介！</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf971f546409?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>GIF Brewery 3 可以在录制好之后，调整动画播放速度、帧率的时长、截取动画的某个部分、在某些帧下增加文字或贴图、增加水印或字幕、重新调整视频尺寸、调整色彩数等功能，可以说，它已经具备一个视频编辑软件大部分的功能了。</p><p>有时候我在录制完一个比较长的 GIF 并生成的时候发现体积太大，这时可以把播放速度调快一点， 色彩数减小一点，再把 GIF 的尺寸调整小一点，经常可以把体积减少一半还多，这也是为什么经常我们看到的 GIF 图的尺寸比较小的原因，因为大了的话直接导致整个 GIF 文件体积变大，加载缓慢。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf9768daea34?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>你可以在 <a href="https://apps.apple.com/cn/app/gif-brewery-3-by-gfycat/id1081413713?mt=12" rel="nofollow noopener noreferrer">App Store</a> 里免费获取 GIF Brewery 3，当然也可以从其他地方获取，唯一的遗憾是不支持中文，不过界面上都是一些常用的单词，相信你可以的 🤣。</p><h2 id="11-typora">11. Typora</h2><p>使用过很多 Markdown 编辑器，最后选择了简洁轻量的 <a href="https://www.typora.io/" rel="nofollow noopener noreferrer">Typora</a>，与主流编辑器一边编辑一边预览的形式不同，Typora 使用将编辑和预览合并到一起的即时渲染方式，目光不需要在编辑区和预览区中来回切换。</p><p>另外 Typora 还支持 Latex 数学公式、<code>[TOC]</code> 动态目录、拖拽图片自动生成本地预览链接、自定义主题、PDF/Word 导出、专注模式等方便的功能，如果你懂一点 CSS，你可以到 \Application\Support\abnerworks.Typora\themes 中直接修改 CSS 主题文件，或者到<a href="http://theme.typora.io/" rel="nofollow noopener noreferrer">官方主题库</a>中下载。</p><p>在下最喜欢 Misty 和 Catfish 这两个主题，并根据自己的喜好在原主题 CSS 的基础上稍加修改（可以直接找我要 CSS 文件😉）。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/12/3/16ecbf97dcfed70a?imageslim" class="kg-image" alt="16ecbf97dcfed70a?imageslim" width="600" height="400" loading="lazy"></figure><p>你可以到 Typora 的<a href="https://www.typora.io/" rel="nofollow noopener noreferrer">官网</a>下载，是免费的。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 一文搞定前端 Jenkins 自动化部署 ]]>
                </title>
                <description>
                    <![CDATA[ 这两天折腾了一下 Jenkins 持续集成，由于公司使用自己搭建的 svn 服务器来进行代码管理，因此这里 Jenkins 是针对 svn 服务器来进行的配置，后面稍微介绍了下针对 GitHub 管理的项目的 Jenkins 配置。 之前项目每次修改之后都需要本地npm run build一次手动发布到服务器上方便测试和产品查看，有了 Jenkins 持续集成之后只要 svn 或者 git 提交之后就会自动打包，很方便，此次记录以备后询。 声明：  1. 后面的项目地址与打包地址都是使用em-mes，自行修改；  2. 另外还有路径等，根据自己情况自行修改。 1. 安装 1.1 安装 Nginx 可以直接去官网 [http://nginx.org/en/download.html]下直接下载，解压缩start nginx就可以使了，常用命令： start nginx  # 启动 nginx -s reload  # 修改配置后重新加载生效 nginx -s ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/front-end-jenkins-automated-deployment/</link>
                <guid isPermaLink="false">5dc22d18ca1efa04e196a3c0</guid>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Thu, 07 Nov 2019 02:34:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1572874235586-dcbb604ac2e9.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>这两天折腾了一下 Jenkins 持续集成，由于公司使用自己搭建的 svn 服务器来进行代码管理，因此这里 Jenkins 是针对 svn 服务器来进行的配置，后面稍微介绍了下针对 GitHub 管理的项目的 Jenkins 配置。</p><p>之前项目每次修改之后都需要本地<code>npm run build</code>一次手动发布到服务器上方便测试和产品查看，有了 Jenkins 持续集成之后只要 svn 或者 git 提交之后就会自动打包，很方便，此次记录以备后询。</p><p>声明：</p><ol><li>后面的项目地址与打包地址都是使用<code>em-mes</code>，自行修改；</li><li>另外还有路径等，根据自己情况自行修改。</li></ol><h2 id="1-">1. 安装</h2><h3 id="1-1-nginx">1.1 安装 <strong>Nginx</strong></h3><p>可以直接去<a href="http://nginx.org/en/download.html" rel="nofollow noopener noreferrer">官网</a>下直接下载，解压缩<code>start nginx</code>就可以使了，常用命令：</p><pre><code>start nginx  # 启动
nginx -s reload  # 修改配置后重新加载生效
nginx -s reopen  # 重新打开日志文件
nginx -t  # 配置文件检测是否正确</code></pre><p>教程网上不少，就不赘述了。</p><h3 id="1-2-jenkins">1.2 安装<strong>Jenkins</strong></h3><p>从官网下载文件安装之后，我这里安装到<code>C:\Jenkins</code>，默认端口 8080，这时候浏览器访问<code>localhost:8080</code>就能访问 Jenkins 首页，这里注意如果不安装到 C 盘根目录<a href="https://stackoverflow.com/questions/44403642/jenkins-plugin-installation-failing" rel="nofollow noopener noreferrer">有些插件安装会出错</a>。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2018/9/11/165c776eb7b3d145?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>这里会让你去某个地方找一个初始密码文件打开并填到下面的密码框里，验证成功之后进入页面，选择<code>Install suggested plugins</code>推介安装的插件。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/11/image-28.png" class="kg-image" alt="image-28" width="723" height="403" loading="lazy"></figure><p>插件都安装完成之后进入用户登录界面，设定用户名、密码及邮箱。</p><p>然后提示 Jenkins is ready！ → &nbsp; Start using Jenkins ~</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/11/image-29.png" class="kg-image" alt="image-29" width="1080" height="578" loading="lazy"></figure><p>注意这里因为要使用node的命令来执行创建后操作，所以还需要安装插件：<code>NodeJS Plugin</code>、<code>Deploy to container</code>、<code>Github</code>、<code>Post build task</code></p><p>这里顺便记录一下启动和关闭Jenkins服务的命令行：</p><pre><code>net start jenkins            // 启动Jenkins服务
net stop jenkins             // 停止Jenkins服务</code></pre><h2 id="2-svn-jenkins-">2. 创建 svn 项目的 Jenkins 任务</h2><h3 id="2-1-">2.1 新建</h3><p>左边栏新建一个任务，输入一个任务名称，这里随便写一个。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/11/image-30.png" class="kg-image" alt="image-30" width="1080" height="591" loading="lazy"></figure><h3 id="2-2-">2.2 配置</h3><h4 id="general">General</h4><p>这里才是重头戏，进入刚刚创建的任务的配置页面的 <strong>General。</strong></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/11/image-31.png" class="kg-image" alt="image-31" width="1080" height="584" loading="lazy"></figure><p>丢弃旧的构建就是检测到新的版本之后把旧版本的构建删除。</p><h4 id="-">源码管理</h4><p>这里采用的是 svn 来管理代码。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/11/image-32.png" class="kg-image" alt="image-32" width="1080" height="537" loading="lazy"></figure><h4 id="--1">构建触发器</h4><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/11/image-33.png" class="kg-image" alt="image-33" width="1080" height="398" loading="lazy"></figure><p>这里的 Poll SCM 表示去检测是否更新构建的频率，<code>*****</code>表示每分钟，<code>H****</code>表示每小时。</p><h4 id="--2">构建</h4><pre><code>cd cd C:\Jenkins\workspace\em-mes
node -v
npm -v
cnpm i
npm run build</code></pre><h4 id="--3">构建后操作</h4><p>安装插件 <code>Post build task</code> 后，可以在 增加构建后操作步骤中选择 <code>Post build task</code> 选项，增加构建后执行的 script，具体可以参考文章：<a href="https://blog.csdn.net/minebk/article/details/73294785" rel="nofollow noopener noreferrer">jenkins 部署 maven 项目构建后部署前执行 shell 脚本</a>。</p><p>我这里的 <code>Log text</code> 是 <code>Build complete</code>。</p><p>Script：</p><pre><code>rmdir /q/s C:\nginx-1.14.0\html\em-mes
xcopy /y/e/i C:\Jenkins\workspace\em-mes\em-mes C:\nginx-1.14.0\html\em-mes</code></pre><p>复制生成好的文件到 Nginx 的目录下，路径自行修改。</p><h2 id="3-github-jenkins-">3. 创建 GitHub 项目的 Jenkins 任务</h2><p>Jenkins 不仅可以持续集成 svn 项目，Git 项目也是可以的，这里以 GitHub 上的项目为例。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/11/image-34.png" class="kg-image" alt="image-34" width="1036" height="1236" loading="lazy"></figure><p>其他配置和上面一章一样。</p><p>这样如果 GitHub 有新的 push 请求，都会自动化部署到之前的服务器上，可以说很方便了。</p><h2 id="--4">试一试</h2><p>配置好了我们试一试吧，在刚刚 GitHub 项目中随便 commit 一版到 GitHub。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/11/image-35.png" class="kg-image" alt="image-35" width="628" height="670" loading="lazy"></figure><p>稍等片刻去本地 Jenkins 地址<code>http://localhost:8080/job/vue-element-template/</code>就能看到 Jenkins 已经在构建中了。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2018/9/11/165c779f95180b88?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>50 秒之后</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2018/9/11/165c77a28eeebf0f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>构建成功！构建用时 54 秒，现在访问本地地址<code>http://localhost:8282/vue-element-template</code>，已经能看到编译后的发布版本啦~如果你希望发布的是测试版本，可以自行修改构建后操作的 script。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ CSS 是如何影响浏览器元素在文档中的排列 ]]>
                </title>
                <description>
                    <![CDATA[ 最近在项目中遇到了一个问题，menu-bar 希望始终显示在最上面，而在之后的元素都显示在它之下，当时设置了 z-index 也没有效果，不知道什么原因，因此找了一下 CSS 有关层叠方面的资料，解决了这个问题，这里记录一下。 屏幕是一个二维平面，然而 HTML 元素却是排列在三维坐标系中，x 为水平位置，y 为垂直位置，z 为屏幕由内向外方向的位置，我们在看屏幕的时候是沿着 z 轴方向从外向内的；由此，元素在用户视角就形成了层叠的关系，某个元素可能覆盖了其他元素也可能被其他元素覆盖； 那么这里有几个重要的概念：层叠上下文 (堆叠上下文，Stacking Context)、层叠等级 (层叠水平，Stacking Level)、层叠顺序  (层叠次序, 堆叠顺序，Stacking Order)、z-index 声明：  1. 以下定位元素指的是position: absolute|fixed|relative|sticky  2. 以下非定位元素指的是position: initial|static  3. 关于层叠上下文还有一个类似的概念：块级格式化上下文（BFC，Block  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/elements-stacking-with-css/</link>
                <guid isPermaLink="false">5dba4f64ca1efa04e196a242</guid>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ CSS3 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Thu, 31 Oct 2019 03:28:23 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1556742044-3c52d6e88c62.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近在项目中遇到了一个问题，menu-bar 希望始终显示在最上面，而在之后的元素都显示在它之下，当时设置了 z-index 也没有效果，不知道什么原因，因此找了一下 CSS 有关层叠方面的资料，解决了这个问题，这里记录一下。</p><p>屏幕是一个二维平面，然而 HTML 元素却是排列在三维坐标系中，x 为水平位置，y 为垂直位置，z 为屏幕由内向外方向的位置，我们在看屏幕的时候是沿着 z 轴方向从外向内的；由此，元素在用户视角就形成了层叠的关系，某个元素可能覆盖了其他元素也可能被其他元素覆盖；</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2018/9/21/165fc5323852bd61?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>那么这里有几个重要的概念：<strong>层叠上下文</strong> (堆叠上下文，Stacking Context)、<strong>层叠等级</strong> (层叠水平，Stacking Level)、<strong>层叠顺序</strong> (层叠次序, 堆叠顺序，Stacking Order)、<strong>z-index</strong></p><p>声明：</p><ol><li>以下定位元素指的是<code>position: absolute|fixed|relative|sticky</code></li><li>以下非定位元素指的是<code>position: initial|static</code></li><li>关于层叠上下文还有一个类似的概念：<strong>块级格式化上下文（</strong>BFC，Block Formatting Context)，可以参考一下 <a href="https://juejin.im/post/5b51ee276fb9a04f86062cea" rel="">CSS 中重要的 BFC</a>，其中还介绍了一些文档流的内容</li><li>本文蛮长的，但是如果你有勇气看完，那应该对层叠有关概念就基本掌握了</li></ol><h2 id="1-stacking-context-">1. 层叠上下文（Stacking Context）</h2><p><strong>层叠上下文（</strong>堆叠上下文，Stacking Context），是 HTML 中一个三维的概念。在 CSS2.1 规范中，每个元素的位置是三维的，当元素发生层叠，这时它可能覆盖了其他元素或者被其他元素覆盖；排在 z 轴越靠上的位置，距离屏幕观察者越近。</p><p>文章<a href="https://webdesign.tutsplus.com/zh-hans/articles/what-you-may-not-know-about-the-z-index-property--webdesign-16892" rel="nofollow noopener noreferrer">&lt;关于 z-index 那些你不知道的事&gt;</a>有一个很好的比喻，这里引用一下；</p><p>可以想象一张桌子，上面有一堆物品，这张桌子就代表着一个层叠上下文。 如果在第一张桌子旁还有第二张桌子，那第二张桌子就代表着另一个层叠上下文。</p><p>现在想象在第一张桌子上有四个小方块，他们都直接放在桌子上。 在这四个小方块之上有一片玻璃，而在玻璃片上有一盘水果。 这些方块、玻璃片、水果盘各自都代表着层叠上下文中一个不同的层叠层，而这个层叠上下文就是桌子。</p><p>每一个网页都有一个默认的层叠上下文。 这个层叠上下文（桌子）的根源就是<code>&lt;html&gt;&lt;/html&gt;</code>。 html 标签中的一切都被置于这个默认的层叠上下文的一个层叠层上（物品放在桌子上）。</p><p>当你给一个定位元素赋予了除 <code>auto</code> 外的 z-index 值时，你就创建了一个新的层叠上下文，其中有着独立于页面上其他层叠上下文和层叠层的层叠层， 这就相当于你把另一张桌子带到了房间里。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-20.png" class="kg-image" alt="image-20" width="600" height="296" loading="lazy"></figure><p>层叠上下文 1（Stacking Context 1）是由文档根元素形成的， 层叠上下文 2 和 3 &nbsp;（Stacking Context 2, 3）都是层叠上下文 1（Stacking Context 1）上的层叠层。 他们各自也都形成了新的层叠上下文，其中包含着新的层叠层。</p><p>在层叠上下文中，其子元素按照上面解释的规则进行层叠。形成层叠上下文的<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context" rel="nofollow noopener noreferrer">方法</a>有：</p><ul><li>根元素<code>&lt;html&gt;&lt;/html&gt;</code></li><li><code>position</code>值为<code>absolute | relative</code>，且<code>z-index</code>值不为 <code>auto</code></li><li><code>position</code> 值为 <code>fixed | sticky</code></li><li><code>z-index</code> 值不为 <code>auto</code> 的 flex 元素，即：父元素<code>display: flex | inline-flex</code></li><li><code>opacity</code> 属性值小于 <code>1</code> 的元素</li><li><code>transform</code> 属性值不为 <code>none</code>的元素</li><li><code>mix-blend-mode</code> 属性值不为 <code>normal</code> 的元素</li><li><code>filter</code>、<code>perspective</code>、<code>clip-path</code>、<code>mask</code>、<code>mask-image</code>、<code>mask-border</code>、<code>motion-path</code> 值不为 <code>none</code> 的元素</li><li><code>perspective</code> 值不为 <code>none</code> 的元素</li><li><code>isolation</code> 属性被设置为 <code>isolate</code> 的元素</li><li><code>will-change</code> 中指定了任意 CSS 属性，即便你没有直接指定这些属性的值</li><li><code>-webkit-overflow-scrolling</code> 属性被设置 <code>touch</code>的元素</li></ul><p>总结:</p><ol><li>层叠上下文可以包含在其他层叠上下文中，并且一起组建了一个有层级的层叠上下文</li><li>每个层叠上下文完全独立于它的兄弟元素，当处理层叠时只考虑子元素，这里类似于 <a href="https://segmentfault.com/a/1190000013023485" rel="nofollow noopener noreferrer">BFC</a></li><li>每个层叠上下文是自包含的：当元素的内容发生层叠后，整个该元素将会<strong>在父级叠上下文</strong>中按顺序进行层叠</li></ol><h2 id="2-stacking-level-">2. 层叠等级（Stacking Level）</h2><p><strong>层叠等级（</strong>层叠水平，Stacking Level）决定了同一个层叠上下文中元素在 z 轴上的显示顺序的<strong>概念</strong>；</p><ul><li>普通元素的层叠等级优先由其所在的层叠上下文决定</li><li>层叠等级的比较只有在同一个层叠上下文元素中才有意义</li><li>在同一个层叠上下文中，层叠等级描述定义的是该层叠上下文中的元素在 z 轴上的上下顺序</li></ul><p>注意：层叠等级并不一定由 z-index 决定，只有定位元素的层叠等级才由 z-index 决定，其他类型元素的层叠等级由层叠顺序、他们在 HTML 中出现的顺序、他们的父级以上元素的层叠等级一同决定，详细的规则见下面层叠顺序的介绍。</p><h2 id="3-z-index">3. z-index</h2><p>在 CSS2.1 中，所有的盒模型元素都处于三维坐标系中。 除了我们常用的横坐标和纵坐标，盒模型元素还可以沿着"z 轴"层叠摆放，当他们相互覆盖时，z 轴顺序就变得十分重要。</p><p>-- <a href="http://www.w3.org/TR/CSS21/visuren.html#z-index" rel="nofollow noopener noreferrer">CSS 2.1 Section 9.9.1 - Layered presentation</a></p><p>z-index 只适用于定位的元素，对非定位元素无效，它可以被设置为正整数、负整数、0、auto，如果一个定位元素没有设置 z-index，那么默认为auto；</p><p>元素的 z-index 值只在同一个层叠上下文中有意义。如果父级层叠上下文的层叠等级低于另一个层叠上下文的，那么它 z-index 设的再高也没用。所以如果你遇到 z-index 值设了很大，但是不起作用的话，就去看看它的父级层叠上下文是否被其他层叠上下文盖住了。</p><h2 id="4-stacking-order-">4. 层叠顺序（Stacking Order）</h2><p><strong>层叠顺序</strong>（层叠次序, 堆叠顺序，Stacking Order）描述的是元素在同一个层叠上下文中的顺序<strong>规则</strong>，从层叠的底部开始，共有七种层叠顺序：</p><ol><li><strong>背景和边框</strong>：形成层叠上下文的元素的背景和边框</li><li><strong>负 z-index 值</strong>：层叠上下文内有着负 z-index 值的定位子元素，负的越大层叠等级越低</li><li><strong>块级盒</strong>：文档流中块级、非定位子元素</li><li><strong>浮动盒</strong>：非定位浮动元素</li><li><strong>行内盒</strong>：文档流中行内、非定位子元素</li><li><strong>z-index: 0</strong>：z-index为 0 或 auto 的定位元素， 这些元素形成了新的层叠上下文</li><li><strong>正 z-index 值</strong>：z-index 为正的定位元素，正的越大层叠等级越高</li></ol><p>同一个层叠顺序的元素按照在 HTML 里出现的顺序层叠；第 7 级顺序的元素会显示在之前顺序元素的上方，也就是看起来覆盖了更低级的元素。</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2018/9/21/165fc538e321d802?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><h2 id="5-">5. 实战</h2><h3 id="5-1-">5.1 普通情况</h3><p>三个<code>relative</code>定位的<code>div</code>块中各有<code>absolute</code>的不同颜色的<code>span.red</code>、<code>span.green</code>、<code>span.blue</code>，它们都设置了<code>position: absolute</code>；</p><p><a href="https://codepen.io/SHERlocked93/pen/aaPord" rel="nofollow noopener noreferrer">参见 Codepen - 普通情况</a></p><p>那么当没有元素包含 z-index 属性时，这个例子中的元素按照如下顺序层叠（从底到顶顺序）：</p><ol><li>根元素的背景和边界</li><li>块级非定位元素按 HTML 中的出现顺序层叠</li><li>行内非定位元素按 HTML 中的出现顺序层叠</li><li>定位元素按 HTML 中的出现顺序层叠</li></ol><p>红绿蓝都属于 z-index 为 auto 的定位元素，因此按照 7 层层叠顺序规则来说同属于层叠顺序第 6 级，所以按 HTML 中的出现顺序层叠：<code>红-&gt;绿-&gt;蓝</code>。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-21.png" class="kg-image" alt="image-21" width="263" height="254" loading="lazy"></figure><h3 id="5-2-">5.2 在相同层叠上下文的父元素内的情况</h3><p>红绿位于一个<code>div.first-box</code>下，蓝位于<code>div.second-box</code>下，红绿蓝都设置了<code>position: absolute</code>，<code>first-box</code>与<code>second-box</code>都设置了<code>position: relative</code>；</p><p><a href="https://codepen.io/SHERlocked93/pen/RYENBw" rel="nofollow noopener noreferrer">参见 Codepen - 父元素不同但都位于根元素下</a>。</p><p>这个例子中，红蓝绿元素的父元素<code>first-box</code>与<code>second-box</code>都没有生成新的层叠上下文，都属于根层叠上下文中的元素，且都是层叠顺序第 6 级，所以按 HTML 中的出现顺序层叠：<code>红-&gt;绿-&gt;蓝</code>。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-22.png" class="kg-image" alt="image-22" width="254" height="255" loading="lazy"></figure><h3 id="5-3-z-index">5.3 给子元素增加 z-index</h3><p>红绿位于一个<code>div.first-box</code>下，蓝黄位于<code>div.second-box</code>下，红绿蓝都设置了<code>position: absolute</code>，如果这时给绿加一个属性<code>z-index: 1</code>，那么此时<code>.green</code>位于最上面。</p><p>如果再在<code>.second-box</code>下<code>.green</code>后加一个绝对定位的 <code>span.gold</code>，设置<code>z-index: -1</code>，那么它将位于红绿蓝的下面。</p><p><a href="https://codepen.io/SHERlocked93/pen/gdZOrK" rel="nofollow noopener noreferrer">参见 Codepen - 设置了 z-index</a></p><p>这个例子中，红蓝绿黄元素的父元素中都没有生成新的层叠上下文，都属于根层叠上下文中的元素</p><ol><li>红蓝都没有设置 z-index，同属于层叠顺序中的第 6 级，按 HTML 中的出现顺序层叠</li><li>绿设置了正的 z-index，属于第 7 级</li><li>黄设置了负的 z-index，属于第 2 级</li></ol><p>所以这个例子中的从底到高显示的顺序就是：<code>黄-&gt;红-&gt;蓝-&gt;绿</code>。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-23.png" class="kg-image" alt="image-23" width="249" height="250" loading="lazy"></figure><h3 id="5-4-">5.4 在不同层叠上下文的父元素内的情况</h3><p>红绿位于一个<code>div.first-box</code>下，蓝位于<code>div.second-box</code>下，红绿蓝都设置了<code>position: absolute</code>，如果<code>first-box</code>的z-index设置的比<code>second-box</code>的大，那么此时无论蓝的 z-index 设置的多大<code>z-index: 999</code>，蓝都位于红绿的下面；如果我们只更改红绿的 z-index 值，由于这两个元素都在父元素<code>first-box</code>产生的层叠上下文中，此时谁的 z-index 值大，谁在上面。</p><p><a href="https://codepen.io/SHERlocked93/pen/gdZbOJ" rel="nofollow noopener noreferrer">参见 Codepen - 不同层叠上下文的父元素</a>。</p><p>这个例子中，红绿蓝都属于设置了 z-index 的定位元素，不过他们的父元素创建了新的层叠上下文。</p><ol><li>红绿的父元素<code>first-box</code>是设置了正 z-index 的定位元素，因此创建了一个层叠上下文，属于层叠顺序中的第 7 级</li><li>蓝的父元素<code>second-box</code>也同样创建了一个层叠上下文，属于层叠顺序中的第6级</li><li>按照层叠顺序，<code>first-box</code>中所有元素都排在<code>second-box</code>上</li><li>红绿都属于层叠上下文<code>first-box</code>中且设置了不同的正 z-index，都属于层叠顺序中第 7 级</li><li>蓝属于层叠上下文<code>second-box</code>，且设置了一个很大的正 z-index，属于层叠元素中第 7 级；</li><li>虽然蓝的 z-index 很大，但是因为<code>second-box</code>的层叠等级比<code>first-box</code>小，因此位于红绿之下；</li></ol><p>所以这个例子中从低到到显示的顺序：<code>蓝-&gt;红-&gt;绿</code></p><p>(我遇到的的情况就类似这样。)</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-24.png" class="kg-image" alt="image-24" width="251" height="253" loading="lazy"></figure><h3 id="5-5-opacity">5.5 给子元素设置 opacity</h3><p>红绿位于<code>div.first-box</code>下，蓝位于<code>div.second-box</code>下，红绿蓝都设置了<code>position: absolute</code>，绿设置了<code>z-index: 1</code>，那么此时绿位于红蓝的最上面；</p><p>如果此时给<code>first-box</code>设置<code>opacity: .99</code>，这时无论红绿的 z-index 设置的多大<code>z-index: 999</code>，蓝都位于红绿的上面；</p><p>如果再在<code>.second-box</code>下<code>.green</code>后加一个<code>span.gold</code>，设置<code>z-index: -1</code>，那么它将位于红绿蓝的下面；</p><p><a href="https://codepen.io/SHERlocked93/pen/GXPRWB" rel="nofollow noopener noreferrer">参见 Codepen - opacity 的影响</a></p><p>之前已经介绍了，设置<code>opacity</code>也可以形成层叠上下文，因此：</p><ol><li><code>first-box</code>设置了<code>opacity</code>，<code>first-box</code>成为了一个新的层叠上下文</li><li><code>second-box</code>没有形成新的层叠上下文，因此其中的元素都属于根层叠上下文</li><li>黄属于层叠顺序中第 2 级，红绿属于第 7 级，<code>first-box</code>属于第 6 级，蓝属于层叠顺序中第6级且按HTML出现顺序位于<code>first-box</code>之上</li></ol><p>所以这个例子中从低到到显示的顺序：<code>黄-&gt;红-&gt;绿-&gt;蓝</code></p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/7/31/16c46d625f98c04f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Vue 强烈推介的实用技巧 ]]>
                </title>
                <description>
                    <![CDATA[ 在 Vue 的使用过程中会遇到各种场景，当普通使用时觉得没什么，但是或许优化一下可以更高效更优美的进行开发。 1. 多图表resize事件去中心化 1.1 一般情况 有时候我们会遇到这样的场景，一个组件中有几个图表，在浏览器resize的时候我们希望图表也进行resize，因此我们会在父容器组件中写： mounted() {   setTimeout(() => window.onresize = () => {     this.$refs.chart1.chartWrapperDom.resize()     this.$refs.chart2.chartWrapperDom.resize()     // ...    }, 200) destroyed() { window.onresize = null } 这样子图表组件如果跟父容器组件不在一个页面，子组件的状态就被放到父组件进行管理，为了维护方便，我们自然希望子组件的事件和状态由自己来维护，这样在添加删除组件的时候就不需要去父组件挨个修改 1.2 优化 这里使用了 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/vue-skills-i-will-recommend/</link>
                <guid isPermaLink="false">5daea82bca1efa04e196a19b</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ VueJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Tue, 22 Oct 2019 07:09:27 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1571658734974-e513dfb8b86b.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在 Vue 的使用过程中会遇到各种场景，当普通使用时觉得没什么，但是或许优化一下可以更高效更优美的进行开发。</p><h2 id="1-resize-">1. 多图表resize事件去中心化</h2><h3 id="1-1-">1.1 一般情况</h3><p>有时候我们会遇到这样的场景，一个组件中有几个图表，在浏览器resize的时候我们希望图表也进行resize，因此我们会在父容器组件中写：</p><pre><code>mounted() {
  setTimeout(() =&gt; window.onresize = () =&gt; {
    this.$refs.chart1.chartWrapperDom.resize()
    this.$refs.chart2.chartWrapperDom.resize()
    // ... 
  }, 200)
destroyed() { window.onresize = null }</code></pre><p>这样子图表组件如果跟父容器组件不在一个页面，子组件的状态就被放到父组件进行管理，为了维护方便，我们自然希望子组件的事件和状态由自己来维护，这样在添加删除组件的时候就不需要去父组件挨个修改</p><h3 id="1-2-">1.2 优化</h3><p>这里使用了 <a href="https://lodash.com/" rel="nofollow noopener noreferrer">lodash</a> 的节流 throttle 函数，也可以自己实现，这篇<a href="https://segmentfault.com/a/1190000014292298" rel="nofollow noopener noreferrer">文章</a>也有节流的实现可以参考一下。 以 Echarts 为例，在每个图表组件中：</p><pre><code>computed: {
  /**
   * 图表DOM
   */
  chartWrapperDom() {
    const dom = document.getElementById('consume-analy-chart-wrapper')
    return dom &amp;&amp; Echarts.init(dom)
  },
  /**
   * 图表resize节流，这里使用了lodash，也可以自己使用setTimout实现节流
   */
  chartResize() {
    return _.throttle(() =&gt; this.chartWrapperDom &amp;&amp; this.chartWrapperDom.resize(), 400)
  }
},
mounted() {
  window.addEventListener('resize', this.chartResize)
},
destroyed() {
  window.removeEventListener('resize', this.chartResize)
}</code></pre><h3 id="1-3-">1.3 再次优化</h3><p>感谢 @JserWang 的提醒，这里因为多个 chart 实例都使用同一套初始化逻辑，可以使用 extends 来考虑复用，因此我想到了 Vue 提供的 <a href="https://cn.vuejs.org/v2/guide/mixins.html#%E5%9F%BA%E7%A1%80" rel="nofollow noopener noreferrer">Mixins</a>，所以我在这里做了点优化，可以让每个同类型的 chart 组件更优雅一点： 新建一个 mixin.js 文件：</p><pre><code>import Echarts from 'echarts'
import _ from 'lodash'

export default {
  computed: {
    /* 图表DOM */
    $_chartMixin_chartWrapperDom() {
      const dom = document.getElementById(this.thisDomId)
      return dom &amp;&amp; Echarts.init(dom)
    },
    
    /** 图表resize节流，这里使用了lodash，也可以自己使用setTimout实现节流 */
    $_chartMixin_chartResize() {
      return _.throttle(() =&gt; this.$_chartMixin_chartWrapperDom.resize(), 400)
    }
  },
  
  methods: {
    /* 图表初始化 */
    $_chartMixin_initChart() {
      this.$_chartMixin_chartWrapperDom.setOption({ /* options */ })
  },
  
  mounted() {
    this.$_chartMixin_initChart()
    window.addEventListener('resize', this.$_chartMixin_chartResize)
  },
  
  destroyed() {
    window.removeEventListener('resize', this.$_chartMixin_chartResize)
  }
}</code></pre><p>然后在每个 chart 组件中：</p><pre><code>&lt;script type='text/javascript'&gt;
import ChartMixin from './mixin'
export default {
  mixins: [ChartMixin],
  data() {
    return {
      thisDomId: 'consume-analy-chart-wrapper'
    }
  }
}
&lt;/script&gt;</code></pre><p>这样就可以在每个图表组件中混入之前在 <code>mixin.js</code> 中定义的 resize 事件逻辑，且自动初始化，并在 destroyed 的时候自动销毁事件~</p><p>当然可以进一步优化一下，比如一个页面有多个图表的话，上面的实现就力有不逮了，这里需要重构一下，具体代码可以参照 <a href="https://github.com/panda-fe/panda-vue/blob/master/mixins/chartInitMixin.js" rel="nofollow noopener noreferrer">chartInitMixin - Github</a> 的实现~</p><h2 id="2-">2. 全局过滤器注册</h2><h3 id="2-1-">2.1 一般情况</h3><p>官方注册过滤器的方式：</p><pre><code>export default {
  data () { return {} },
  filters:{
    orderBy (){
      // doSomething
    },
    uppercase () {
      // doSomething
    }
  }
}</code></pre><p>但是我们做项目来说，大部分的过滤器是要全局使用的，不会每每用到就在组件里面去写，抽成全局的会更好些。 <a href="https://cn.vuejs.org/v2/api/#filters" rel="nofollow noopener noreferrer">官方</a>注册全局的方式：</p><pre><code>// 注册
Vue.filter('my-filter', function (value) {
  // 返回处理后的值
})
// getter，返回已注册的过滤器
var myFilter = Vue.filter('my-filter')</code></pre><p>但是分散写的话不美观，因此可以抽出成单独文件。</p><h3 id="2-2-">2.2 优化</h3><p>我们可以抽出到独立文件，然后使用 Object.keys 在 main.js 入口统一注册。</p><p>/src/common/filters.js</p><pre><code>let dateServer = value =&gt; value.replace(/(\d{4})(\d{2})(\d{2})/g, '$1-$2-$3') 

export { dateServer }</code></pre><p>/src/main.js</p><pre><code>import * as custom from './common/filters/custom'
Object.keys(custom).forEach(key =&gt; Vue.filter(key, custom[key]))</code></pre><p>然后在其他的 .vue 文件中就可愉快地使用这些我们定义好的全局过滤器了。</p><pre><code>&lt;template&gt;
  &lt;section class="content"&gt;
    &lt;p&gt;{{ time | dateServer }}&lt;/p&gt; &lt;!-- 2016-01-01 --&gt;
  &lt;/section&gt;
&lt;/template&gt;
&lt;script&gt;
  export default {
    data () {
      return {
        time: 20160101
      }
    }
  }
&lt;/script&gt;</code></pre><h2 id="3-">3. 全局组件注册</h2><h3 id="3-1-">3.1 一般情况</h3><p>需要使用组件的场景：</p><pre><code>&lt;template&gt;
    &lt;BaseInput  v-model="searchText"  @keydown.enter="search"/&gt;
    &lt;BaseButton @click="search"&gt;
        &lt;BaseIcon name="search"/&gt;
    &lt;/BaseButton&gt;
&lt;/template&gt;
&lt;script&gt;
    import BaseButton from './baseButton'
    import BaseIcon from './baseIcon'
    import BaseInput from './baseInput'
    export default {
      components: { BaseButton, BaseIcon, BaseInput }
    }
&lt;/script&gt;</code></pre><p>我们写了一堆基础 UI 组件，然后每次我们需要使用这些组件的时候，都得先 import，然后声明 components，很繁琐，这里可以使用统一注册的形式。</p><h3 id="3-2-">3.2 优化</h3><p>我们需要借助一下神器 webpack，使用 <a href="https://doc.webpack-china.org/guides/dependency-management/#require-context" rel="nofollow noopener noreferrer"><code>require.context()</code></a> 方法来创建自己的<strong>模块</strong>上下文，从而实现自动动态 require 组件。这个方法需要 3 个参数：要搜索的文件夹目录、是否还应该搜索它的子目录、以及一个匹配文件的正则表达式。 我们在 components 文件夹添加一个叫 componentRegister.js 的文件，在这个文件里借助 webpack 动态将需要的基础组件统统打包进来。</p><p>/src/components/componentRegister.js</p><pre><code>import Vue from 'vue'

/**
 * 首字母大写
 * @param str 字符串
 * @example heheHaha
 * @return {string} HeheHaha
 */
function capitalizeFirstLetter(str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

/**
 * 对符合'xx/xx.vue'组件格式的组件取组件名
 * @param str fileName
 * @example abc/bcd/def/basicTable.vue
 * @return {string} BasicTable
 */
function validateFileName(str) {
  return /^\S+\.vue$/.test(str) &amp;&amp;
    str.replace(/^\S+\/(\w+)\.vue$/, (rs, $1) =&gt; capitalizeFirstLetter($1))
}

const requireComponent = require.context('./', true, /\.vue$/)

// 找到组件文件夹下以.vue命名的文件，如果文件名为index，那么取组件中的name作为注册的组件名
requireComponent.keys().forEach(filePath =&gt; {
  const componentConfig = requireComponent(filePath)
  const fileName = validateFileName(filePath)
  const componentName = fileName.toLowerCase() === 'index'
    ? capitalizeFirstLetter(componentConfig.default.name)
    : fileName
  Vue.component(componentName, componentConfig.default || componentConfig)
})</code></pre><p>这里文件夹结构：</p><pre><code>components
│ componentRegister.js
├─BasicTable
│ BasicTable.vue
├─MultiCondition
│ index.vue</code></pre><p>这里对组件名做了判断，如果是 index 的话就取组件中的 name 属性处理后作为注册组件名，所以最后注册的组件为：<code>multi-condition</code>、<code>basic-table</code>最后我们在 main.js 中 import 'components/componentRegister.js'，然后我们就可以随时随地使用这些基础组件，无需手动引入了。</p><h2 id="4-">4. 不同路由的组件复用</h2><h3 id="4-1-">4.1 场景还原</h3><p>当某个场景中 vue-router 从 /post-page/a，跳转到 /post-page/b。然后我们惊人的发现，页面跳转后数据竟然没更新？！原因是 vue-router "智能地"发现这是同一个组件，然后它就决定要复用这个组件，所以你在 created 函数里写的方法压根就没执行。通常的解决方案是监听 $route 的变化来初始化数据，如下：</p><pre><code>data() {
  return {
    loading: false,
    error: null,
    post: null
  }
},
watch: {
  '$route': {        // 使用watch来监控是否是同一个路由
    handler: 'resetData',
    immediate: true
  }
},
methods: {
  resetData() {
    this.loading = false
    this.error = null
    this.post = null
    this.getPost(this.$route.params.id)
  },
  getPost(id){ }
}</code></pre><h3 id="4-2-">4.2 优化</h3><p>为了实现这样的效果可以给<code>router-view</code>添加一个不同的 key，这样即使是公用组件，只要 url 变化了，就一定会重新创建这个组件。</p><pre><code>&lt;router-view :key="$route.fullpath"&gt;&lt;/router-view&gt;</code></pre><p>还可以在其后加<code>+ +new Date()</code>时间戳，保证独一无二。</p><p>感谢网友 @rolitter 的提醒，如果组件被放在<code>&lt;keep-alive&gt;</code>中的话，可以把获取新数据的方法放在 activated 钩子，代替原来在 created、mounted 钩子中获取数据的任务。</p><h2 id="5-">5. 组件事件属性穿透</h2><h3 id="5-1-">5.1 一般情况</h3><pre><code>// 父组件
&lt;BaseInput :value="value"
           label="密码"
           placeholder="请填写密码"
           @input="handleInput"
           @focus="handleFocus"&gt;
&lt;/BaseInput&gt;

// 子组件
&lt;template&gt;
  &lt;label&gt;
    {{ label }}
    &lt;input :value=" value"
           :placeholder="placeholder"
           @focus="$emit('focus', $event)"
           @input="$emit('input', $event.target.value)"&gt;
  &lt;/label&gt;
&lt;/template&gt;</code></pre><h3 id="5-2-">5.2 优化</h3><p>vue 的组件实例中的<code>$props</code>、<code>$attrs</code>给我们提供了很大的便利，特别是父子组件传值的时候。 1、 每一个从父组件传到子组件的 props，我们都得在子组件的 Props 中显式的声明才能使用。这样一来，我们的子组件每次都需要申明一大堆 props，这里我们知道 <a href="https://cn.vuejs.org/v2/api/index.html#v-bind" rel="nofollow noopener noreferrer">v-bind 是可以传对象</a>的，可以在 <a href="https://cn.vuejs.org/v2/api/index.html#vm-props" rel="nofollow noopener noreferrer"><code>vm.$props</code></a> 中拿到所有父组件 props 的值 <code>v-bind="$props"</code>。</p><pre><code>&lt;input  v-bind="$props" 
       @input="$emit('input', $event.target.value)"&gt;</code></pre><p>2、 类似 placeholer 这种 dom 原生的 property 可以使用<a href="https://cn.vuejs.org/v2/api/#vm-attrs" rel="nofollow noopener noreferrer"><code>$attrs</code></a>直接从父传到子，无需声明。方法如下：</p><pre><code>&lt;input :value="value"
       v-bind="$attrs"
       @input="$emit('input', $event.target.value)"&gt;</code></pre><p><code>$attrs</code>包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时，这里会包含所有父作用域的绑定，并且可以通过 <code>v-bind="$attrs"</code> 传入内部组件。</p><p>3、 注意到子组件的<code>@focus="$emit('focus', $event)"</code>其实什么都没做，只是把event传回给父组件而已，那其实和上面类似，完全没必要显式地申明：</p><pre><code>&lt;input :value="value"
       v-bind="$attrs"
       v-on="listeners"/&gt;

computed: {
  listeners() {
    return {
      ...this.$listeners,
      input: event =&gt;
        this.$emit('input', event.target.value)
    }
  }
}</code></pre><p><a href="https://cn.vuejs.org/v2/api/#vm-listeners" rel="nofollow noopener noreferrer"><code>$listeners</code></a>包含了父作用域中的 (不含 .native 修饰器的) <a href="https://cn.vuejs.org/v2/api/#v-on" rel="nofollow noopener noreferrer">v-on</a> 事件监听器。它可以通过 <code>v-on="$listeners"</code> 传入内部组件——在创建更高层次的组件时非常有用。</p><p>需要注意的是，由于我们 input 并不是 BaseInput 这个组件的根节点，而默认情况下父作用域的不被认作 <code>props</code> 的特性绑定将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。所以我们需要设置 <a href="https://cn.vuejs.org/v2/api/#inheritAttrs" rel="nofollow noopener noreferrer"><code>inheritAttrs: false</code></a>，这些默认行为将会被去掉，上面优化才能成功。</p><h2 id="6-">6. 路由根据开发状态懒加载</h2><h3 id="6-1-">6.1 一般情况</h3><p>一般我们在路由中加载组件的时候：</p><pre><code>import Login from '@/views/login.vue'

export default new Router({
  routes: [{ path: '/login', name: '登陆', component: Login}]
})</code></pre><p>当你需要懒加载 lazy-loading 的时候，需要一个个把 routes 的 component 改为<code>() =&gt; import('@/views/login.vue')</code>，甚为麻烦。</p><p>当你的项目页面越来越多之后，在开发环境之中使用 lazy-loading 会变得不太合适，每次更改代码触发热更新都会变得非常的慢。所以建议只在生成环境之中使用路由懒加载功能。</p><h3 id="6-2-">6.2 优化</h3><p>根据 Vue 的<a href="https://cn.vuejs.org/v2/guide/components.html#%E5%BC%82%E6%AD%A5%E7%BB%84%E4%BB%B6" rel="nofollow noopener noreferrer">异步组件</a>和 Webpack 的<a href="https://doc.webpack-china.org/guides/code-splitting" rel="nofollow noopener noreferrer">代码分割功能</a>可以轻松实现组件的懒加载，如：</p><pre><code>const Foo = () =&gt; import('./Foo.vue')</code></pre><p>在区分开发环境与生产环境时，可以在路由文件夹下分别新建两个文件：</p><p><code>_import_production.js</code></p><pre><code>module.exports = file =&gt; () =&gt; import('@/views/' + file + '.vue')</code></pre><p><code>_import_development.js</code> (这种写法<code>vue-loader</code>版本至少 v13.0.0 以上)</p><pre><code>module.exports = file =&gt; require('@/views/' + file + '.vue').default</code></pre><p>而在设置路由的<code>router/index.js</code>文件中：</p><pre><code>const _import = require('./_import_' + process.env.NODE_ENV)

export default new Router({
  routes: [{ path: '/login', name: '登陆', component: _import('login/index') }]
})</code></pre><p>这样组件在开发环境下就是非懒加载，生产环境下就是懒加载的了。</p><h2 id="7-vue-loader-">7 vue-loader 小技巧</h2><p><a href="https://vue-loader.vuejs.org/zh-cn/" rel="nofollow noopener noreferrer">vue-loader</a> 是处理 *.vue 文件的 webpack loader。它本身提供了丰富的 API，有些 API 很实用但很少被人熟知。例如接下来要介绍的 <a href="https://vue-loader.vuejs.org/zh-cn/options.html#preservewhitespace" rel="nofollow noopener noreferrer"><code>preserveWhitespace</code></a> 和 <a href="https://vue-loader.vuejs.org/zh-cn/options.html#transformtorequire" rel="nofollow noopener noreferrer"><code>transformToRequire</code></a>。</p><h3 id="7-1-preservewhitespace-">7.1 用 <code>preserveWhitespace</code> 减少文件体积</h3><p>有些时候我们在写模板时不想让元素和元素之间有空格，可能会写成这样：</p><pre><code>&lt;ul&gt;
  &lt;li&gt;1111&lt;/li&gt;&lt;li&gt;2222&lt;/li&gt;&lt;li&gt;333&lt;/li&gt;
&lt;/ul&gt;</code></pre><p>当然还有其他方式，比如设置字体的<code>font-size: 0</code>，然后给需要的内容单独设置字体大小，目的是为了去掉元素间的空格。其实我们完全可以通过配置 vue-loader 实现这一需求。</p><pre><code>{
  vue: {
    preserveWhitespace: false
  }
}</code></pre><p>它的作用是阻止元素间生成空白内容，在 Vue 模板编译后使用 <code>_v(" ")</code> 表示。如果项目中模板内容多的话，它们还是会占用一些文件体积的。例如 Element 配置该属性后，未压缩情况下文件体积减少了近 30Kb。</p><h3 id="7-2-transformtorequire-">7.2 使用 <code>transformToRequire</code> 再也不用把图片写成变量了</h3><p>以前在写 Vue 的时候经常会写到这样的代码：把图片提前 require 传给一个变量再传给组件。</p><pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;avatar :default-src="DEFAULT_AVATAR"&gt;&lt;/avatar&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
  export default {
    created () {
      this.DEFAULT_AVATAR = require('./assets/default-avatar.png')
    }
  }
&lt;/script&gt;</code></pre><p>其实通过配置 <code>transformToRequire</code> 后，就可以直接配置，这样 vue-loader 会把对应的属性自动 require 之后传给组件。</p><pre><code>{
  vue: {
    transformToRequire: {
      avatar: ['default-src']
    }
  }
}</code></pre><p>于是我们代码就可以简化不少。</p><pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;avatar default-src="./assets/default-avatar.png"&gt;&lt;/avatar&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><p>在 vue-cli 的 webpack 模板下，默认配置是：</p><pre><code>transformToRequire: {
  video: ['src', 'poster'],
  source: 'src',
  img: 'src',
  image: 'xlink:href'
}</code></pre><p>可以举一反三进行一下类似的配置。</p><p>vue-loader 还有很多实用的 API 例如最近加入的<a href="https://vue-loader.vuejs.org/zh-cn/configurations/custom-blocks.html" rel="nofollow noopener noreferrer">自定义块</a>，感兴趣的各位可以去文档里找找看。</p><h2 id="8-render-">8. render 函数</h2><p>在某些场景下你可能需要 <a href="https://cn.vuejs.org/v2/guide/render-function.html" rel="nofollow noopener noreferrer">render 渲染函数</a>带来的完全编程能力来解决不太容易解决的问题，特别是要动态选择生成标签和组件类型的场景。</p><h3 id="8-1-">8.1 动态标签</h3><h4 id="1-">1. 一般情况</h4><p>比如根据 props 来生成标签的场景</p><pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;div v-if="level === 1"&gt; &lt;slot&gt;&lt;/slot&gt; &lt;/div&gt;
    &lt;p v-else-if="level === 2"&gt; &lt;slot&gt;&lt;/slot&gt; &lt;/p&gt;
    &lt;h1 v-else-if="level === 3"&gt; &lt;slot&gt;&lt;/slot&gt; &lt;/h1&gt;
    &lt;h2 v-else-if="level === 4"&gt; &lt;slot&gt;&lt;/slot&gt; &lt;/h2&gt;
    &lt;strong v-else-if="level === 5"&gt; &lt;slot&gt;&lt;/slot&gt; &lt;/stong&gt;
    &lt;textarea v-else-if="level === 6"&gt; &lt;slot&gt;&lt;/slot&gt; &lt;/textarea&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><p>其中 level 是 data 中的变量，可以看到这里有大量重复代码，如果逻辑复杂点，加上一些绑定和判断就更复杂了，这里可以利用 render 函数来对要生成的标签加以判断。</p><h4 id="2--1">2. 优化</h4><p>使用 render 方法根据参数来生成对应标签可以避免上面的情况。</p><pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;child :level="level"&gt;Hello world!&lt;/child&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script type='text/javascript'&gt;
  import Vue from 'vue'
  Vue.component('child', {
    render(h) {
      const tag = ['div', 'p', 'strong', 'h1', 'h2', 'textarea'][this.level]
      return h(tag, this.$slots.default)
    },
    props: {
      level: {  type: Number,  required: true  } 
    }
  })   
  export default {
    name: 'hehe',
    data() { return { level: 3 } }
  }
&lt;/script&gt;</code></pre><p>示例可以查看 <a href="https://codepen.io/SHERlocked93/pen/mLEJPE" rel="nofollow noopener noreferrer">CodePen</a>。</p><h3 id="8-2-">8.2 动态组件</h3><p>当然 render 函数还有很多用法，比如要使用动态组件，除了使用 <code>:is</code> 之外也可以使用 render 函数。</p><pre><code>&lt;template&gt;
  &lt;div&gt;
    &lt;button @click='level = 0'&gt;嘻嘻&lt;/button&gt;
    &lt;button @click='level = 1'&gt;哈哈&lt;/button&gt;
    &lt;hr&gt;
    &lt;child :level="level"&gt;&lt;/child&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script type='text/javascript'&gt;
  import Vue from 'vue'
  import Xixi from './Xixi'
  import Haha from './Haha'
  Vue.component('child', {
    render(h) {
      const tag = ['xixi', 'haha'][this.level]
      return h(tag, this.$slots.default)
    },
    props: { level: { type: Number, required: true } },
    components: { Xixi, Haha }
  })
  export default {
    name: 'hehe',
    data() { return { level: 0 } }
  }
&lt;/script&gt;</code></pre> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ JS 服务器推送技术 WebSocket 入门指北 ]]>
                </title>
                <description>
                    <![CDATA[ 最近在工作中遇到了需要服务器推送消息的场景，这里总结一下收集整理 WebSocket 相关资料的收获。 1. 概述 1.1 服务器推送 WebSocket 作为一种通信协议，属于服务器推送技术 [https://en.wikipedia.org/wiki/Push_technology] 的一种，IE10+ 支持。 服务器推送技术不止一种 [http://www.daimajiayuan.com/sitejs-65893-1.html] ，有短轮询、长轮询、WebSocket、Server-sent Events（SSE）等，它们各有优缺点 [https://mp.weixin.qq.com/s/unagcn33kfCnMrlI-Oslig]： 短轮询最简单，在一些简单的场景也会经常使用，就是隔一段时间就发起一个 ajax 请求。那么长轮询是什么呢？ 长轮询（Long Polling）是在 Ajax 轮询基础上做的一些改进，在没有更新的时候不再返回空响应，而且把连接保持到有更新的时候，客户端向服务器发送 Ajax 请求，服务器接到请求后 hold 住连接，直到有新消息才返回响应 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/introduction-to-websocket/</link>
                <guid isPermaLink="false">5da6e85dca1efa04e196a0fd</guid>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Wed, 16 Oct 2019 10:06:25 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1570900029966-270739c099ee.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近在工作中遇到了需要服务器推送消息的场景，这里总结一下收集整理 WebSocket 相关资料的收获。</p><h2 id="1-">1. 概述</h2><h3 id="1-1-">1.1 服务器推送</h3><p><strong>WebSocket </strong>作为一种通信协议，属于<a href="https://en.wikipedia.org/wiki/Push_technology" rel="nofollow noopener noreferrer">服务器推送技术</a>的一种，IE10+ 支持。</p><p><a href="http://www.daimajiayuan.com/sitejs-65893-1.html" rel="nofollow noopener noreferrer">服务器推送技术不止一种</a>，有短轮询、长轮询、WebSocket、Server-sent Events（SSE）等，它们<a href="https://mp.weixin.qq.com/s/unagcn33kfCnMrlI-Oslig" rel="nofollow noopener noreferrer">各有优缺点</a>：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-11.png" class="kg-image" alt="image-11" width="619" height="558" loading="lazy"></figure><p><strong>短轮询</strong>最简单，在一些简单的场景也会经常使用，就是隔一段时间就发起一个 ajax 请求。那么长轮询是什么呢？</p><p><strong>长轮询（</strong>Long Polling）是在 Ajax 轮询基础上做的一些改进，在没有更新的时候不再返回空响应，而且把连接保持到有更新的时候，客户端向服务器发送 Ajax 请求，服务器接到请求后 hold 住连接，直到有新消息才返回响应信息并关闭连接，客户端处理完响应信息后再向服务器发送新的请求。它是一个解决方案，但不是最佳的技术方案。</p><p>如果说短轮询是客户端不断打电话问服务端有没有消息，服务端回复后立刻挂断，等待下次再打；长轮询是客户端一直打电话，服务端接到电话不挂断，有消息的时候再回复客户端并挂断。</p><p><strong>SSE（</strong>Server-Sent Events）与长轮询机制类似，区别是每个连接不只发送一个消息。客户端发送一个请求，服务端保持这个连接直到有新消息发送回客户端，仍然保持着连接，这样连接就可以支持消息的再次发送，由服务器单向发送给客户端。然而IE直到11都不支持，不多说了......</p><h3 id="1-2-websocket-">1.2 WebSocket 的特点</h3><p>为什么已经有了轮询还要 WebSocket 呢，是因为短轮询和长轮询有个缺陷：通信只能由客户端发起。</p><p>那么如果后端想往前端推送消息需要前端去轮询，不断查询后端是否有新消息，而轮询的效率低且浪费资源（必须不停 setInterval 或 setTimeout 去连接，或者 HTTP 连接始终打开），WebSocket 提供了一个文明优雅的全双工通信方案。一般适合于对数据的实时性要求比较强的场景，如通信、股票、直播、共享桌面，特别适合于客户端与服务频繁交互的情况下，如聊天室、实时共享、多人协作等平台。</p><h4 id="-">特点</h4><ol><li>建立在 TCP 协议之上，服务器端的实现比较容易。</li><li>与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443，并且握手阶段采用 HTTP 协议，因此握手时不容易屏蔽，能通过各种 HTTP 代理服务器。</li><li>数据格式比较轻量，性能开销小，通信高效。服务器与客户端之间交换的标头信息大概只有 2 字节;</li><li>可以发送文本，也可以发送二进制数据。</li><li>没有同源限制，客户端可以与任意服务器通信。</li><li>协议标识符是<code>ws</code>（如果加密，则为 wss），服务器网址就是 URL。ex：<code>ws://example.com:80/some/path</code></li><li>不用频繁创建及销毁 TCP 请求，减少网络带宽资源的占用，同时也节省服务器资源;</li><li>WebSocket 是纯事件驱动的，一旦连接建立，通过监听事件可以处理到来的数据和改变的连接状态，数据都以帧序列的形式传输。服务端发送数据后，消息和事件会异步到达。</li><li>无超时处理。</li></ol><h4 id="http-ws-">HTTP 与 WS 协议结构</h4><p>WebSocket 协议标识符用<code>ws</code>表示。`wss 协议表示加密的 WebSocket 协议，对应 HTTPs 协议。结构如下：</p><ul><li><strong>HTTP</strong>: TCP &gt; HTTP</li><li><strong>HTTPS</strong>: TCP &gt; TLS &gt; HTTP</li><li><strong>WS</strong>: TCP &gt; WS</li><li><strong>WSS</strong>: TCP &gt; TLS &gt; WS</li></ul><h2 id="2-websocket-">2 WebSocket 的通信过程</h2><p>首先，Websocket 是一个持久化的协议，相对于 HTTP 这种非持久的协议来说。</p><p>一个 HTTP 的通信生命周期通过 Request 来界定，也就是一个 Request 一个 &nbsp;Response ，那么在 HTTP1.0 中，这次 HTTP 请求就结束了。 在 HTTP1.1 中进行了改进，有了一个<code>keep-alive</code>，在一个 HTTP 连接中，可以发送多个 Request，接收多个 Response，也就是合并多个请求。但是一个 Request 只能对应一个 Response，而且这个 Response 是被动的，不能主动发起。</p><p>Websocket 其实是一个新协议，但是为了兼容现有浏览器的握手规范而借用了 HTTP 的协议来完成一部分握手。</p><p>WebSocket 是纯事件驱动的，一旦连接建立，通过监听事件可以处理到来的数据和改变的连接状态，数据都以帧序列的形式传输。服务端发送数据后，消息和事件会异步到达。WebSocket 编程遵循一个异步编程模型，只需要对 WebSocket 对象增加回调函数就可以监听事件。</p><h3 id="2-1-websocket-">2.1 WebSocket 通信流程图</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-12.png" class="kg-image" alt="image-12" width="628" height="511" loading="lazy"></figure><p>这里可以看出传统 HTTP 通讯与 WebSocket 通讯的通信流程上的区别，下图显示 WebSocket 主要的三步中浏览器和服务器端分别做了哪些事情。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/10/image-13.png" class="kg-image" alt="image-13" width="600" height="449" loading="lazy"></figure><h3 id="2-2-">2.2 建立连接的握手</h3><p>当 Web 应用程序调用<code>new WebSocket(url)</code>接口时，客户端就开始了与地址为 url 的 WebServer 建立握手连接的过程。</p><ol><li>客户端与服务端通过 TCP 三次握手建立连接，如果这个建立连接失败，那么后面的过程就不会执行，Web 应用程序将收到错误消息通知。</li><li>在 TCP 建立连接成功后，客户端通过 HTTP 协议传送 WebSocket 支持的版本号、协议的字版本号、原始地址、主机地址等等一些列字段给服务器端。</li><li>服务端收到客户端发送来的握手请求后，如果数据包数据和格式正确、客户端和服务端的协议版本号匹配等等，就接受本次握手连接，并给出相应的数据回复，同样回复的数据包也是采用 HTTP 协议传输。</li><li>客户端收到服务端回复的数据包后，如果数据包内容、格式都没有问题的话，就表示本次连接成功，触发<code>onopen</code>，此时 Web 开发者就可以在此时通过<code>send()</code>向服务器发送数据。否则握手连接失败，Web 应用程序触发<code>onerror</code>，并且能知道连接失败的原因。</li></ol><p>这个握手很像 HTTP，但是实际上却不是，它允许服务器以 HTTP 的方式解释一部分 handshake 的请求，然后切换为 websocket。</p><h3 id="2-3-websocket-">2.3 WebSocket 握手报文</h3><p>一个浏览器发出的 WebSocket 请求报文类似于：</p><pre><code>GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com</code></pre><p>HTTP1.1 协议规定，Upgrade 头信息表示将通信协议从 HTTP/1.1 转向该项所指定的协议。</p><ul><li><code>Connection: Upgrade</code>表示浏览器通知服务器，如果可以，就升级到 webSocket 协议。</li><li><code>Origin</code>用于验证浏览器域名是否在服务器许可的范围内。</li><li><code>Sec-WebSocket-Key</code>则是用于握手协议的密钥，是浏览器生成的 Base64 编码的 16 字节随机字符串。</li><li><code>Sec-WebSocket-Protocol</code>是一个用户定义的字符串，用来区分同 URL 下，不同的服务所需要的协议。</li><li><code>Sec-WebSocket-Version</code>是告诉服务器所使用的协议版本。</li></ul><p>服务端 WebSocket 回复报文：</p><pre><code>HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/</code></pre><ul><li>服务器端同样用<code>Connection: Upgrade</code>通知浏览器，服务端已经成功切换协议。</li><li><code>Sec-WebSocket-Accept</code>是经过服务器确认并且加密过后的<code>Sec-WebSocket-Key</code>。</li><li><code>Sec-WebSocket-Location</code>表示进行通信的 WebSocket 网址。</li><li><code>Sec-WebSocket-Protocol</code>表示最终使用的协议。</li></ul><p>在这样一个类似于 HTTP 通信的握手结束之后，下面就按照 WebSocket 协议进行通信了。客户端与服务器之间不会再发生 HTTP 通信，一切由 WebSocket 协议接管。</p><h2 id="3-websocket-api">3. WebSocket API</h2><p>浏览器提供了一个 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket" rel="nofollow noopener noreferrer">WebSocket</a> 对象的实现，可以用这个对象来创建和管理 WebSocket 连接，并且可以通过该连接发送和接受数据。WebSocket 是事件驱动的，因此只需要对 WebSocket 对象增加回调函数就可以监听事件的发生。</p><p>跟 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest" rel="nofollow noopener noreferrer">XMLHttpRequest</a> 一样，通过该构造函数先 new 出来对象实例<code>const ws = new WebSocket('ws://localhost:8080')</code>，再使用对象下挂载的属性与方法来操作。后文都用 <strong>ws </strong>来指代 WebSocket 的实例。</p><p><a href="https://codepen.io/SHERlocked93/pen/payNzm" rel="nofollow noopener noreferrer">查看 DEMO</a></p><h3 id="3-1-ws-">3.1 ws上常用属性</h3><h4 id="ws-readystate">ws.readyState</h4><p>WebSocket 实例对象类似于 XHR 有个的只读属性<code>readyState</code>来指示连接的当前状态：状态值描述 CONNECTING0 连接还没开启。OPEN1 连接已开启并准备好进行通信。CLOSING2 连接正在关闭的过程中。CLOSED3 连接已经关闭，或者连接无法建立。</p><p>一个示例：</p><pre><code>switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // ...
    break;
  case WebSocket.OPEN:
    // ...
    break;
  case WebSocket.CLOSING:
    // ...
    break;
  case WebSocket.CLOSED:
    // ...
    break;
  default:
    //  this never happens
    break;
}</code></pre><h4 id="ws-onopen-ws-onclose">ws.onopen / &nbsp;ws.onclose</h4><p>实例对象的<code>onopen</code>属性，用于指定连接成功后的回调函数。</p><pre><code>ws.onopen = function () {
  ws.send('Hello Server!');
}</code></pre><p>如果要指定多个回调函数，可以<code>addEventListener</code>。</p><pre><code>ws.addEventListener('open', function (event) {
  ws.send('Hello Server!');
});</code></pre><p>实例对象的<code>onclose</code>属性，用于指定连接关闭后的回调函数。</p><pre><code>ws.onclose = function(event) {
    const { code, reason, wasClean} = event
    // ...
};
ws.addEventListener('close', function(event) {
    const { code, reason, wasClean} = event
    // ...
})</code></pre><h4 id="ws-onmessage">ws.onmessage</h4><p>实例对象的<code>onmessage</code>属性，用于指定收到服务器数据后的回调函数。</p><pre><code>ws.onmessage = function(event) {
  const { data } = event;
  // ...
};
ws.addEventListener('message', function(event) {
  const { data } = event; 
  // ...
});</code></pre><p>注意，服务器数据可能是文本，也可能是二进制数据（blob 对象或 Arraybuffer 对象）。</p><pre><code>ws.onmessage = function(event){
  if(typeof event.data === String) {
    // string
  }
  if(event.data instanceof ArrayBuffer){
    const { data: buffer } = event;
    // array buffer
  }
}</code></pre><p>除了动态判断收到的数据类型，也可以使用<code>binaryType</code>属性，显式指定收到的二进制数据类型。<code>binaryType</code>取值应当是 'blob' 或者 'arraybuffer'，'blob' 表示使用 Blob &nbsp;对象，而 'arraybuffer' 表示使用 ArrayBuffer 对象。</p><pre><code>ws.binaryType = 'blob';                // 收到的是 Blob 数据
ws.onmessage = function(e) {
  console.log(e.data.size);
};

ws.binaryType = 'arraybuffer';            // 收到的是 ArrayBuffer 数据
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};</code></pre><p><a href="https://codepen.io/SHERlocked93/pen/EQKZab" rel="nofollow noopener noreferrer">查看 DEMO</a></p><h4 id="ws-bufferedamount">ws.bufferedAmount</h4><p>实例对象的<code>bufferedAmount</code>只读属性，表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。 该值会在所有队列数据被发送后重置为 0，而当连接关闭时不会设为 0。如果持续调用 send()，这个值会持续增长。</p><pre><code>var data = new ArrayBuffer(10000000);
ws.send(data);
if (ws.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送还没结束
}</code></pre><h4 id="ws-onerror">ws.onerror</h4><p>实例对象的<code>onerror</code>属性，用于指定报错时的回调函数。</p><pre><code>ws.onerror = function(event) {
  // handle error event
};
ws.addEventListener("error", function(event) {
  // handle error event
});</code></pre><h3 id="3-2-ws-">3.2 ws上常用方法</h3><h4 id="ws-close-">ws.close()</h4><p>关闭 WebSocket 连接或停止正在进行的连接请求。如果连接的状态已经是 closed，这个方法不会有任何效果。</p><h4 id="ws-send-">ws.send()</h4><p>实例对象的<code>send()</code>方法用于向服务器发送数据。</p><pre><code>ws.send('your message');                // 发送文本的例子

var file = document
  .querySelector('input[type="file"]')
  .files[0];
ws.send(file);                            // 发送 Blob 对象的例子

// Sending canvas ImageData as ArrayBuffer   
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i &lt; img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);                // 发送 ArrayBuffer 对象的例子</code></pre><p>最后一个 ArrayBuffer 对象栗子中的 canvas_context 实例是 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/getImageData" rel="nofollow noopener noreferrer">CanvasRenderingContext2D</a> 类型的对象，其上的<code>.getImageData()</code>方法返回一个 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData" rel="nofollow noopener noreferrer">ImageData</a> 对象。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 Proxy 来代理 JavaScript 里的类 ]]>
                </title>
                <description>
                    <![CDATA[ Proxy 对象（Proxy [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy] ）是 ES6 的一个非常酷却鲜为人知的特性。虽然这个特性存在已久，但是我还是想在本文中对其稍作解释，并用一个例子说明一下它的用法。 什么是 Proxy 正如 MDN 上简单而枯燥的定义： Proxy 对象用于定义基本操作的自定义行为（如属性查找，赋值，枚举，函数调用等）。 虽然这是一个不错的总结，但是我却并没有从中搞清楚 Proxy 能做什么，以及它能帮我们实现什么。 首先，Proxy 的概念来源于元编程。简单的说，元编程是允许我们运行我们编写的应用程序（或核心）代码的代码。例如，臭名昭著的 eval  函数允许我们将字符串代码当做可执行代码来执行，它是就属于元编程领域。 Proxy API 允许我们在对象和其消费实体中创建中间层，这种特性为我们提供了控制该对象的能力，比如可以决定怎样去进行它的 get 和 set ，甚至可以自定义当访问这个对象上不存在的属性的时候我们可 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/using-proxy-to-track-javascript-class/</link>
                <guid isPermaLink="false">5da69549ca1efa04e196a0ef</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Wed, 16 Oct 2019 04:11:03 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1535537501131-a93684fda998.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Proxy 对象（<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" rel="nofollow noopener noreferrer">Proxy</a>）是 ES6 的一个非常酷却鲜为人知的特性。虽然这个特性存在已久，但是我还是想在本文中对其稍作解释，并用一个例子说明一下它的用法。</p><h3 id="-proxy">什么是 Proxy</h3><p>正如 MDN 上简单而枯燥的定义：</p><p><strong>Proxy</strong> 对象用于定义基本操作的自定义行为（如属性查找，赋值，枚举，函数调用等）。</p><p>虽然这是一个不错的总结，但是我却并没有从中搞清楚 Proxy 能做什么，以及它能帮我们实现什么。</p><p>首先，Proxy 的概念来源于元编程。简单的说，元编程是允许我们运行我们编写的应用程序（或核心）代码的代码。例如，臭名昭著的 <code>eval</code> 函数允许我们将字符串代码当做可执行代码来执行，它是就属于元编程领域。</p><p><code>Proxy</code> API 允许我们在对象和其消费实体中创建中间层，这种特性为我们提供了控制该对象的能力，比如可以决定怎样去进行它的 <code>get</code> 和 <code>set</code>，甚至可以自定义当访问这个对象上不存在的属性的时候我们可以做些什么。</p><h3 id="proxy-api">Proxy 的 API</h3><pre><code>var p = new Proxy(target, handler);</code></pre><p><code>Proxy</code> 构造函数获取一个 <code>target</code> 对象，和一个用来拦截 <code>target</code> 对象不同行为的 <code>handler</code> 对象。你可以设置下面这些拦截项：</p><ul><li><code>has</code> — 拦截 <code>in</code> 操作。比如，你可以用它来隐藏对象上某些属性。</li><li><code>get</code> — 用来拦截<strong>读取</strong>操作。比如当试图读取不存在的属性时，你可以用它来返回默认值。</li><li><code>set</code> — 用来拦截<strong>赋值</strong>操作。比如给属性赋值的时候你可以增加验证的逻辑，如果验证不通过可以抛出错误。</li><li><code>apply</code> — 用来拦截<strong>函数调用</strong>操作。比如，你可以把所有的函数调用都包裹在 <code>try/catch</code> 语句块中。</li></ul><p>这只是一部分拦截项，你可以在 MDN 上找到<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy" rel="nofollow noopener noreferrer">完整的列表</a>。</p><p>下面是将 Proxy 用在验证上的一个简单的例子：</p><pre><code>const Car = {
  maker: 'BMW',
  year: 2018,
};

const proxyCar = new Proxy(Car, {
  set(obj, prop, value) {
    if (prop === 'maker' &amp;&amp; value.length &lt; 1) {
      throw new Error('Invalid maker');
    }

    if (prop === 'year' &amp;&amp; typeof value !== 'number') {
      throw new Error('Invalid year');
    }
    obj[prop] = value;
    return true;
  }

});

proxyCar.maker = ''; // throw exception
proxyCar.year = '1999'; // throw exception</code></pre><p>可以看到，我们可以用 Proxy 来验证赋给被代理对象的值。</p><h3 id="-proxy-">使用 Proxy 来调试</h3><p>为了在实践中展示 Proxy 的能力，我创建了一个简单的监测库，用来监测给定的对象或类，监测项如下：</p><ul><li>函数执行时间</li><li>函数的调用者或属性的访问者</li><li>统计每个函数或属性的被访问次数。</li></ul><p>这是通过在访问任意对象、类、甚至是函数时，调用一个名为 <code>proxyTrack</code> 的函数来完成的。</p><p>如果你希望监测是谁给一个对象的属性赋的值，或者一个函数执行了多久、执行了多少次、谁执行的，这个库将非常有用。我知道可能还有其他更好的工具来实现上面的功能，但是在这里我创建这个库就是为了用一用这个 API。</p><h4 id="-proxytrack">使用 proxyTrack</h4><p>首先，我们看看怎么用：</p><pre><code>function MyClass() {}

MyClass.prototype = {
    isPrime: function() {
        const num = this.num;
        for(var i = 2; i &lt; num; i++)
            if(num % i === 0) return false;
        return num !== 1 &amp;&amp; num !== 0;
    },

    num: null,
};

MyClass.prototype.constructor = MyClass;

const trackedClass = proxyTrack(MyClass);

function start() {
    const my = new trackedClass();
    my.num = 573723653;
    if (!my.isPrime()) {
        return `${my.num} is not prime`;
    }
}

function main() {
    start();
}

main();</code></pre><p>如果我们运行这段代码，控制台将会输出：</p><pre><code>MyClass.num is being set by start for the 1 time
MyClass.num is being get by isPrime for the 1 time
MyClass.isPrime was called by start for the 1 time and took 0 mils.
MyClass.num is being get by start for the 2 time</code></pre><p><code>proxyTrack</code> 接受 2 个参数：第一个是要监测的对象/类，第二个是一个配置项对象，如果没传递的话将被置为默认值。我们看看这个配置项默认值长啥样：</p><pre><code>const defaultOptions = {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    stdout: null,
    filter: null,
};</code></pre><p>可以看到，你可以通过配置你关心的监测项来监测你的目标。比如你希望将结果输出出来，那么你可以将 <code>console.log</code> 赋给 <code>stdout</code>。</p><p>还可以通过赋给 <code>filter</code> 的回调函数来自定义地控制输出哪些信息。你将会得到一个包括有监测信息的对象，并且如果你希望保留这个信息就返回 <code>true</code>，反之返回 <code>false</code>。</p><h4 id="-react-proxytrack">在 React 中使用 proxyTrack</h4><p>因为 React 的组件实际上也是类，所以你可以通过 <code>proxyTrack</code> 来实时监控它。比如：</p><pre><code>class MyComponent extends Component{...}

export default connect(mapStateToProps)(proxyTrack(MyComponent, {
    trackFunctions: true,
    trackProps: true,
    trackTime: true,
    trackCaller: true,
    trackCount: true,
    filter: (data) =&gt; {
        if( data.type === 'get' &amp;&amp; data.prop === 'componentDidUpdate') return false;
        return true;
    }
}));</code></pre><p>可以看到，你可以将你不关心的信息过滤掉，否则输出将会变得杂乱无章。</p><h3 id="-proxytrack-1">实现 proxyTrack</h3><p>我们来看看 <code>proxyTrack</code> 的实现。</p><p>首先是这个函数本身：</p><pre><code>export function proxyTrack(entity, options = defaultOptions) {
    if (typeof entity === 'function') return trackClass(entity, options);
    return trackObject(entity, options);
}</code></pre><p>没什么特别的嘛，这里只是调用相关函数。</p><p>再看看 <code>trackObject</code>：</p><pre><code>function trackObject(obj, options = {}) {
    const { trackFunctions, trackProps } = options;

    let resultObj = obj;
    if (trackFunctions) {
        proxyFunctions(resultObj, options);
    }
    if (trackProps) {
        resultObj = new Proxy(resultObj, {
            get: trackPropertyGet(options),
            set: trackPropertySet(options),
        });
    }
    return resultObj;
}
function proxyFunctions(trackedEntity, options) {
    if (typeof trackedEntity === 'function') return;
    Object.getOwnPropertyNames(trackedEntity).forEach((name) =&gt; {
        if (typeof trackedEntity[name] === 'function') {
            trackedEntity[name] = new Proxy(trackedEntity[name], {
                apply: trackFunctionCall(options),
            });
        }
    });
}</code></pre><p>可以看到，假如我们希望监测对象的属性，我们创建了一个带有 <code>get</code> 和 <code>set</code> 拦截器的被监测对象。下面是 <code>set</code> 拦截器的实现：</p><pre><code>function trackPropertySet(options = {}) {
    return function set(target, prop, value, receiver) {
        const { trackCaller, trackCount, stdout, filter } = options;
        const error = trackCaller &amp;&amp; new Error();
        const caller = getCaller(error);
        const contextName = target.constructor.name === 'Object' ? '' : `${target.constructor.name}.`;
        const name = `${contextName}${prop}`;
        const hashKey = `set_${name}`;
        if (trackCount) {
            if (!callerMap[hashKey]) {
                callerMap[hashKey] = 1;
            } else {
                callerMap[hashKey]++;
            }
        }
        let output = `${name} is being set`;
        if (trackCaller) {
            output += ` by ${caller.name}`;
        }
        if (trackCount) {
            output += ` for the ${callerMap[hashKey]} time`;
        }
        let canReport = true;
        if (filter) {
            canReport = filter({
                type: 'get',
                prop,
                name,
                caller,
                count: callerMap[hashKey],
                value,
            });
        }
        if (canReport) {
            if (stdout) {
                stdout(output);
            } else {
                console.log(output);
            }
        }
        return Reflect.set(target, prop, value, receiver);
    };
}</code></pre><p>更有趣的是 <code>trackClass</code> 函数（至少对我来说是这样）：</p><pre><code>function trackClass(cls, options = {}) {
    cls.prototype = trackObject(cls.prototype, options);
    cls.prototype.constructor = cls;

    return new Proxy(cls, {
        construct(target, args) {
            const obj = new target(...args);
            return new Proxy(obj, {
                get: trackPropertyGet(options),
                set: trackPropertySet(options),
            });
        },
        apply: trackFunctionCall(options),
    });
}</code></pre><p>在这个案例中，因为我们希望拦截这个类上不属于原型上的属性，所以我们给这个类的原型创建了个代理，并且创建了个构造函数拦截器。</p><p>别忘了，即使你在原型上定义了一个属性，但如果你再给这个对象赋值一个同名属性，JavaScript 将会创建一个这个属性的本地副本，所以赋值的改动并不会改变这个类其他实例的行为。这就是为何只对原型做代理并不能满足要求的原因。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Webpack 打包太慢? 试试 Dllplugin ]]>
                </title>
                <description>
                    <![CDATA[ Webpack 在 build 包的时候，有时候会遇到打包时间很长的问题，这里提供了一个解决方案，让打包如丝般顺滑。 1. 介绍 在用 Webpack 打包的时候，对于一些不经常更新的第三方库，比如 react，lodash，vue 我们希望能和自己的代码分离开，Webpack 社区有两种方案：  * CommonsChunkPlugin  * DLLPlugin 对于 CommonsChunkPlugin，Webpack 每次打包实际还是需要去处理这些第三方库，只是打包完之后，能把第三方库和我们自己的代码分开。而  DLLPlugin 则是能把第三方代码完全分离开，即每次只打包项目自身的代码。Dll 这个概念是借鉴了 Windows 系统的 dll。一个 dll 包，就是一个纯纯的依赖库，它本身不能运行，是用来给你的app引用的。 2. 模板 webpack-simple 用法 要使用 DLLPlugin，需要额外新建一个配置文件。所以对于用这种方式打包的项目，一般会有下面两个配置文件：  * webpack.config.js  * webpack.dll.config. ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/webpack-packaging-skills/</link>
                <guid isPermaLink="false">5d8ae9c9fbfdee429dc5fe72</guid>
                
                    <category>
                        <![CDATA[ Webpack ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Wed, 25 Sep 2019 04:29:47 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1569271532860-dd35503aaf1f.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Webpack 在 build 包的时候，有时候会遇到打包时间很长的问题，这里提供了一个解决方案，让打包如丝般顺滑。</p><h2 id="1-">1. 介绍</h2><p>在用 Webpack 打包的时候，对于一些不经常更新的第三方库，比如 <code>react</code>，<code>lodash</code>，<code>vue</code> 我们希望能和自己的代码分离开，Webpack 社区有两种方案：</p><ul><li>CommonsChunkPlugin</li><li>DLLPlugin</li></ul><p>对于 <code>CommonsChunkPlugin</code>，Webpack 每次打包实际还是需要去处理这些第三方库，只是打包完之后，能把第三方库和我们自己的代码分开。而 <code>DLLPlugin</code> 则是能把第三方代码完全分离开，即每次只打包项目自身的代码。Dll 这个概念是借鉴了 Windows 系统的 dll。一个 dll 包，就是一个纯纯的依赖库，它本身不能运行，是用来给你的app引用的。</p><h2 id="2-webpack-simple-">2. 模板 webpack-simple 用法</h2><p>要使用 <code>DLLPlugin</code>，需要额外新建一个配置文件。所以对于用这种方式打包的项目，一般会有下面两个配置文件：</p><ul><li>webpack.config.js</li><li>webpack.dll.config.js</li></ul><p><strong><strong>在项目根目录新建一个文件 webpack.dll.config.js</strong>。</strong></p><pre><code>const path    = require('path');
const webpack = require('webpack');
module.exports = {
  entry: {
      vendor: ['vue-router','vuex','vue/dist/vue.common.js','vue/dist/vue.js','vue-loader/lib/component-normalizer.js','vue']
  },
  output: {
    path: path.resolve('./dist'),
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.resolve('./dist', '[name]-manifest.json'),
      name: '[name]_library'
    })
  ]
};</code></pre><p>这是把用到的第三方插件添加到 vendor 中。<strong><strong>然后在</strong> <strong>webpack.config.js</strong> <strong>中添加代码</strong>。</strong></p><pre><code>plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dist/vendor-manifest.json')
    })
  ]</code></pre><p><strong><strong>再在入口</strong> <strong>html</strong> <strong>文件中引入 vendor.dll.js</strong>。</strong><br><code>&lt;script type="text/javascript" src="./../vendor.dll.js"&gt;&lt;/script&gt;</code></p><p><strong><strong>然后在</strong> <strong>package.json</strong> <strong>文件中添加快捷命令</strong></strong><strong>（build:dll）。</strong></p><pre><code>"scripts": {
    "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
    "build:dll": "webpack --config webpack.dll.config.js"
  },</code></pre><p>最后打包的时候首先执行 npm run build:dll 命令会在打包目录下生成 <code>vendor-manifest.json</code> 文件与 <code>vendor.dll.js</code> 文件。打包 dll 的时候，Webpack 会将所有包含的库做一个索引，写在一个 manifest 文件中，而引用 dll 的代码（dll user）在打包的时候，只需要读取这个 manifest 文件，就可以了。</p><p><strong><strong>再执行</strong></strong><code><strong><strong>npm run build</strong></strong></code><br>发现现在的 Webpack 打包速度为 2，3 秒左右，与之前的 20 秒左右快了很多。</p><h2 id="3-webpack-">3. 模板webpack 用法</h2><p><strong><strong>在</strong> <strong>build</strong> <strong>下创建 webpack.dll.config.js</strong>。</strong></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-58.png" class="kg-image" alt="image-58" width="293" height="303" loading="lazy"></figure><p>内容：</p><pre><code>const path = require('path')
const webpack = require('webpack')
module.exports = {
  entry: {
    vendor: [
      'vue-router',
      'vuex',
      'vue/dist/vue.common.js',
      'vue/dist/vue.js',
      'vue-loader/lib/component-normalizer.js',
      'vue',
      'axios',
      'echarts'
    ]
  },
  output: {
    path: path.resolve('./dist'),
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.resolve('./dist', '[name]-manifest.json'),
      name: '[name]_library'
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ]
}</code></pre><p>建议加上代码压缩插件，否则 dll 包会比较大。</p><p><strong><strong>在 webpack.prod.conf.js 的 plugin 后面加入配置</strong>。</strong></p><pre><code>new webpack.DllReferencePlugin({
    manifest: require('../dist/vendor-manifest.json')
})</code></pre><p><strong><strong>根目录下的入口 index.html 加入引用</strong>。</strong><br><code>&lt;script type="text/javascript" src="./vendor.dll.js"&gt;&lt;/script&gt;</code></p><p><strong><strong>package.json</strong> <strong>的</strong> <strong>script</strong> <strong>里加入快捷命令</strong>。</strong><br><code>"build:dll": "webpack --config build/webpack.dll.config.js"</code></p><p>要生成 dll 时运行<code>npm run build:dll</code>，即生成 dist 目录下两个文件 <code>vender-manifest.json</code> 与 <code>vender.dll.js</code>。<br>然后正式生成 prod <code>npm run build:prod</code>，即生成除<code>webpack.dll.config.js</code>中指定包之外的其他打包文件。</p><p>在尝试在 <a href="http://panjiachen.github.io/vue-element-admin" rel="nofollow noreferrer"><code>vue-element-admin</code></a> 中引入 DllPlugin 时，加入 20 个打包项，测试结果：<br>原来的打包时间——</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-59.png" class="kg-image" alt="image-59" width="600" height="257" loading="lazy"></figure><p>引入 DllPlugin 后的打包时间——</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-60.png" class="kg-image" alt="image-60" width="600" height="261" loading="lazy"></figure><p>可以看到大幅缩短了打包时间。</p><h2 id="4-externals-">4. 另一种方法 externals 选项</h2><p>也可以使用 externals 让 Webpack 不打包某部分，然后在其他地方引入 cdn 上的 js 文件，利用缓存下载 cdn 文件达到减少打包时间的目的。<br><strong><strong>配置</strong> <strong>externals</strong> <strong>选项</strong></strong>：</p><pre><code>// webpack.prod.config.js
// 多余代码省略
module.exports = {
    externals: {
        'vue': 'window.Vue',
        'vuex': 'window.Vuex',
        'vue-router': 'window.VueRouter'
        ...
    }
}

// 配置externals之后，webpack不会把配置项中的代码打包进去，别忘了需要在外部引入cdn上的js文件
// html
&lt;body&gt;
    &lt;script src="XXX/cdn/vue.min.js"&gt;&lt;/script&gt;
    ......
&lt;/body&gt;</code></pre> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 一文搞懂 Webpack 多入口配置 ]]>
                </title>
                <description>
                    <![CDATA[ 最近在做项目的时候遇到了一个场景：一个项目有多个入口，不同的入口，路由、组件、资源等有重叠部分，也有各自不同的部分。由于不同入口下的路由页面有一些是重复的，因此我考虑使用 Webpack 多入口配置来解决这个需求。 再一次，在网上找的不少文章都不合我的需求，很多文章都是只简单介绍了生产环境下配置，没有介绍开发环境下的配置，有的也没有将多入口结合 vue-router、 vuex、ElementUI  等进行配置，因此在下通过不断探坑，然后将思路和配置过程记录下来，留给自己作为笔记，同时也分享给大家，希望可以帮助到有同样需求的同学们。 1. 目标分析  1. 一个项目中保存了多个 HTML 模版，不同的模版有不同的入口，并且有各自的 router、store 等；  2. 不仅可以打包出不同 HTML，而且开发的时候也可以顺利进行调试；  3. 不同入口的文件可以引用同一份组件、图片等资源，也可以引用不同的资源； 代码仓库：multi-entry-vue [https://github.com/SHERlocked93/multi-entry-vue] 示意图如下： 2. 准备 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/an-introduction-to-webpack-multi-entry-configuration/</link>
                <guid isPermaLink="false">5d7f3f0afbfdee429dc5fc46</guid>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Mon, 16 Sep 2019 08:06:01 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/09/web-design-2906159_960_720.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近在做项目的时候遇到了一个场景：一个项目有多个入口，不同的入口，路由、组件、资源等有重叠部分，也有各自不同的部分。由于不同入口下的路由页面有一些是重复的，因此我考虑使用 Webpack 多入口配置来解决这个需求。</p><p>再一次，在网上找的不少文章都不合我的需求，很多文章都是只简单介绍了生产环境下配置，没有介绍开发环境下的配置，有的也没有将多入口结合 <code>vue-router</code>、<code>vuex</code>、<code>ElementUI</code> 等进行配置，因此在下通过不断探坑，然后将思路和配置过程记录下来，留给自己作为笔记，同时也分享给大家，希望可以帮助到有同样需求的同学们。</p><h2 id="1-">1. 目标分析</h2><ol><li>一个项目中保存了多个 HTML 模版，不同的模版有不同的入口，并且有各自的 router、store 等；</li><li>不仅可以打包出不同 HTML，而且开发的时候也可以顺利进行调试；</li><li>不同入口的文件可以引用同一份组件、图片等资源，也可以引用不同的资源；</li></ol><p>代码仓库：<a href="https://github.com/SHERlocked93/multi-entry-vue">multi-entry-vue</a></p><p>示意图如下：</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/9/10/16d1a5d14da28ebc?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><h2 id="2-">2. 准备工作</h2><p>首先我们 <code>vue init webpack multi-entry-vue</code> 使用 <code>vue-cli</code> 创建一个 webpack 模版的项。文件结构如下：</p><pre><code>.
├── build
├── config
├── src
│&nbsp;&nbsp; ├── assets
│&nbsp;&nbsp; │&nbsp;&nbsp; └── logo.png
│&nbsp;&nbsp; ├── components
│&nbsp;&nbsp; │&nbsp;&nbsp; └── HelloWorld.vue
│&nbsp;&nbsp; ├── router
│&nbsp;&nbsp; │&nbsp;&nbsp; └── index.js
│&nbsp;&nbsp; ├── App.vue
│&nbsp;&nbsp; └── main.js 
├── static
├── README.md
├── index.html
├── package-lock.json
└── package.json</code></pre><p>这里顺便介绍在不同系统下生成目录树的方法：</p><ol><li>mac 系统命令行生成目录树的方法 <code>tree -I node_modules --dirsfirst</code> ，这个命令的意思是，不显示 <code>node_modules</code> 路径的文件，并且以文件夹在前的排序方式生成目录树。如果报没有找到 tree 命令的错，安装 tree 命令行 <code>brew install tree</code> 即可。</li><li>windows 系统在目标目录下使用 <code>tree /f 1.txt</code> 即可把当前目录树生成到一个新文件 <code>1.txt</code> 中。</li></ol><p>首先我们简单介绍一下 Webpack 的相关配置项，这些配置项根据使用的 Webpack 模版不同，一般存放在 <code>webpack.config.js</code> 或 <code>webpack.base.conf.js</code> 中：</p><pre><code>const path = require('path')
module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'output-file.js',
    publicPath: '/'
  },
  module: {},        // 文件的解析 loader 配置
  plugins: [],       // 插件，根据需要配置各种插件
  devServer: {}      // 配置 dev 服务功能
}</code></pre><p>这个配置的意思是，进行 Webpack 后，会在命令的执行目录下新建 <code>dist</code> 目录（如果需要的话），并将打包 <code>src</code> 目录下的 <code>main.js</code> 和它的依赖，生成 <code>output-file.js</code> 放在 <code>dist</code> 目录中。</p><p>下面稍微解释一下相关配置项：</p><ol><li><strong>entry：</strong> 入口文件配置项，可以为字符串、对象、数组。 以上面的对象形式为例，<code>app</code> 是入口名称，如果 <code>output.filename</code> 中有 <code>[name]</code> 的话，就会被替换成 <code>app</code>。</li><li><strong>context：</strong> 是 webpack 编译时的基础目录，用于解析 <code>entry</code> 选项的基础目录(绝对路径)，<code>entry</code> 入口起点会相对于此目录查找，相当于公共目录，下面所有的目录都在这个公共目录下面。</li><li><strong>output：</strong> 出口文件的配置项。</li><li><strong>output/path：</strong> 打包文件输出的目录，比如上面的 <code>dist</code>，那么就会将输出的文件放在当前目录同级目录的 <code>dist</code> 文件夹下，没有这个文件夹就新建一个。 可以配置为 <code>path.resolve(__dirname, './dist/${Date.now()}/')</code> （md 语法不方便改成模板字符串，请自行修改）方便做持续集成。</li><li><strong>output.filename：</strong> 输出的文件名称，<code>[name]</code> 的意为根据入口文件的名称，打包成相同的名称，有几个入口，就可以打包出几个文件。 比如入口的 <code>key</code> 为 <code>app</code>，打包出来就是 <code>app.js</code>，入口是 <code>my-entry</code>，打包出来就是 <code>my-entry.js</code>。</li><li><strong>output.publicPath：</strong> 静态资源的公共路径，可以记住这个公式： <code>静态资源最终访问路径 = output.publicPath + 资源loader或插件等配置路径</code>。 举个例子，<code>publicPath</code> 配置为 <code>/dist/</code>，图片的 <code>url-loader</code> 配置项为 <code>name: 'img/[name].[ext]'</code> ，那么最终打包出来文件中图片的引用路径为 <code>output.publicPath + 'img/[name].[ext]' = '/dist/img/[name].[ext]'</code>。</li></ol><p>本文由于是入口和出口相关的配置，所以内容主要围绕着 <code>entry</code> 、<code>output</code> 和一个重要的 webpack 插件 <a href="https://webpack.js.org/plugins/html-webpack-plugin/">html-webpack-plugin</a>，这个插件是跟打包出来的 HTML 文件密切相关，主要有下面几个作用：</p><ol><li>根据模版生成 HTML 文件；</li><li>给生成的 HTML 文件引入外部资源比如 <code>link</code>、<code>script</code> 等；</li><li>改变每次引入的外部文件的 Hash，防止 HTML 引用缓存中的过时资源；</li></ol><p>下面我们从头一步步配置一个多入口项目。</p><h2 id="3-">3. 开始配置</h2><h3 id="3-1-">3.1 文件结构改动</h3><p>在 <code>src</code> 目录下将 <code>main.js</code> 和 <code>App.vue</code> 两个文件各复制一下，作为不同入口，文件结构变为：</p><pre><code>.
├── build
│&nbsp;&nbsp; ├── build.js
│&nbsp;&nbsp; ├── check-versions.js
│&nbsp;&nbsp; ├── logo.png
│&nbsp;&nbsp; ├── utils.js
│&nbsp;&nbsp; ├── vue-loader.conf.js
│&nbsp;&nbsp; ├── webpack.base.conf.js
│&nbsp;&nbsp; ├── webpack.dev.conf.js    # 主要配置目标
│&nbsp;&nbsp; └── webpack.prod.conf.js   # 主要配置目标
├── config
│&nbsp;&nbsp; ├── dev.env.js
│&nbsp;&nbsp; ├── index.js
│&nbsp;&nbsp; └── prod.env.js
├── src
│&nbsp;&nbsp; ├── assets
│&nbsp;&nbsp; │&nbsp;&nbsp; └── logo.png
│&nbsp;&nbsp; ├── components
│&nbsp;&nbsp; │&nbsp;&nbsp; └── HelloWorld.vue
│&nbsp;&nbsp; ├── router
│&nbsp;&nbsp; │&nbsp;&nbsp; └── index.js
│&nbsp;&nbsp; ├── App.vue
│&nbsp;&nbsp; ├── App2.vue       # 新增的入口
│&nbsp;&nbsp; ├── main.js
│&nbsp;&nbsp; └── main2.js       # 新增的入口
├── static
├── README.md
├── index.html
└── package.json</code></pre><h3 id="3-2-">3.2 简单配置</h3><p>要想从不同入口，打包出不同 HTML，我们可以改变一下 <code>entry</code> 和 <code>output</code> 两个配置，</p><pre><code>// build/webpack.prod.conf.js

module.exports = {
  entry: {
    entry1: './src/main.js',
    entry2: './src/main2.js'
  },
  output: {
    filename: '[name].js',
    publicPath: '/'
  },
    plugins: [
        new HtmlWebpackPlugin({
            template: "index.html",  // 要打包输出哪个文件，可以使用相对路径
            filename: "index.html"   // 打包输出后该html文件的名称
        })
    ]
}</code></pre><p>根据上面一小节我们知道，webpack 配置里的 <code>output.filename</code> 如果有 <code>[name]</code> 意为根据入口文件的名称，打包成对应名称的 JS 文件，那么现在我们是可以根据两个入口打包出 <code>entry.js</code> 和 <code>entry2.js</code>。</p><p>打包的结果如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-41.png" class="kg-image" alt="image-41" width="552" height="345" loading="lazy"></figure><p>当前代码：<a href="https://github.com/SHERlocked93/multi-entry-vue/tree/entry-output">Github - multi-entry-vue1</a></p><p>如上图，此时我们 <code>npm run build</code> 打包出一个引用了这两个文件的 <code>index.html</code>，那么如何打包出不同 HTML 文件，分别应用不同入口 JS 文件呢，此时我们需要借助于 <code>HtmlWebpackPlugin</code> 这个插件。</p><p><code>HtmlWebpackPlugin</code> 这个插件，<code>new</code> 一个，就打包一个 HTML 页面，所以我们在 <code>plugins</code> 配置里 <code>new</code> 两个，就能打包出两个页面来。</p><h3 id="3-3-html-">3.3 打包出不同的 HTML 页面</h3><p>我们把配置文件改成下面这样：</p><pre><code>// build/webpack.prod.conf.js

module.exports = {
  entry: {
    entry: './src/main.js',   // 打包输出的chunk名为entry
    entry2: './src/main2.js'  // 打包输出的chunk名为entry2
  },
  output: {
    filename: '[name].js',
    publicPath: '/'
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'entry.html',  // 要打包输出的文件名
      template: 'index.html',  // 打包输出后该html文件的名称
      chunks: ['manifest', 'vendor', 'entry']  // 输出的html文件引入的入口chunk
      // 还有一些其他配置比如minify、chunksSortMode和本文无关就省略，详见github
    }),
    new HtmlWebpackPlugin({
      filename: 'entry2.html',
      template: 'index.html',
      chunks: ['manifest', 'vendor', 'entry2']
    })
  ]
}</code></pre><p>上面一个配置要注意的是 <code>chunks</code>，如果没有配置，那么生成的 HTML 会引入所有入口 JS 文件，在上面的例子就是，生成的两个 HTML 文件都会引入 <code>entry.js</code> 和 <code>entry2.js</code>，所以要使用 <code>chunks</code> 配置来指定生成的 HTML 文件应该引入哪个 JS 文件。配置了 <code>chunks</code> 之后，才能达到不同的 HTML 只引入对应 <code>chunks</code> 的 JS 文件的目的。</p><p>大家可以看到除了我们打包生成的 <code>chunk</code> 文件 <code>entry.js</code> 和 <code>entry2.js</code> 之外，还有 <code>manifest</code> 和 <code>vendor</code> 这两个，这里稍微解释一下这两个 <code>chunk</code>：</p><ol><li><code>vendor</code> 是指提取涉及 <code>node_modules</code> 中的公共模块；</li><li><code>manifest</code> 是对 <code>vendor</code> 模块做的缓存；</li></ol><p>打包完的结果如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-42.png" class="kg-image" alt="image-42" width="800" height="439" loading="lazy"></figure><p>文件结构：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-43.png" class="kg-image" alt="image-43" width="553" height="374" loading="lazy"></figure><p>现在打包出来的样式正是我们所需要的，此时我们在 <code>dist</code> 目录下启动 <code>live-server</code>（如果你没安装的话可以先安装 <code>npm i -g live-server</code>），就可以看到效果出来了：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/09/image-45.png" class="kg-image" alt="image-45" width="640" height="391" loading="lazy"></figure><p>当前代码：<a href="https://github.com/SHERlocked93/multi-entry-vue/tree/import-htmlwebpackplugin">Github - multi-entry-vue2</a></p><p>至此就实现了一个简单的多入口项目的配置。</p><h2 id="4-">4. 配置改进</h2><h3 id="4-1-">4.1 文件结构改动</h3><p>我们在前文进行了多入口的配置，要想新建一个新的入口，就复制多个文件，再手动改一下对应配置。</p><p>但是如果不同的 HTML 文件下不同的 <code>vue-router</code>、<code>vuex</code> 都放到 <code>src</code> 目录下，多个入口的内容平铺在一起，项目目录会变得凌乱不清晰，因此在下将多入口相关的文件放到一个单独的文件夹中，以后如果有多入口的内容，就到这个文件夹中处理。</p><p>下面我们进行文件结构的改造：</p><ol><li>首先我们在根目录创建一个 <code>entries</code> 文件夹，把不同入口的 <code>router</code>、<code>store</code>、<code>main.js</code> 都放这里，每个入口相关单独放在一个文件夹；</li><li>在 <code>src</code> 目录下建立一个 <code>common</code> 文件夹，用来存放多入口共用的组件等；</li></ol><p>现在的目录结构：</p><pre><code>.
├── build    # 没有改动
├── config   # 没有改动
├── entries  # 存放不同入口的文件
│&nbsp;&nbsp; ├── entry1
│&nbsp;&nbsp; │&nbsp;&nbsp; ├── router       # entry1 的 router
│&nbsp;&nbsp; │&nbsp;&nbsp; │&nbsp;&nbsp; └── index.js
│&nbsp;&nbsp; │&nbsp;&nbsp; ├── store        # entry1 的 store
│&nbsp;&nbsp; │&nbsp;&nbsp; │&nbsp;&nbsp; └── index.js
│&nbsp;&nbsp; │&nbsp;&nbsp; ├── App.vue      # entry1 的根组件
│&nbsp;&nbsp; │&nbsp;&nbsp; ├── index.html   # entry1 的页面模版
│&nbsp;&nbsp; │&nbsp;&nbsp; └── main.js      # entry1 的入口
│&nbsp;&nbsp; └── entry2
│&nbsp;&nbsp;     ├── router
│&nbsp;&nbsp;     │&nbsp;&nbsp; └── index.js
│&nbsp;&nbsp;     ├── store
│&nbsp;&nbsp;     │&nbsp;&nbsp; └── index.js
│&nbsp;&nbsp;     ├── App.vue
│&nbsp;&nbsp;     ├── index.html
│&nbsp;&nbsp;     └── main.js
├── src
│&nbsp;&nbsp; ├── assets
│&nbsp;&nbsp; │&nbsp;&nbsp; └── logo.png
│&nbsp;&nbsp; ├── common          # 多入口通用组件
│&nbsp;&nbsp; │&nbsp;&nbsp; └── CommonTemplate.vue
│&nbsp;&nbsp; └── components
│&nbsp;&nbsp;     ├── HelloWorld.vue
│&nbsp;&nbsp;     ├── test1.vue
│&nbsp;&nbsp;     └── test2.vue
├── static
├── README.md
├── index.html
├── package-lock.json
└── package.json</code></pre><h3 id="4-2-webpack-">4.2 webpack 配置</h3><p>然后我们在 <code>build/utils</code> 文件中加两个函数，分别用来生成 webpack 的 <code>entry</code> 配置和 <code>HtmlWebpackPlugin</code> 插件配置，由于要使用 <code>node.js</code> 来读取文件夹结构，因此需要引入 <code>fs</code>、<code>glob</code> 等模块：</p><pre><code>// build/utils
const fs = require('fs')
const glob = require('glob')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ENTRY_PATH = path.resolve(__dirname, '../entries')

// 多入口配置，这个函数从 entries 文件夹中读取入口文件，装配成webpack.entry配置
exports.entries = function() {
  const entryFiles = glob.sync(ENTRY_PATH + '/*/*.js')
  const map = {}
  entryFiles.forEach(filePath =&gt; {
    const filename = filePath.replace(/.*\/(\w+)\/\w+(\.html|\.js)$/, (rs, $1) =&gt; $1)
    map[filename] = filePath
  })
  return map
}

// 多页面输出模版配置 HtmlWebpackPlugin，根据环境装配html模版配置
exports.htmlPlugin = function() {
  let entryHtml = glob.sync(ENTRY_PATH + '/*/*.html')
  let arr = []
  entryHtml.forEach(filePath =&gt; {
    let filename = filePath.replace(/.*\/(\w+)\/\w+(\.html|\.js)$/, (rs, $1) =&gt; $1)
    let conf = {
      template: filePath,
      filename: filename + '.html',
      chunks: [filename],
      inject: true
    }
    
    // production 生产模式下配置
    if (process.env.NODE_ENV === 'production') {
      conf = merge(conf, {
        chunks: ['manifest', 'vendor'],
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeAttributeQuotes: true
        },
        chunksSortMode: 'dependency'
      })
    }
    arr.push(new HtmlWebpackPlugin(conf))
  })
  return arr
}</code></pre><p>稍微解释一下这两个函数：</p><p><code>exports.entries</code> 函数从 <code>entries</code> 文件夹中找到二级目录下的 JS 文件作为入口文件，并且将二级目录的文件夹名作为 <code>key</code>，生成这样一个对象：<code>{"entry1": "/multi-entry-vue/entries/entry1/main.js"}</code>，多个入口情况下会有更多键值对；</p><p><code>exports.htmlPlugin</code> 函数和之前函数的原理类似，不过组装的是 <code>HtmlWebpackPlugin</code> 插件的配置，生成这样一个数组，可以看到和我们手动设置的配置基本一样，只不过现在是根据文件夹结构来生成的：</p><pre><code>// production 下
[
  {
    template: "/multi-entry-vue/entries/entry1/index.html",
    chunks: ['manifest', 'vendor', 'entry1'],
    filename: "entry1.html",
    chunksSortMode: 'dependency'
  },
  { ... }   // 下一个入口的配置
]</code></pre><p>有了这两个根据 <code>entries</code> 文件夹的结构来自动生成 webpack 配置的函数，下面来改一下 webpack 相关的几个配置文件：</p><pre><code>// build/webpack.base.conf.js

module.exports = {
  entry: utils.entries(),   // 使用函数生成 entry 配置
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  }
}</code></pre><pre><code>// build/webpack.dev.conf.js

// const HtmlWebpackPlugin = require('html-webpack-plugin')  // 不需要了

const devWebpackConfig = merge(baseWebpackConfig, {
  devServer: {
    historyApiFallback: {
      rewrites: [        // 别忘了把 devserver 的默认路由改一下
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'entry1.html') },
      ],
    }
  },
  plugins: [
    // https://github.com/ampedandwired/html-webpack-plugin
    // new HtmlWebpackPlugin({
    //   filename: 'index.html',
    //   template: 'index.html',
    //   inject: true
    // }),                   // 注释掉原来的 HtmlWebpackPlugin 配置，使用生成的配置
  ].concat(utils.htmlPlugin())
})</code></pre><pre><code>// build/webpack.prod.conf.js

// const HtmlWebpackPlugin = require('html-webpack-plugin')

const webpackConfig = merge(baseWebpackConfig, {
  plugins: [
    // new HtmlWebpackPlugin({
    //   ... 注释掉，不需要了
    // }),
  ].concat(utils.htmlPlugin())
})</code></pre><p>现在我们再 <code>npm run build</code>，看看生成的目录是什么样的：</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/9/10/16d1a5d152172efa?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><p>此时我们在 <code>dist</code> 目录下启动 <code>live-server</code> 看看是什么效果：</p><figure class="kg-card kg-image-card"><img src="https://user-gold-cdn.xitu.io/2019/9/10/16d1a5d1a43b2fe2?imageslim" class="kg-image" alt="16d1a5d1a43b2fe2?imageslim" width="600" height="400" loading="lazy"></figure><p>当前代码：<a href="https://github.com/SHERlocked93/multi-entry-vue/tree/config-gene">Github - multi-entry-vue3</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 「Vue 实用技巧」策略模式实现动态表单验证 ]]>
                </title>
                <description>
                    <![CDATA[ 策略模式 （Strategy Pattern）又称政策模式，其定义一系列的算法，把它们一个个封装起来，并且使它们可以互相替换。封装的策略算法一般是独立的，策略模式根据输入来调整采用哪个算法。关键是策略的 实现和使用分离。 > 注意： 本文可能用到一些编码技巧比如 IIFE [https://developer.mozilla.org/zh-CN/docs/Glossary/%E7%AB%8B%E5%8D%B3%E6%89%A7%E8%A1%8C%E5%87%BD%E6%95%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F] （Immediately Invoked Function Expression, 立即调用函数表达式），ES6 的语法 let/const [http://es6.ruanyifeng.com/#docs/let]、箭头函数 [http://es6.ruanyifeng.com/#docs/function]、rest 参数 [http://es6.ruanyifeng.com/#docs/function]，短路运算符 [https://jueji ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/dynamic-form-validation-with-strategy-pattern/</link>
                <guid isPermaLink="false">5d64d5e3fbfdee429dc5f95d</guid>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Tue, 27 Aug 2019 07:32:26 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/08/1_VyUN0eeSx1yujR3tP3zToQ.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><strong>策略模式</strong> （Strategy Pattern）又称政策模式，其定义一系列的算法，把它们一个个封装起来，并且使它们可以互相替换。封装的策略算法一般是独立的，策略模式根据输入来调整采用哪个算法。关键是策略的<strong>实现和使用分离</strong>。</p><blockquote><strong>注意：</strong> 本文可能用到一些编码技巧比如 <a href="https://developer.mozilla.org/zh-CN/docs/Glossary/%E7%AB%8B%E5%8D%B3%E6%89%A7%E8%A1%8C%E5%87%BD%E6%95%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F">IIFE</a>（Immediately Invoked Function Expression, 立即调用函数表达式），ES6 的语法 <a href="http://es6.ruanyifeng.com/#docs/let">let/const</a>、<a href="http://es6.ruanyifeng.com/#docs/function">箭头函数</a>、<a href="http://es6.ruanyifeng.com/#docs/function">rest 参数</a>，<a href="https://juejin.im/post/5b51e5d3f265da0f4861143c#heading-5">短路运算符</a> 等，如果还没接触过可以点击链接稍加学习 。</blockquote><h2 id="1-">1. 你曾见过的策略模式</h2><p>现在电子产品种类繁多，尺寸多种多样，有时候你会忍不住想拆开看看里面啥样（想想小时候拆的玩具车还有遥控器😅），但是螺丝规格很多，螺丝刀尺寸也不少，如果每碰到一种规格就买一个螺丝刀，家里就得堆满螺丝刀了。所以现在人们都用多功能的螺丝刀套装，螺丝刀把只需要一个，碰到不同规格的螺丝只要换螺丝刀头就行了，很方便，体积也变小很多。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/WechatIMG1816.jpeg" class="kg-image" alt="WechatIMG1816" width="450" height="454" loading="lazy"></figure><p>再举个栗子，一辆车的轮胎有很多规格，在泥泞路段开的多的时候可以用泥地胎，在雪地开得多可以用雪地胎，高速公路上开的多的时候使用高性能轮胎，针对不同使用场景更换不同的轮胎即可，不需更换整个车。</p><p>这些都是策略模式的实例，螺丝刀/车属于封装上下文，封装和使用不同的螺丝刀头/轮胎，螺丝刀头/轮胎这里就相当于策略，可以根据需求不同来更换不同的使用策略。</p><p>在这些场景中，有以下特点：</p><ol><li>螺丝刀头/轮胎（策略）之间相互独立，但又可以相互替换；</li><li>螺丝刀/车（封装上下文）可以根据需要的不同选用不同的策略；</li></ol><h2 id="2-">2. 实例的代码实现</h2><p>具体的例子我们用编程上的例子来演示，比较好量化。</p><p>场景是这样的，某个电商网站希望举办一个活动，通过打折促销来销售库存物品，有的商品满 100 减 30，有的商品满 200 减 80，有的商品直接 8 折出售（想起被双十一支配的恐惧），这样的逻辑交给我们，我们要怎样去实现呢？</p><pre><code>function priceCalculate(discountType, price) {
    if (discountType === 'minus100_30') {   		// 满100减30
        return price - Math.floor(price / 100) * 30
    }
    else if (discountType === 'minus200_80') {  // 满200减80
        return price - Math.floor(price / 200) * 80
    }
    else if (discountType === 'percent80') {    // 8折
        return price * 0.8
    }
}

priceCalculate('minus100_30', 270)    // 输出: 210
priceCalculate('percent80', 250)      // 输出: 200</code></pre><p>通过判断输入的折扣类型来计算计算商品总价的方式，几个 <code>if-else</code> 就满足了需求，但是这样的做法的缺点也很明显：</p><ol><li><code>priceCalculate</code> 函数随着折扣类型的增多，<code>if-else</code> 判断语句会变得越来越臃肿；</li><li>如果增加了新的折扣类型或者折扣类型的算法有所改变，那么需要更改 <code>priceCalculate</code> 函数的实现，这是违反开放-封闭原则的；</li><li>可复用性差，如果在其他的地方也有类似这样的算法，但规则不一样，上述代码不能复用；</li></ol><p>我们可以改造一下，将计算折扣的<strong>算法部分提取出来</strong>保存为一个对象，折扣的<strong>类型作为 key</strong>，这样索引的时候<strong>通过对象的键值索引调用具体的算法</strong>：</p><pre><code>const DiscountMap = {
    minus100_30: function(price) {
        return price - Math.floor(price / 100) * 30
    },
    minus200_80: function(price) {
        return price - Math.floor(price / 200) * 80
    },
    percent80: function(price) {
        return price * 0.8
    }
}

/* 计算总售价*/
function priceCalculate(discountType, price) {
    return DiscountMap[discountType] &amp;&amp; DiscountMap[discountType](price)
}

priceCalculate('minus100_30', 270)
priceCalculate('percent80', 250)

// 输出: 210
// 输出: 200</code></pre><p>这样<strong>算法的实现和算法的使用就被分开了</strong>，想添加新的算法也变得十分简单：</p><pre><code>DiscountMap.minus150_40 = function(price) {
    return price - Math.floor(price / 150) * 40
}</code></pre><p>如果你希望计算算法隐藏起来，那么可以借助 IIFE 使用闭包的方式，这时需要添加增加策略的入口，以方便扩展：</p><pre><code>const PriceCalculate = (function() {
    /* 售价计算方式 */
    const DiscountMap = {
        minus100_30: function(price) {      // 满100减30
            return price - Math.floor(price / 100) * 30
        },
        minus200_80: function(price) {      // 满200减80
            return price - Math.floor(price / 200) * 80
        },
        percent80: function(price) {        // 8折
            return price * 0.8
        }
    }
    
    return {
        priceClac: function(discountType, price) {
            return DiscountMap[discountType] &amp;&amp; DiscountMap[discountType](price)
        },
        addStrategy: function(discountType, fn) {		// 注册新计算方式
            if (DiscountMap[discountType]) return
            DiscountMap[discountType] = fn
        }
    }
})()

PriceCalculate.priceClac('minus100_30', 270)	// 输出: 210

PriceCalculate.addStrategy('minus150_40', function(price) {
    return price - Math.floor(price / 150) * 40
})
PriceCalculate.priceClac('minus150_40', 270)	// 输出: 230</code></pre><p>这样算法就被隐藏起来，并且预留了增加策略的入口，便于扩展。</p><h2 id="3-">3. 策略模式的通用实现</h2><p>根据上面的例子提炼一下策略模式，折扣计算方式可以被认为是策略（Strategy），这些策略之间可以相互替代，而具体折扣的计算过程可以被认为是封装上下文（Context），封装上下文可以根据需要选择不同的策略。</p><p>主要有下面几个概念：</p><ol><li><strong>Context</strong> ：封装上下文，根据需要调用需要的策略，屏蔽外界对策略的直接调用，只对外提供一个接口，根据需要调用对应的策略；</li><li><strong>Strategy</strong> ：策略，含有具体的算法，其方法的外观相同，因此可以互相代替；</li><li><strong>StrategyMap</strong> ：所有策略的合集，供封装上下文调用；</li></ol><p>结构图如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/image-68.png" class="kg-image" alt="image-68" width="800" height="370" loading="lazy"></figure><p>下面使用通用化的方法来实现一下。</p><pre><code>const StrategyMap = {}

function context(type, ...rest) {
    return StrategyMap[type] &amp;&amp; StrategyMap[type](...rest)
}

StrategyMap.minus100_30 = function(price) { 
  	return price - Math.floor(price / 100) * 30
}

context('minus100_30', 270)			// 输出: 210</code></pre><p>通用实现看起来似乎比较简单，这里分享一下项目实战。</p><h2 id="4-">4. 实战中的策略模式</h2><h3 id="4-1-formatter">4.1 表格 formatter</h3><p>这里举一个 Vue + ElementUI 项目中用到的例子，其他框架的项目原理也类似，和大家分享一下。</p><p>Element 的<a href="https://element.eleme.cn/#/zh-CN/component/table#table-biao-ge">表格控件</a>的 Column 接受一个 <code>formatter</code> 参数，用来格式化内容，其类型为函数，并且还可以接受几个特定参数，像这样： <code>Function(row, column, cellValue, index)</code>。</p><p>以文件大小转化为例，后端经常会直接传 bit 单位的文件大小，那么前端需要根据后端的数据，根据需求转化为自己需要的单位的文件大小，比如 KB/MB。</p><p>首先实现文件计算的算法：</p><pre><code>export const StrategyMap = {
    /* Strategy 1: 将文件大小（bit）转化为 KB */
    bitToKB: val =&gt; {
        const num = Number(val)
        return isNaN(num) ? val : (num / 1024).toFixed(0) + 'KB'
    },
    /* Strategy 2: 将文件大小（bit）转化为 MB */
    bitToMB: val =&gt; {
        const num = Number(val)
        return isNaN(num) ? val : (num / 1024 / 1024).toFixed(1) + 'MB'
    }
}

/* Context: 生成el表单 formatter */
const strategyContext = function(type, rowKey){ 
  return function(row, column, cellValue, index){
  	return StrategyMap[type](row[rowKey])
  }
}

export default strategyContext</code></pre><p>那么在组件中我们可以直接：</p><pre><code>&lt;template&gt;
    &lt;el-table :data="tableData"&gt;
        &lt;el-table-column prop="date" label="日期"&gt;&lt;/el-table-column&gt;
        &lt;el-table-column prop="name" label="文件名"&gt;&lt;/el-table-column&gt;
        &lt;!-- 直接调用 strategyContext --&gt;
        &lt;el-table-column prop="sizeKb" label="文件大小(KB)"
                         :formatter='strategyContext("bitToKB", "sizeKb")'&gt;
        &lt;/el-table-column&gt;
        &lt;el-table-column prop="sizeMb" label="附件大小(MB)"
                         :formatter='strategyContext("bitToMB", "sizeMb")'&gt;
        &lt;/el-table-column&gt;
    &lt;/el-table&gt;
&lt;/template&gt;

&lt;script type='text/javascript'&gt;
    import strategyContext from './strategyContext.js'
    
    export default {
        name: 'ElTableDemo',
        data() {
            return {
                strategyContext,
                tableData: [
                    { date: '2019-05-02', name: '文件1', sizeKb: 1234, sizeMb: 1234426 },
                    { date: '2019-05-04', name: '文件2', sizeKb: 4213, sizeMb: 8636152 }]
            }
        }
    }
&lt;/script&gt;

&lt;style scoped&gt;&lt;/style&gt;</code></pre><p>代码实例可以参看 <a href="https://codepen.io/SHERlocked93/pen/NmzLBK">codepen - 策略模式实战</a>。</p><p>运行结果如下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/image-69.png" class="kg-image" alt="image-69" width="1080" height="271" loading="lazy"></figure><h3 id="4-2-">4.2 表单验证</h3><p>除了表格中的 formatter 之外，策略模式也经常用在表单验证的场景，这里举一个 Vue + ElementUI 项目的例子，其他框架同理。</p><p>ElementUI 的 <a href="https://element.eleme.cn/#/zh-CN/component/form">Form 表单</a> 具有表单验证功能，用来校验用户输入的表单内容。实际需求中表单验证项一般会比较复杂，所以需要给每个表单项增加 validator 自定义校验方法。</p><p>我们可以像<a href="https://codepen.io/SHERlocked93/pen/VJbgey">官网示例</a>一样把表单验证都写在组件的状态 <code>data</code> 函数中，但是这样就不好复用使用频率比较高的表单验证方法了，这时我们可以结合策略模式和函数柯里化的知识来重构一下。首先我们在项目的工具模块（一般是 <code>utils</code> 文件夹）实现通用的表单验证方法：</p><pre><code>// src/utils/validates.js

/* 姓名校验 由2-10位汉字组成 */
export function validateUsername(str) {
    const reg = /^[\u4e00-\u9fa5]{2,10}$/
    return reg.test(str)
}

/* 手机号校验 由以1开头的11位数字组成  */
export function validateMobile(str) {
    const reg = /^1\d{10}$/
    return reg.test(str)
}

/* 邮箱校验 */
export function validateEmail(str) {
    const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
    return reg.test(str)
}</code></pre><p>然后在 <code>utils/index.js</code> 中增加一个柯里化方法，用来生成表单验证函数：</p><pre><code>// src/utils/index.js

import * as Validates from './validates.js'

/* 生成表格自定义校验函数 */
export const formValidateGene = (key, msg) =&gt; (rule, value, cb) =&gt; {
    if (Validates[key](value)) {
        cb()
    } else {
        cb(new Error(msg))
    }
}</code></pre><p>上面的 <code>formValidateGene</code> 函数接受两个参数，第一个是验证规则，也就是 <code>src/utils/validates.js</code> 文件中提取出来的通用验证规则的方法名，第二个参数是报错的话表单验证的提示信息。</p><pre><code>&lt;template&gt;
    &lt;el-form ref="ruleForm"
             label-width="100px"
             class="demo-ruleForm"
             :rules="rules"
             :model="ruleForm"&gt;
        
        &lt;el-form-item label="用户名" prop="username"&gt;
            &lt;el-input v-model="ruleForm.username"&gt;&lt;/el-input&gt;
        &lt;/el-form-item&gt;
        
        &lt;el-form-item label="手机号" prop="mobile"&gt;
            &lt;el-input v-model="ruleForm.mobile"&gt;&lt;/el-input&gt;
        &lt;/el-form-item&gt;
        
        &lt;el-form-item label="邮箱" prop="email"&gt;
            &lt;el-input v-model="ruleForm.email"&gt;&lt;/el-input&gt;
        &lt;/el-form-item&gt;
    &lt;/el-form&gt;
&lt;/template&gt;

&lt;script type='text/javascript'&gt;
    import * as Utils from '../utils'
    
    export default {
        name: 'ElTableDemo',
        data() {
            return {
                ruleForm: { pass: '', checkPass: '', age: '' },
                rules: {
                    username: [{
                        validator: Utils.formValidateGene('validateUsername', '姓名由2-10位汉字组成'),
                        trigger: 'blur'
                    }],
                    mobile: [{
                        validator: Utils.formValidateGene('validateMobile', '手机号由以1开头的11位数字组成'),
                        trigger: 'blur'
                    }],
                    email: [{
                        validator: Utils.formValidateGene('validateEmail', '不是正确的邮箱格式'),
                        trigger: 'blur'
                    }]
                }
            }
        }
    }
&lt;/script&gt;</code></pre><p>可以看见在使用的时候非常方便，把表单验证方法提取出来作为策略，使用柯里化方法动态选择表单验证方法，从而对策略灵活运用，大大加快开发效率。</p><p>代码实例可以参看 <a href="https://codesandbox.io/embed/demo-for-element-validate-ztlie">codesandbox - 策略模式表单验证实战</a>。</p><p>运行结果：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/image-70.png" class="kg-image" alt="image-70" width="800" height="278" loading="lazy"></figure><h2 id="5-">5. 策略模式的优缺点</h2><p>策略模式将算法的<strong>实现和使用拆分</strong>，这个特点带来了很多优点：</p><ol><li>策略之间相互独立，但<strong>策略可以自由切换</strong>，这个策略模式的特点给策略模式带来很多灵活性，也提高了策略的复用率；</li><li>如果不采用策略模式，那么在选策略时一般会采用多重的条件判断，采用策略模式可以<strong>避免多重条件判断</strong>，增加可维护性；</li><li><strong>可扩展性好</strong>，策略可以很方便的进行扩展；</li></ol><p>策略模式的缺点：</p><ol><li>策略相互独立，因此一些复杂的算法逻辑<strong>无法共享</strong>，造成一些资源浪费；</li><li>如果用户想采用什么策略，必须了解策略的实现，因此<strong>所有策略都需向外暴露</strong>，这是违背迪米特法则/最少知识原则的，也增加了用户对策略对象的使用成本。</li></ol><h2 id="6-">6. 策略模式的适用场景</h2><p>那么应该在什么场景下使用策略模式呢：</p><ol><li>多个算法<strong>只在行为上稍有不同</strong>的场景，这时可以使用策略模式来动态选择算法；</li><li>算法<strong>需要自由切换</strong>的场景；</li><li>有时<strong>需要多重条件判断</strong>，那么可以使用策略模式来规避多重条件判断的情况；</li></ol><h2 id="7-">7. 其他相关模式</h2><h3 id="7-1-">7.1 策略模式和模板方法模式</h3><p>策略模式和模板方法模式的作用比较类似，但是结构和实现方式有点不一样。</p><ol><li><strong>策略模式</strong> 让我们在程序运行的时候动态地指定要使用的算法；</li><li><strong>模板方法模式</strong> 是在子类定义的时候就已经确定了使用的算法；</li></ol><h3 id="7-2-">7.2 策略模式和享元模式</h3><p>见<a href="https://chinese.freecodecamp.org/news/flyweight-pattern-and-resource-pool/">享元模式</a>中的介绍。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 享元模式与资源池 ]]>
                </title>
                <description>
                    <![CDATA[ 享元模式 （Flyweight Pattern）运用共享技术来有效地支持大量细粒度对象的复用，以减少创建的对象的数量。 享元模式的主要思想是共享细粒度对象 ，也就是说如果系统中存在多个相同的对象，那么只需共享一份就可以了，不必每个都去实例化每一个对象，这样来精简内存资源，提升性能和效率。 Fly 意为苍蝇，Flyweight 指轻蝇量级，指代对象粒度很小。 > 注意： 本文用到 ES6 的语法 let/const [http://es6.ruanyifeng.com/#docs/let] 、Class [http://es6.ruanyifeng.com/#docs/class]、Promise [http://es6.ruanyifeng.com/#docs/promise] 等，如果还没接触过可以点击链接稍加学习。 1. 你曾见过的享元模式 我们去驾考的时候，如果给每个考试的人都准备一辆车，那考场就挤爆了，考点都堆不下考试车，因此驾考现场一般会有几辆车给要考试的人依次使用。如果考生人数少，就分别少准备几个自动档和手动档的驾考车，考生多的话就多准备几辆。如果考手动档的考生比较多 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/flyweight-pattern-and-resource-pool/</link>
                <guid isPermaLink="false">5d5e5c83fbfdee429dc5f8f5</guid>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Thu, 22 Aug 2019 09:30:36 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/08/WechatIMG1809.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><strong>享元模式</strong> （Flyweight Pattern）运用共享技术来有效地支持大量细粒度对象的复用，以减少创建的对象的数量。</p><p>享元模式的主要思想是<strong>共享细粒度对象</strong>，也就是说如果系统中存在多个相同的对象，那么只需共享一份就可以了，不必每个都去实例化每一个对象，这样来精简内存资源，提升性能和效率。</p><p>Fly 意为苍蝇，Flyweight 指轻蝇量级，指代对象粒度很小。</p><blockquote><strong>注意：</strong> 本文用到 ES6 的语法 <a href="http://es6.ruanyifeng.com/#docs/let">let/const</a> 、<a href="http://es6.ruanyifeng.com/#docs/class">Class</a>、<a href="http://es6.ruanyifeng.com/#docs/promise">Promise</a> 等，如果还没接触过可以点击链接稍加学习。</blockquote><h2 id="1-">1. 你曾见过的享元模式</h2><p>我们去驾考的时候，如果给每个考试的人都准备一辆车，那考场就挤爆了，考点都堆不下考试车，因此驾考现场一般会有几辆车给要考试的人依次使用。如果考生人数少，就分别少准备几个自动档和手动档的驾考车，考生多的话就多准备几辆。如果考手动档的考生比较多，就多准备几辆手动档的驾考车。</p><p>我们去考四六级的时候（为什么这么多考试？😅），如果给每个考生都准备一个考场，怕是没那么多考场也没有这么多监考老师，因此现实中的大多数情况都是几十个考生共用一个考场。四级考试和六级考试一般同时进行，如果考生考的是四级，那么就安排四级考场，听四级的听力和试卷，六级同理。</p><p>生活中类似的场景还有很多，比如咖啡厅的咖啡口味，餐厅的菜品种类，拳击比赛的重量级等等。</p><p>在类似场景中，这些例子有以下特点：</p><ol><li>目标对象具有一些共同的状态，比如驾考考生考的是自动档还是手动档，四六级考生考的是四级还是六级；</li><li>这些共同的状态所对应的对象，可以被共享出来；</li></ol><h2 id="2-">2. 实例的代码实现</h2><p>首先假设考生的 ID 为奇数则考的是手动档，为偶数则考的是自动档。如果给所有考生都 <code>new</code> 一个驾考车，那么这个系统中就会创建了和考生数量一致的驾考车对象：</p><pre><code>var candidateNum = 10   // 考生数量
var examCarNum = 0      // 驾考车的数量

/* 驾考车构造函数 */
function ExamCar(carType) {
    examCarNum++
    this.carId = examCarNum
    this.carType = carType ? '手动档' : '自动档'
}

ExamCar.prototype.examine = function(candidateId) {
    console.log('考生- ' + candidateId + ' 在' + this.carType + '驾考车- ' + this.carId + ' 上考试')
}

for (var candidateId = 1; candidateId &lt;= candidateNum; candidateId++) {
    var examCar = new ExamCar(candidateId % 2)
    examCar.examine(candidateId)
}

console.log('驾考车总数 - ' + examCarNum)
// 输出: 驾考车总数 - 10</code></pre><p>如果考生很多，那么系统中就会存在更多个驾考车对象实例，假如驾考车对象比较复杂，那么这些新建的驾考车实例就会占用大量内存。这时我们将同种类型的驾考车实例进行合并，手动档和自动档档驾考车分别引用同一个实例，就可以节约大量内存：</p><pre><code>var candidateNum = 10   // 考生数量
var examCarNum = 0      // 驾考车的数量

/* 驾考车构造函数 */
function ExamCar(carType) {
    examCarNum++
    this.carId = examCarNum
    this.carType = carType ? '手动档' : '自动档'
}

ExamCar.prototype.examine = function(candidateId) {
    console.log('考生- ' + candidateId + ' 在' + this.carType + '驾考车- ' + this.carId + ' 上考试')
}

var manualExamCar = new ExamCar(true)
var autoExamCar = new ExamCar(false)

for (var candidateId = 1; candidateId &lt;= candidateNum; candidateId++) {
    var examCar = candidateId % 2 ? manualExamCar : autoExamCar
    examCar.examine(candidateId)
}

console.log('驾考车总数 - ' + examCarNum)
// 输出: 驾考车总数 - 2</code></pre><p>可以看到我们使用 2 个驾考车实例就实现了刚刚 10 个驾考车实例实现的功能。这是仅有 10 个考生的情况，如果有几百上千考生，这时我们节约的内存就比较可观了，这就是享元模式要达到的目的。</p><h2 id="3-">3. 享元模式改进</h2><p>如果你阅读了之前文章关于继承部分的讲解，那么你实际上已经接触到享元模式的思想了。相比于构造函数窃取，在原型链继承和组合继承中，子类通过原型 <code>prototype</code> 来复用父类的方法和属性，如果子类实例每次都创建新的方法与属性，那么在子类实例很多的情况下，内存中就存在有很多重复的方法和属性，即使这些方法和属性完全一样，因此这部分内存完全可以通过复用来优化，这也是享元模式的思想。</p><p>传统的享元模式是将目标对象的状态区分为<strong>内部状态</strong>和<strong>外部状态</strong>，内部状态相同的对象可以被共享出来指向同一个内部状态。正如之前举的驾考和四六级考试的例子中，自动档还是手动档、四级还是六级，就属于驾考考生、四六级考生中的内部状态，对应的驾考车、四六级考场就是可以被共享的对象。而考生的年龄、姓名、籍贯等就属于外部状态，一般没有被共享出来的价值。</p><p>主要的原理可以参看下面的示意图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/image-71.png" class="kg-image" alt="image-71" width="577" height="600" loading="lazy"></figure><p>享元模式的主要思想是细粒度对象的共享和复用，因此对之前的驾考例子，我们可以继续改进一下：</p><ol><li>如果某考生正在使用一辆驾考车，那么这辆驾考车的状态就是被占用，其他考生只能选择剩下未被占用状态的驾考车；</li><li>如果某考生对驾考车的使用完毕，那么将驾考车开回考点，驾考车的状态改为未被占用，供给其他考生使用；</li><li>如果所有驾考车都被占用，那么其他考生只能等待正在使用驾考车的考生使用完毕，直到有驾考车的状态变为未被占用；</li><li>组织单位可以根据考生数量多准备几辆驾考车，比如手动档考生比较多，那么手动档驾考车就应该比自动档驾考车多准备几辆；</li></ol><p>我们可以简单实现一下，为了方便起见，这里就直接使用 ES6 的语法。</p><p>首先创建 3 个手动档驾考车，然后注册 10 个考生参与考试，一开始肯定有 3 个考生同时上车，然后在某个考生考完之后其他考生接着后面考。为了实现这个过程，这里使用了 <code>Promise</code>，考试的考生在 0 到 2 秒后的随机时间考试完毕归还驾考车，其他考生在前面考生考完之后接着进行考试：</p><pre><code>let examCarNum = 0                  // 驾考车总数

/* 驾考车对象 */
class ExamCar {
    constructor(carType) {
        examCarNum++
        this.carId = examCarNum
        this.carType = carType ? '手动档' : '自动档'
        this.usingState = false    // 是否正在使用
    }
    
    /* 在本车上考试 */
    examine(candidateId) {
        return new Promise((resolve =&gt; {
            this.usingState = true
            console.log(`考生- ${ candidateId } 开始在${ this.carType }驾考车- ${ this.carId } 上考试`)
            setTimeout(() =&gt; {
                this.usingState = false
                console.log(`%c考生- ${ candidateId } 在${ this.carType }驾考车- ${ this.carId } 上考试完毕`, 'color:#f40')
                resolve()                       // 0~2秒后考试完毕
            }, Math.random() * 2000)
        }))
    }
}

/* 手动档汽车对象池 */
ManualExamCarPool = {
    _pool: [],                  // 驾考车对象池
    _candidateQueue: [],        // 考生队列
    
    /* 注册考生 ID 列表 */
    registCandidates(candidateList) {
        candidateList.forEach(candidateId =&gt; this.registCandidate(candidateId))
    },
    
    /* 注册手动档考生 */
    registCandidate(candidateId) {
        const examCar = this.getManualExamCar()    // 找一个未被占用的手动档驾考车
        if (examCar) {
            examCar.examine(candidateId)           // 开始考试，考完了让队列中的下一个考生开始考试
              .then(() =&gt; {
                  const nextCandidateId = this._candidateQueue.length &amp;&amp; this._candidateQueue.shift()
                  nextCandidateId &amp;&amp; this.registCandidate(nextCandidateId)
              })
        } else this._candidateQueue.push(candidateId)
    },
    
    /* 注册手动档车 */
    initManualExamCar(manualExamCarNum) {
        for (let i = 1; i &lt;= manualExamCarNum; i++) {
            this._pool.push(new ExamCar(true))
        }
    },
    
    /* 获取状态为未被占用的手动档车 */
    getManualExamCar() {
        return this._pool.find(car =&gt; !car.usingState)
    }
}

ManualExamCarPool.initManualExamCar(3)          // 一共有3个驾考车
ManualExamCarPool.registCandidates([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])  // 10个考生来考试</code></pre><p>在浏览器中运行下试试：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/640.gif" class="kg-image" alt="640" width="640" height="387" loading="lazy"></figure><p>可以看到一个驾考的过程被模拟出来了，这里只简单实现了手动档，自动档驾考场景同理，就不进行实现了。上面的实现还可以进一步优化，比如考生多的时候自动新建驾考车，考生少的时候逐渐减少驾考车，但又不能无限新建驾考车对象，这些情况读者可以自行发挥～</p><p>如果可以将目标对象的内部状态和外部状态区分的比较明显，就可以将内部状态一致的对象很方便地共享出来，但是对 JavaScript 来说，我们并不一定要严格区分内部状态和外部状态才能进行资源共享，比如资源池模式。</p><h2 id="4-">4. 资源池</h2><p>上面这种改进的模式一般叫做<strong>资源池</strong>（Resource Pool），或者叫对象池（Object Pool），可以当作是享元模式的升级版，实现不一样，但是目的相同。资源池一般维护一个装载对象的池子，封装有获取、释放资源的方法，当需要对象的时候直接从资源池中获取，使用完毕之后释放资源等待下次被获取。</p><p>在上面的例子中，驾考车相当于有限资源，考生作为访问者根据资源的使用情况从资源池中获取资源，如果资源池中的资源都正在被占用，要么资源池创建新的资源，要么访问者等待占用的资源被释放。</p><p>资源池在后端应用相当广泛，比如缓冲池、连接池、线程池、字符常量池等场景，前端使用场景不多，但是也有使用，比如有些频繁的 DOM 创建销毁操作，就可以引入对象池来节约一些 DOM 创建损耗。</p><p>下面介绍资源池的几种主要应用。</p><h3 id="4-1-">4.1 线程池</h3><p>以 Node.js 中的线程池为例，Node.js 的 JavaScript 引擎是执行在单线程中的，启动的时候会新建 4 个线程放到线程池中，当遇到一些异步 I/O 操作（比如文件异步读写、DNS 查询等）或者一些 CPU 密集的操作（Crypto、Zlib 模块等）的时候，会在线程池中拿出一个线程去执行。如果有需要，线程池会按需创建新的线程。</p><p>线程池在整个 Node.js 事件循环中的位置可以参照下图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/image-73.png" class="kg-image" alt="image-73" width="800" height="474" loading="lazy"></figure><p>上面这个图就是 Node.js 的事件循环（Event Loop）机制，简单解读一下（扩展视野，不一定需要懂）：</p><ol><li>所有任务都在主线程上执行，形成执行栈（Execution Context Stack）；</li><li>主线程之外维护一个任务队列（Task Queue），接到请求时将请求作为一个任务放入这个队列中，然后继续接收其他请求；</li><li>一旦执行栈中的任务执行完毕，主线程空闲时，主线程读取任务队列中的任务，检查队列中是否有要处理的事件，这时要分两种情况：如果是非 I/O 任务，就亲自处理，并通过回调函数返回到上层调用；如果是 I/O 任务，将传入的参数和回调函数封装成请求对象，并将这个请求对象推入线程池等待执行，主线程则读取下一个任务队列的任务，以此类推处理完任务队列中的任务；</li><li>线程池当线程可用时，取出请求对象执行 I/O 操作，任务完成以后归还线程，并把这个完成的事件放到任务队列的尾部，等待事件循环，当主线程再次循环到该事件时，就直接处理并返回给上层调用；</li></ol><p>感兴趣的同学可以阅读《深入浅出 Nodejs》或 Node.js 依赖的底层库 <a href="http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop">Libuv 官方文档</a> 来了解更多。</p><h3 id="4-2-">4.2 缓存</h3><p>根据二八原则，80% 的请求其实访问的是 20% 的资源，我们可以将频繁访问的资源缓存起来，如果用户访问被缓存起来的资源就直接返回缓存的版本，这就是 Web 开发中经常遇到的<strong>缓存</strong>。</p><p>缓存服务器就是缓存的最常见应用之一，也是复用资源的一种常用手段。缓存服务器的示意图如下：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/image-74.png" class="kg-image" alt="image-74" width="800" height="361" loading="lazy"></figure><p>缓存服务器位于访问者与业务服务器之间，对业务服务器来说，减轻了压力，减小了负载，提高了数据查询的性能。对用户来说，提升了网页打开速度，优化了体验。</p><p>缓存技术用的非常多，不仅仅用在缓存服务器上，浏览器本地也有缓存，查询的 DNS 也有缓存，包括我们的电脑 CPU 上，也有缓存硬件。</p><h3 id="4-3-">4.3 连接池</h3><p>我们知道对数据库进行操作需要先创建一个数据库连接对象，然后通过创建好的数据库连接来对数据库进行 CRUD（增删改查）操作。如果访问量不大，对数据库的 CRUD 操作就不多，每次访问都创建连接并在使用完销毁连接就没什么，但是如果访问量比较多，并发的要求比较高时，频繁创建和销毁连接就比较消耗资源了。</p><p>这时，可以不销毁连接，一直使用已创建的连接，就可以避免频繁创建销毁连接的损耗了。但是有个问题，一个连接同一时间只能做一件事，某使用者（一般是线程）正在使用时，其他使用者就不可以使用了，所以如果只创建一个不关闭的连接显然不符合要求，我们需要创建多个不关闭的连接。</p><p>这就是<strong>连接池</strong>的来源，创建多个数据库连接，当有调用的时候直接在创建好的连接中拿出来使用，使用完毕之后将连接放回去供其他调用者使用。</p><p>我们以 Node.js 中 <code>mysql</code> 模块的连接池应用为例，看看后端一般是如何使用数据库连接池的。在 Node.js 中使用 <code>mysql</code> 创建单个连接，一般这样使用：</p><pre><code>var mysql = require('mysql')

var connection = mysql.createConnection({     // 创建数据库连接
    host: 'localhost',
    user: 'root',         // 用户名
    password: '123456',   // 密码
    database: 'db',       // 指定数据库
    port: '3306'          // 端口号
})

// 连接回调，在回调中增删改查
connection.connect(...)

// 关闭连接
connection.end(...)</code></pre><p>在 Node.js 中使用 mysql 模块的连接池创建连接：</p><pre><code>var mysql = require('mysql')

var pool = mysql.createPool({     // 创建数据库连接池
    host: 'localhost',
    user: 'root',         // 用户名
    password: '123456',   // 密码
    database: 'db',       // 制定数据库
    port: '3306'          // 端口号
})

// 从连接池中获取一个连接，进行增删改查
pool.getConnection(function(err, connection) {
    // ... 数据库操作
    connection.release()  // 将连接释放回连接池中
})

// 关闭连接池
pool.end()</code></pre><p>一般连接池在初始化的时候，都会自动打开 n 个连接，称为<strong>连接预热</strong>。如果这 n 个连接都被使用了，再从连接池中请求新的连接时，会动态地隐式创建额外连接，即<strong>自动扩容</strong>。如果扩容后的连接池一段时间后有不少连接没有被调用，则<strong>自动缩容</strong>，适当释放空闲连接，增加连接池中连接的使用效率。在连接失效的时候，自动<strong>抛弃无效连接</strong>。在系统关闭的时候，自动<strong>释放所有连接</strong>。为了维持连接池的有效运转和避免连接池无限扩容，还会给连接池设置最大最小连接数。</p><p>这些都是连接池的功能，可以看到连接池一般可以根据当前使用情况自动地进行缩容和扩容，来进行连接池资源的最优化，和连接池连接的复用效率最大化。这些连接池的功能点，看着是不是和之前驾考例子的优化过程有点似曾相识呢～</p><p>在实际项目中，除了数据库连接池外，还有 <strong>HTTP 连接池</strong>。使用 HTTP 连接池管理长连接可以复用 HTTP 连接，省去创建 TCP 连接的 3 次握手和关闭 TCP 连接的 4 次挥手的步骤，降低请求响应的时间。</p><p>连接池某种程度也算是一种缓冲池，只不过这种缓冲池是专门用来管理连接的。</p><h3 id="4-4-">4.4 字符常量池</h3><p>很多语言的引擎为了减少字符串对象的重复创建，会在内存中维护有一个特殊的内存，这个内存就叫<strong>字符常量池</strong>。当创建新的字符串时，引擎会对这个字符串进行检查，与字符常量池中已有的字符串进行比对，如果存在有相同内容的字符串，就直接将引用返回，否则在字符常量池中创建新的字符常量，并返回引用。</p><p>类似于 Java、C# 这些语言，都有字符常量池的机制。JavaScript 有多个引擎，以 Chrome 的 V8 引擎为例，V8 在把 JavaScript 编译成字节码过程中就引入了字符常量池这个优化手段，这就是为什么很多 JavaScript 的书籍都提到了 JavaScript 中的字符串具有不可变性，因为如果内存中的字符串可变，一个引用操作改变了字符串的值，那么其他同样的字符串也会受到影响。</p><p>V8 引擎中的字符常量池存在一个变量 <code>string_table_</code> 中，这个变量保存有所有的字符串 <code>All strings are copied here, one after another</code>，地址位于 <a href="https://github.com/v8/v8/blob/7.7.205/src/ast/ast-value-factory.h#L349-L350">v8/src/ast/ast-value-factory.h</a>，核心方法是 <a href="https://github.com/v8/v8/blob/7.7.205/src/ast/ast-value-factory.cc#L275">LookupOrInsert</a>，这个方法给每一个字符串计算出 hash 值，并从 table 中搜索，没有则插入，感兴趣的同学可以自行阅读。</p><p>可以引用《JavaScript 高级程序设计》中的话解释一下：</p><p>ECMAScript 中的字符串是不可变的，也就是说，字符串一旦创建，它们的值就不能改变。要改变某个变量保存的字符串，首先要销毁原来的字符串，然后再用另一个包含新值的字符串填充该变量。</p><p>字符常量池也是复用资源的一种手段，只不过这种手段通常用在编译器的运行过程中，通常开发（搬砖）过程用不到，了解即可。</p><h2 id="5-">5. 享元模式的优缺点</h2><p>享元模式的优点：</p><ol><li>由于<strong>减少了系统中的对象数量</strong>，提高了程序运行效率和性能，精简了内存占用，加快运行速度；</li><li><strong>外部状态相对独立</strong>，不会影响到内部状态，所以享元对象能够在不同的环境被共享；</li></ol><p>享元模式的缺点：</p><ol><li>引入了共享对象，使对象结构变得复杂；</li><li>共享对象的创建、销毁等需要维护，带来额外的复杂度（如果需要把共享对象维护起来的话）；</li></ol><h2 id="6-">6. 享元模式的适用场景</h2><ol><li>如果一个程序中大量使用了相同或相似对象，那么可以考虑引入享元模式；</li><li>如果使用了大量相同或相似对象，并造成了比较大的内存开销；</li><li>对象的大多数状态可以被转变为外部状态；</li><li>剥离出对象的外部状态后，可以使用相对较少的共享对象取代大量对象；</li></ol><p>在一些程序中，如果引入享元模式对系统的性能和内存的占用影响不大时，比如目标对象不多，或者场景比较简单，则不需要引入，以免适得其反。</p><h2 id="7-">7. 其他相关模式</h2><p>享元模式和单例模式、工厂模式、组合模式、策略模式、状态模式等等经常会一起使用。</p><h3 id="7-1-">7.1 享元模式和工厂模式、单例模式</h3><p>在区分出不同种类的外部状态后，创建新对象时需要选择不同种类的共享对象，这时就可以使用工厂模式来提供共享对象，在共享对象的维护上，经常会采用单例模式来提供单实例的共享对象。</p><h3 id="7-2-">7.2 享元模式和组合模式</h3><p>在使用工厂模式来提供共享对象时，比如某些时候共享对象中的某些状态就是对象不需要的，可以引入组合模式来提升自定义共享对象的自由度，对共享对象的组成部分进一步归类、分层，来实现更复杂的多层次对象结构，当然系统也会更难维护。</p><h3 id="7-3-">7.3 享元模式和策略模式</h3><p>策略模式中的策略属于一系列功能单一、细粒度的细粒度对象，可以作为目标对象来考虑引入享元模式进行优化，但是前提是这些策略是会被频繁使用的，如果不经常使用，就没有必要了。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ GraphQL 入门看这篇就够了 ]]>
                </title>
                <description>
                    <![CDATA[ 本文首先介绍了 GraphQL，再通过 MongoDB + graphql + graph-pack 的组合实战应用 GraphQL，详细阐述如何使用 GraphQL 来进行增删改查和数据订阅推送，并附有使用示例，边用边学印象深刻。 如果希望将 GraphQL 应用到前后端分离的生产环境，请期待后续文章。 本文实例代码：GitHub [https://github.com/SHERlocked93/graphql-demo] 0. 什么是 GraphQL GraphQL 是一种面向数据的 API 查询风格。 传统的 API 拿到的是前后端约定好的数据格式，GraphQL 对 API 中的数据提供了一套易于理解的完整描述，客户端能够准确地获得它需要的数据，没有任何冗余，也让 API 更容易地随着时间推移而演进，还能用于构建强大的开发者工具。 1. 概述 前端的开发随着 SPA 框架全面普及，组件化开发也随之成为大势所趋，各个组件分别管理着各自的状态，组件化给前端仔带来便利的同时也带来了一些烦恼。比如，组件需要负责把异步请求的状态分发给子组件或通知给父组件，这个过程中，由组件间通 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/a-detailed-guide-to-graphql/</link>
                <guid isPermaLink="false">5d53cf65fbfdee429dc5f76b</guid>
                
                <dc:creator>
                    <![CDATA[ SHERlocked93 ]]>
                </dc:creator>
                <pubDate>Wed, 14 Aug 2019 09:23:15 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/08/123.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文首先介绍了 GraphQL，再通过 MongoDB + graphql + graph-pack 的组合实战应用 GraphQL，详细阐述如何使用 GraphQL 来进行增删改查和数据订阅推送，并附有使用示例，边用边学印象深刻。</p><p>如果希望将 GraphQL 应用到前后端分离的生产环境，请期待后续文章。</p><p>本文实例代码：<a href="https://github.com/SHERlocked93/graphql-demo">GitHub</a></p><h2 id="0-graphql">0. 什么是 GraphQL</h2><p>GraphQL 是一种面向数据的 API 查询风格。</p><p>传统的 API 拿到的是前后端约定好的数据格式，GraphQL 对 API 中的数据提供了一套易于理解的完整描述，客户端能够准确地获得它需要的数据，没有任何冗余，也让 API 更容易地随着时间推移而演进，还能用于构建强大的开发者工具。</p><h2 id="1-">1. 概述</h2><p>前端的开发随着 SPA 框架全面普及，组件化开发也随之成为大势所趋，各个组件分别管理着各自的状态，组件化给前端仔带来便利的同时也带来了一些烦恼。比如，组件需要负责把异步请求的状态分发给子组件或通知给父组件，这个过程中，由组件间通信带来的结构复杂度、来源不明的数据源、不知从何订阅的数据响应会使得数据流变得杂乱无章，也使得代码可读性变差，以及可维护性的降低，为以后项目的迭代带来极大困难。</p><p>试想一下你都开发完了，产品告诉你要大改一番，从接口到组件结构都得改，后端也骂骂咧咧不愿配合让你从好几个 API 里取数据自己组合，这酸爽！</p><p>在一些产品链复杂的场景，后端需要提供对应 WebApp、WebPC、APP、小程序、快应用等各端 API，此时 API 的粒度大小就显得格外重要，粗粒度会导致移动端不必要的流量损耗，细粒度则会造成函数爆炸 (Function Explosion)；在此情景下 Facebook 的工程师于 2015 年开源了 <strong>GraphQL</strong> 规范，让前端自己描述自己希望的数据形式，服务端则返回前端所描述的数据结构。</p><p>简单使用可以参照下面这个图：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/123.gif" class="kg-image" alt="123" width="476" height="349" loading="lazy"></figure><p>比如前端希望返回一个 ID 为 <code>233</code> 的用户的名称和性别，并查找这个用户的前十个雇员的名字和 Email，再找到这个人父亲的电话，和这个父亲的狗的名字（别问我为什么有这么奇怪的查找），那么我们可以通过 GraphQL 的一次 query 拿到全部信息，无需从好几个异步 API 里面来回找：</p><pre><code>query {
  user (id : "233") {
    name
    gender
    employee (first: 10) {
      name
      email
    }
    father {
      telephone
      dog {
          name
      }
    }
  }
}</code></pre><p>返回的数据格式则刚好是前端提供的数据格式，不多不少，是不是心动了？</p><h2 id="2-">2. 几个重要概念</h2><p>这里先介绍几个对理解 GraphQL 比较重要的概念，其他类似于指令、联合类型、内联片段等更复杂的用法，参考 <a href="https://graphql.cn/learn/queries/">GraphQL</a> 官网文档 ~</p><h3 id="2-1-operation-type">2.1 操作类型 Operation Type</h3><p>GraphQL 的操作类型可以是 <code>query</code>、<code>mutation</code> 或 <code>subscription</code>，描述客户端希望进行什么样的操作</p><ol><li>query 查询：获取数据，比如查找，CRUD 中的 R</li><li>mutation 变更：对数据进行变更，比如增加、删除、修改，CRUD 中的 CUD</li><li>substription 订阅：当数据发生更改，进行消息推送</li></ol><p>这些操作类型都将在后文实际用到，比如这里进行一个查询操作</p><pre><code>query {
  user { id }
}</code></pre><h3 id="2-2-object-type-scalar-type">2.2 对象类型和标量类型 Object Type &amp; Scalar Type</h3><p>如果一个 GraphQL 服务接受到了一个 <code>query</code>，那么这个 <code>query</code> 将从 <code>Root Query</code> 开始查找，找到对象类型（Object Type）时则使用它的解析函数 Resolver 来获取内容，如果返回的是对象类型则继续使用解析函数获取内容，如果返回的是标量类型（Scalar Type）则结束获取，直到找到最后一个标量类型。</p><ol><li>对象类型：用户在 schema 中定义的 <code>type</code></li><li>标量类型：GraphQL 中内置有一些标量类型 <code>String</code>、<code>Int</code>、<code>Float</code>、<code>Boolean</code>、<code>ID</code>，用户也可以定义自己的标量类型</li></ol><p>比如在 Schema 中声明：</p><pre><code>type User {
  name: String!
  age: Int
}</code></pre><p>这个 <code>User</code> 对象类型有两个字段，name 字段是一个为 <code>String</code> 的非空标量，age 字段为一个 <code>Int</code> 的可空标量。</p><h3 id="2-3-schema">2.3 模式 Schema</h3><p>如果你用过 MongoOSE，那你应该对 Schema 这个概念很熟悉，翻译过来是『模式』。</p><p>它定义了字段的类型、数据的结构，描述了接口数据请求的规则，当我们进行一些错误的查询的时候 GraphQL 引擎会负责告诉我们哪里有问题，和详细的错误信息，对开发调试十分友好。</p><p>Schema 使用一个简单的强类型模式语法，称为模式描述语言（Schema Definition Language, SDL），我们可以用一个真实的例子来展示一下一个真实的 Schema 文件是怎么用 SDL 编写的：</p><pre><code># src/schema.graphql

# Query 入口
type Query {
    hello: String
    users: [User]!
    user(id: String): [User]!
}

# Mutation 入口
type Mutation {
    createUser(id: ID!, name: String!, email: String!, age: Int,gender: Gender): User!
    updateUser(id: ID!, name: String, email: String, age: Int, gender: Gender): User!
    deleteUser(id: ID!): User
}

# Subscription 入口
type Subscription {
    subsUser(id: ID!): User
}

type User implements UserInterface {
    id: ID!
    name: String!
    age: Int
    gender: Gender
    email: String!
}

# 枚举类型
enum Gender {
    MAN
    WOMAN
}

# 接口类型
interface UserInterface {
    id: ID!
    name: String!
    age: Int
    gender: Gender
}</code></pre><p>这个简单的 Schema 文件从 Query、Mutation、Subscription 入口开始定义了各个对象类型或标量类型，这些字段的类型也可能是其他的对象类型或标量类型，组成一个树形的结构，而用户在向服务端发送请求的时候，沿着这个树选择一个或多个分支就可以获取多组信息。</p><p>注意：<strong>在 Query 查询字段时，是并行执行的，而在 Mutation 变更的时候，是线性执行，一个接着一个</strong>，防止同时变更带来的竞态问题，比如说我们在一个请求中发送了两个 Mutation，那么前一个将始终在后一个之前执行。</p><h3 id="2-4-resolver">2.4 解析函数 Resolver</h3><p>前端请求信息到达后端之后，需要由解析函数 <a href="https://link.juejin.im?target=https%3A%2F%2Fwww.apollographql.com%2Fdocs%2Fgraphql-tools%2Fresolvers.html" rel="nofollow noopener noreferrer">Resolver</a> 来提供数据，比如这样一个 Query：</p><pre><code>query {
  hello
}</code></pre><p>那么同名的解析函数应该是这样的</p><pre><code>Query: {
  hello (parent, args, context, info) {
    return ...
  }
}</code></pre><p>解析函数接受四个参数，分别为</p><ol><li><code>parent</code>：当前上一个解析函数的返回值</li><li><code>args</code>：查询中传入的参数</li><li><code>context</code>：提供给所有解析器的上下文信息</li><li><code>info</code>：一个保存与当前查询相关的字段特定信息以及 schema 详细信息的值</li></ol><p>解析函数的返回值可以是一个具体的值，也可以是 Promise 或 Promise 数组。</p><p>一些常用的解决方案如 Apollo 可以帮省略一些简单的解析函数，比如一个字段没有提供对应的解析函数时，会从上层返回对象中读取和返回与这个字段同名的属性。</p><h3 id="2-5-">2.5 请求格式</h3><p>GraphQL 最常见的是通过 HTTP 来发送请求，那么如何通过 HTTP 来进行 GraphQL 通信呢</p><p>举个栗子，如何通过 Get/Post 方式来执行下面的 GraphQL 查询呢</p><pre><code>query {
  me {
    name
  }
}</code></pre><p>Get 是将请求内容放在 URL 中，Post 是在 <code>content-type: application/json</code> 情况下，将 JSON 格式的内容放在请求体里</p><pre><code># Get 方式
http://myapi/graphql?query={me{name}}

# Post 方式的请求体
{
  "query": "...",
  "operationName": "...",
  "variables": { "myVariable": "someValue", ... }
}</code></pre><p>返回的格式一般也是 JSON 体</p><pre><code># 正确返回
{
  "data": { ... }
}

# 执行时发生错误
{
  "errors": [ ... ]
}</code></pre><p>如果执行时发生错误，则 errors 数组里有详细的错误信息，比如错误信息、错误位置、抛错现场的调用堆栈等信息，方便进行定位。</p><h2 id="3-">3. 实战</h2><p>这里使用 <a href="https://docs.mongodb.com/v3.6/">MongoDB</a> + <a href="https://github.com/glennreyes/graphpack">graph-pack</a> 进行一下简单的实战，并在实战中一起学习一下，详细代码参见 <a href="https://github.com/SHERlocked93/graphql-demo">GitHub</a>。</p><p>MongoDB 是一个使用的比较多的 NoSQL，可以方便的在社区找到很多现成的解决方案，报错了也容易找到解决方法。</p><p>graph-pack 是集成了 Webpack + Express + Prisma + Babel + Apollo-server + Websocket 的支持热更新的零配置 GraphQL 服务环境，这里将其用来演示 GraphQL 的使用。</p><h3 id="3-1-">3.1 环境部署</h3><p>首先我们把 MongoDB 启起来，这个过程就不赘述了，网上很多教程；</p><p>搭一下 graph-pack 的环境。</p><pre><code>npm i -S graphpack</code></pre><p>在 <code>package.json</code> 的 <code>scripts</code> 字段加上：</p><pre><code>"scripts": {
    "dev": "graphpack",
    "build": "graphpack build"
}</code></pre><p>创建文件结构：</p><pre><code>.
├── src
│&nbsp;&nbsp; ├── db					// 数据库操作相关
│&nbsp;&nbsp; │&nbsp;&nbsp; ├── connect.js		// 数据库操作封装
│&nbsp;&nbsp; │&nbsp;&nbsp; ├── index.js		// DAO 层
│&nbsp;&nbsp; │&nbsp;&nbsp; └── setting.js		// 配置
│&nbsp;&nbsp; ├── resolvers			// resolvers
│&nbsp;&nbsp; │&nbsp;&nbsp; └── index.js
│&nbsp;&nbsp; └── schema.graphql		// schema
└── package.json</code></pre><p>这里的 <code>schema.graphql</code> 是 2.3 节的示例代码，其他实现参见 &nbsp;<a href="https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2FSHERlocked93%2Fgraphql-demo" rel="nofollow noopener noreferrer">Github</a>，主要关注 <code>src/db</code> &nbsp;、<code>src/resolvers</code>、<code>src/schema.graphql</code> 这三个地方。</p><ol><li><code>src/db</code>：数据库操作层，包括 DAO 层和 Service 层（如果对分层不太了解可以看一下最后一章）</li><li><code>src/resolvers</code>：Resolver 解析函数层，给 GraphQL 的 Query、Mutation、Subscription 请求提供 resolver 解析函数</li><li><code>src/schema.graphql</code>：Schema 层</li></ol><p>然后 <code>npm run dev</code> ，浏览器打开 <code>http://localhost:4000/</code> 就可以使用 GraphQL Playground 开始调试了，左边是请求信息栏，左下是请求参数栏和请求头设置栏，右边是返回参数栏，详细用法可以参考 <a href="https://www.prisma.io/blog/introducing-graphql-playground-f1e0a018f05d/">Prisma 文档</a>。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/image-54.png" class="kg-image" alt="image-54" width="1280" height="747" loading="lazy"></figure><h3 id="3-2-query">3.2 Query</h3><p>首先我们来试试 <code>hello world</code>，我们在 <code>schema.graphql</code> 中写上 Query 的一个入口 <code>hello</code>，它接受 String 类型的返回值。</p><pre><code># src/schema.graphql

# Query 入口
type Query {
    hello: String
}</code></pre><p>在 <code>src/resolvers/index.js</code> 中补充对应的 Resolver，这个 Resolver 比较简单，直接返回的 String。</p><pre><code>// src/resolvers/index.js

export default {
    Query: {
        hello: () =&gt; 'Hello world!'
    }
}</code></pre><p>我们在 Playground 中进行 Query。</p><pre><code># 请求
query {
  hello
}

# 返回值
{
  "data": {
    "hello": "Hello world!"
  }
}</code></pre><p>Hello world 总是如此愉快，下面我们来进行稍微复杂一点的查询。</p><p>查询入口 <code>users</code> 查找所有用户列表，返回一个不可空但长度可以为 0 的数组，数组中如果有元素，则必须为 User 类型；另一个查询入口 <code>user</code> 接受一个字符串，查找 ID 为这个字符串的用户，并返回一个 User 类型的可空字段。</p><pre><code># src/schema.graphql

# Query 入口
type Query {
    user(id: String): User
    users: [User]!
}

type User {
    id: ID!
    name: String!
    age: Int
    email: String!
}</code></pre><p>增加对应的 Resolver。</p><pre><code>// src/resolvers/index.js

import Db from '../db'

export default {
    Query: {
        user: (parent, { id }) =&gt; Db.user({ id }),
        users: (parent, args) =&gt; Db.users({})
    }
}</code></pre><p>这里的两个方法 <code>Db.user</code>、<code>Db.users</code> 分别是查找对应数据的函数，返回的是 Promise，如果这个 Promise 被 resolve，那么传给 resolve 的数据将被作为结果返回。</p><p>然后进行一次查询就可以查找我们所希望的所有信息。</p><pre><code># 请求
query {
  user(id: "2") {
    id
    name
    email
    age
  }
  users {
    id
    name
  }
}

# 返回值
{
  "data": {
    "user": {
      "id": "2",
      "name": "李四",
      "email": "mmmmm@qq.com",
      "age": 18
    },
    "users": [{
        "id": "1",
        "name": "张三"
      },{
        "id": "2",
        "name": "李四"
      }]
  }
}
复制代码</code></pre><p>注意这里，返回的数组只希望拿到 <code>id</code>、<code>name</code> 这两个字段，因此 GraphQL 并没有返回多余的数据，怎么样，是不是很贴心呢？</p><h3 id="3-3-mutation">3.3 Mutation</h3><p>知道如何查询数据，还得了解增加、删除、修改，毕竟这是 CRUD 工程师必备的几板斧，不过这里只介绍比较复杂的修改，另外两个方法可以看一下 <a href="https://github.com/SHERlocked93/graphql-demo">GitHub</a> 上。</p><pre><code># src/schema.graphql

# Mutation 入口
type Mutation {
    updateUser(id: ID!, name: String, email: String, age: Int): User!
}

type User {
    id: ID!
    name: String!
    age: Int
    email: String!
}</code></pre><p>同理，Mutation 也需要 Resolver 来处理请求。</p><pre><code>// src/resolvers/index.js

import Db from '../db'

export default {
    Mutation: {
        updateUser: (parent, { id, name, email, age }) =&gt; Db.user({ id })
            .then(existUser =&gt; {
                if (!existUser)
                    throw new Error('没有这个id的人')
                return existUser
            })
            .then(() =&gt; Db.updateUser({ id, name, email, age }))
    }
}</code></pre><p>Mutation 入口 updateUser 拿到参数之后首先进行一次用户查询，如果没找到则抛错，这个错将作为 error 信息返回给用户，<code>Db.updateUser</code> 这个函数返回的也是 Promise，不过是将改变之后的信息返回。</p><pre><code># 请求
mutation UpdataUser ($id: ID!, $name: String!, $email: String!, $age: Int) {
  updateUser(id: $id, name: $name, email: $email, age: $age) {
    id
    name
    age
  }
}

# 参数
{"id": "2", "name": "王五", "email": "xxxx@qq.com", "age": 19}

# 返回值
{
  "data": {
    "updateUser": {
      "id": "2",
      "name": "王五",
      "age": 19
    }
  }
}</code></pre><p>这样完成了对数据的更改，且拿到了更改后的数据，并给定希望的字段。</p><h3 id="3-4-subscription">3.4 Subscription</h3><p>GraphQL 还有一个有意思的地方就是它可以进行数据订阅，当前端发起订阅请求之后，如果后端发现数据改变，可以给前端推送实时信息，我们用一下看看。</p><p>照例，在 Schema 中定义 Subscription 的入口。</p><pre><code># src/schema.graphql

# Subscription 入口
type Subscription {
    subsUser(id: ID!): User
}

type User {
    id: ID!
    name: String!
    age: Int
    email: String!
}</code></pre><p>补充上它的 Resolver。</p><pre><code>// src/resolvers/index.js

import Db from '../db'

const { PubSub, withFilter } = require('apollo-server')
const pubsub = new PubSub()
const USER_UPDATE_CHANNEL = 'USER_UPDATE'

export default {
    Mutation: {
        updateUser: (parent, { id, name, email, age }) =&gt; Db.user({ id })
            .then(existUser =&gt; {
                if (!existUser)
                    throw new Error('没有这个id的人')
                return existUser
            })
            .then(() =&gt; Db.updateUser({ id, name, email, age }))
            .then(user =&gt; {
                pubsub.publish(USER_UPDATE_CHANNEL, { subsUser: user })
                return user
            })
    },
    Subscription: {
        subsUser: {
            subscribe: withFilter(
                (parent, { id }) =&gt; pubsub.asyncIterator(USER_UPDATE_CHANNEL),
                (payload, variables) =&gt; payload.subsUser.id === variables.id
            ),
            resolve: (payload, variables) =&gt; {
                console.log('🚢 接收到数据： ', payload)
            }
        }
    }
}</code></pre><p>这里的 <code>pubsub</code> 是 apollo-server 里负责订阅和发布的类，它在接受订阅时提供一个异步迭代器，在后端觉得需要发布订阅的时候向前端发布 payload。<code>withFilter</code> 的作用是过滤掉不需要的订阅消息，详细用法参照<a href="https://www.apollographql.com/docs/apollo-server/features/subscriptions/#subscription-filters">订阅过滤器</a>。</p><p>首先我们发布一个订阅请求。</p><pre><code># 请求
subscription subsUser($id: ID!) {
  subsUser(id: $id) {
    id
    name
    age
    email
  }
}

# 参数
{ "id": "2" }</code></pre><p>我们用刚刚的数据更新操作来进行一次数据的更改，然后我们将获取到并打印出 <code>pubsub.publish</code> 发布的 payload，这样就完成了数据订阅。</p><p>在 graph-pack 中数据推送是基于 websocket 来实现的，可以在通信的时候打开 Chrome DevTools 看一下。</p><h2 id="4-">4. 总结</h2><p>目前前后端的结构大概如下图。后端通过 DAO 层与数据库连接实现数据持久化，服务于处理业务逻辑的 Service 层，Controller 层接受 API 请求调用 Service 层处理并返回；前端通过浏览器 URL 进行路由命中获取目标视图状态，而页面视图是由组件嵌套组成，每个组件维护着各自的组件级状态，一些稍微复杂的应用还会使用集中式状态管理的工具，比如 Vuex、Redux、Mobx 等。前后端只通过 API 来交流，这也是现在前后端分离开发的基础。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2019/08/image-57.png" class="kg-image" alt="image-57" width="689" height="606" loading="lazy"></figure><p>如果使用 GraphQL，那么后端将不再产出 API，而是将 Controller 层维护为 Resolver，和前端约定一套 Schema，这个 Schema 将用来生成接口文档，前端直接通过 Schema 或生成的接口文档来进行自己期望的请求。</p><p>经过几年一线开发者的填坑，已经有一些不错的<a href="https://github.com/chentsulin/awesome-graphql">工具链</a>可以使用于开发与生产，很多语言也提供了对 GraphQL 的支持，比如 JavaScript/Nodejs、Java、PHP、Ruby、Python、Go、C# 等。</p><p>一些比较有名的公司比如 Twitter、IBM、Coursera、Airbnb、Facebook、Github、携程等，内部或外部 API 从 RESTful 转为了 GraphQL 风格，特别是 Github，它的 v4 版外部 API 只使用 GraphQL。据<a href="https://www.zhihu.com/question/38596306/answer/207364003">一位在 Twitter 工作的大佬</a>说硅谷不少一线二线的公司都在想办法转到 GraphQL 上，但是同时也说了 GraphQL 还需要时间发展，因为将它使用到生产环境需要前后端大量的重构，这无疑需要高层的推动和决心。</p><p>正如<a href="https://www.zhihu.com/question/38596306/answer/79714979">尤雨溪所说</a>，为什么 GraphQL 两三年前没有广泛使用起来呢，可能有下面两个原因：</p><ol><li>GraphQL 的 field resolve 如果按照 naive 的方式来写，每一个 field 都对数据库直接跑一个 query，会产生大量冗余 query，虽然网络层面的请求数被优化了，但数据库查询可能会成为性能瓶颈，这里面有很大的优化空间，但并不是那么容易做。FB 本身没有这个问题，因为他们内部数据库这一层也是抽象掉的，写 GraphQL 接口的人不需要顾虑 query 优化的问题。</li><li>GraphQL 的利好主要是在于前端的开发效率，但落地却需要服务端的全力配合。如果是小公司或者整个公司都是全栈，那可能可以做，但在很多前后端分工比较明确的团队里，要推动 GraphQL 还是会遇到各种协作上的阻力。</li></ol><p>大约可以概括为性能瓶颈和团队分工的原因，希望随着社区的发展，基础设施的完善，会渐渐有完善的解决方案提出，让广大前后端开发者们可以早日用上此利器。</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
