<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>MARKSZのBlog</title>
  
  <subtitle>Do what you love,Love what you do</subtitle>
  <link href="https://molunerfinn.com/atom.xml" rel="self"/>
  
  <link href="https://molunerfinn.com/"/>
  <updated>2026-03-08T01:14:37.986Z</updated>
  <id>https://molunerfinn.com/</id>
  
  <author>
    <name>Molunerfinn</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>一次由 pnpm 小版本差异引发的 PicGo 2.5.3 发布事故</title>
    <link href="https://molunerfinn.com/picgo-2-5-3-release-pitfall/"/>
    <id>https://molunerfinn.com/picgo-2-5-3-release-pitfall/</id>
    <published>2026-03-08T09:05:00.000Z</published>
    <updated>2026-03-08T01:14:37.986Z</updated>
    
    <content type="html"><![CDATA[<p>最近 PicGo 2.5.3 发出去没多久，就有人来报了个很怪的问题：应用能装上，但一启动就直接报错，核心信息是 <code>Cannot find module &#39;esprima&#39;</code>。</p><p><img src="https://pics.molunerfinn.com/blog/20260308085649899.png" alt="PicGo launch error"></p><p>最开始我还以为这事大概率是 macOS arm64 打包出了岔子。毕竟最早的反馈来自 mac 用户，而且 2.5.2 明明还是好的，2.5.3 看起来也没怎么动依赖。结果没过多久，我自己在 Windows 上也复现了。到这一步就知道，事情没那么简单了。</p><p>先说结论：这次不是某个依赖“没装上”，也不是代码里手滑删了 <code>esprima</code>。真正的问题出在发布链路上：GitHub Actions 里 <code>pnpm/action-setup</code> 用的是浮动的 <code>version: 10</code>，PicGo <code>2.5.2</code> 和 <code>2.5.3</code> 实际用到的 <code>pnpm</code> 小版本并不一样。偏偏当前 <code>electron-builder</code> 收集生产依赖的方式，又刚好会吃这个差异。</p><span id="more"></span><h2 id="问题是怎么暴露出来的"><a href="#问题是怎么暴露出来的" class="headerlink" title="问题是怎么暴露出来的"></a>问题是怎么暴露出来的</h2><p>这次的用户反馈在这个 issue：</p><blockquote><p><a href="https://github.com/Molunerfinn/PicGo/issues/1399">Molunerfinn&#x2F;PicGo#1399</a></p></blockquote><p>报错现象很直接，就是安装后启动失败，提示缺少 <code>esprima</code>。</p><p>一开始我有两个很自然的判断：</p><ol><li>这是不是某个平台特有的问题？</li><li>这是不是 <code>2.5.3</code> 里某个依赖偷偷变了？</li></ol><p>结果这两个判断最后都只对了一半。</p><p>平台特有这条很快就被排除了，因为 Windows 也中了。至于“依赖偷偷变了”，从 <code>package.json</code> 表面看，确实没有什么特别大的变化，至少不是那种一眼就能看出会把安装包搞炸的改动。</p><p>更迷惑的是，我本地把 <code>node_modules</code> 删掉，重新安装依赖，再重新构建，打出来的包居然是正常的。</p><p>这就很烦。因为这意味着：</p><ul><li>代码仓库本身看起来没问题</li><li>本地 fresh install 看起来也没问题</li><li>线上发布出来的安装包却有问题</li></ul><p>这种情况我以前也遇到过一次，所以我第一反应不是业务代码，而是打包链路或者依赖问题。</p><h2 id="为什么这次特别难查"><a href="#为什么这次特别难查" class="headerlink" title="为什么这次特别难查"></a>为什么这次特别难查</h2><p>真正让我卡住的，不是报错本身，而是“本地正常，远端不正常”。</p><p>我本机最开始用的是 <code>pnpm 10.25.0</code>，而 GitHub Actions workflow 里写的是：</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">pnpm/action-setup@v4</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">version:</span> <span class="number">10</span></span><br></pre></td></tr></table></figure><p>这个写法看起来像“固定到了 pnpm 10”，实际上并没有固定到具体小版本。它只保证你拿到的是某个 <code>10.x</code>，至于到底是 <code>10.25.0</code>、<code>10.29.2</code> 还是 <code>10.30.3</code>，要看当时 action 解析到了什么版本。</p><p>我当时已经隐约觉得，八成和 <code>pnpm</code> 版本差异有关。只是这个判断很模糊，我只能感觉“版本可能不一样”，但具体不一样在哪里、为什么会影响到最终安装包，我其实说不上来。</p><p>后来我索性让 AI 帮我一起排查这条链路：一边对比本地和线上 release 产物，一边翻 GitHub Actions 日志，再顺着 <code>electron-builder</code> 的依赖收集逻辑继续往下查。也是从这时候开始，我不再盯着源码和 lockfile 打转，而是直接去看最终产物。</p><h2 id="第一步：先看最终产物"><a href="#第一步：先看最终产物" class="headerlink" title="第一步：先看最终产物"></a>第一步：先看最终产物</h2><p>后面我让 AI 做了一件很朴素的事：直接去看打出来的安装包内容。</p><p>本地构建出来的 <code>app.asar</code> 里，<code>comment-json</code>、<code>esprima</code>、<code>array-timsort</code> 都在。</p><p>但已经发布出去的 <code>2.5.3</code> 安装包里，情况就不一样了：</p><ul><li><code>comment-json</code> 在</li><li><code>picgo</code> 在</li><li><code>esprima</code> 不在</li><li><code>array-timsort</code> 也不在</li></ul><p>到这里，问题就清楚很多了：不是程序运行时“找错路径”，而是这些依赖在打包阶段就根本没有被带进最终产物。</p><p>也就是说，报错只是结果。真正出问题的是依赖收集。</p><h2 id="第二步：依赖明明装了，为什么还是没进包"><a href="#第二步：依赖明明装了，为什么还是没进包" class="headerlink" title="第二步：依赖明明装了，为什么还是没进包"></a>第二步：依赖明明装了，为什么还是没进包</h2><p>这部分是这次踩坑里最关键的一点。</p><p>我一开始也有点想不通：既然 <code>pnpm install</code> 都成功了，<code>node_modules</code> 里也确实能看到 <code>esprima</code>，那为什么 <code>electron-builder</code> 还会漏掉它？</p><p>后面 AI 自己定位问题，翻了 <code>electron-builder</code> 的实现才发现，它并不是傻乎乎把磁盘上的 <code>node_modules</code> 整个搬进去。</p><p>对于 <code>pnpm</code> 项目，它会先跑一条命令去拿“生产依赖树”：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pnpm list --prod --json --depth Infinity</span><br></pre></td></tr></table></figure><p>然后基于这棵树去决定哪些依赖应该进 <code>app.asar</code>。</p><p>前面看 <code>app.asar</code>，只是确认“哪些依赖没被打进去”；真正把原因讲明白，还得继续往下查。后面我让 AI 帮我把 <code>app.asar</code>、<code>pnpm list</code> 输出、Actions 日志和 <code>electron-builder</code> 源码串起来之后，问题才真正坐实。</p><p>我把 <code>pnpm 10.29.2</code> 和 <code>pnpm 10.30.3</code> 的输出拿来对比，结果非常扎眼：</p><p>在 <code>10.29.2</code> 下，<code>comment-json</code> 节点下面能看到这些依赖：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">array-timsort</span><br><span class="line">core-util-is</span><br><span class="line">esprima</span><br></pre></td></tr></table></figure><p>但在 <code>10.30.3</code> 下，同样的命令，同样的仓库，同样的 lockfile，<code>comment-json</code> 返回出来的依赖列表是空的。</p><p>注意，这不代表这些包没被安装。</p><p>磁盘上它们还在，<code>node_modules/esprima</code> 也能找到。只是 <code>pnpm list --prod --json --depth Infinity</code> 给 <code>electron-builder</code> 的那棵树里，已经不再把它们挂到 <code>comment-json</code> 下面了。<code>electron-builder</code> 又正好是按这棵树来拷贝依赖，于是最后这些包就没进安装包。</p><h2 id="第三步：把本地和远端的版本差异对上"><a href="#第三步：把本地和远端的版本差异对上" class="headerlink" title="第三步：把本地和远端的版本差异对上"></a>第三步：把本地和远端的版本差异对上</h2><p>这也是这次最容易让人误判的地方。</p><p>后面去翻 release run 日志，才发现：</p><ul><li><code>v2.5.2</code> 的线上构建实际使用的是 <code>pnpm 10.29.2</code></li><li><code>v2.5.3</code> 的线上构建实际使用的是 <code>pnpm 10.30.3</code></li><li>而我本地构建使用的是 <code>pnpm 10.25.0</code></li></ul><p>到这里整个链路终于对上了：</p><ol><li>本地因为不是 <code>10.30.3</code>，所以打包正常</li><li><code>2.5.2</code> 线上还没漂到有问题的小版本，所以正常</li><li><code>2.5.3</code> 线上漂到了 <code>10.30.3</code>，刚好触发了这个问题</li></ol><p>我一开始真没往 <code>pnpm</code> 小版本漂移上想。毕竟大家平时更警惕的是大版本升级，小版本总让人下意识觉得“应该没事”。结果这次偏偏就是小版本把我绊了一下。</p><p>更巧的是，后面我又去翻了一圈，发现 <code>pnpm</code> 官方那边也已经有人提了类似问题：</p><blockquote><p><a href="https://github.com/pnpm/pnpm/issues/10601">pnpm&#x2F;pnpm#10601</a></p></blockquote><p>看到这个 issue 的时候，我心里反而踏实了一点。至少可以确认，这不是我仓库里某个奇怪配置独有的玄学问题，而是确实有人在真实项目里踩到了同一类坑。</p><h2 id="最后的修复方式"><a href="#最后的修复方式" class="headerlink" title="最后的修复方式"></a>最后的修复方式</h2><p>最后的处理其实不复杂，核心就一条：不要再让 CI 里的 <code>pnpm</code> 用大版本了。</p><p>我把 workflow 里的 <code>pnpm</code> 版本从浮动的 <code>10</code> 改成了明确的 <code>10.29.2</code>。之所以选这个版本，一方面，<code>2.5.2</code> 的线上构建已经验证过它是正常的；另一方面，我后来去看 <code>pnpm</code> 那个 issue 时，也看到有人提到 <code>10.29.2</code> 是最后一个还能稳定构建的版本。所以最后把它锁在 <code>10.29.2</code>，算是一个相对稳妥的选择。</p><p>本来 AI 给我一开始的建议是让我显式补充 <code>esprima</code> 这个依赖，但我觉得这只是治标不治本。还好最后让 AI 继续深挖，才把真正的根因找出来了。之前其实 PicGo 也有过一次这种依赖问题，但是当时试了好久就是没找到根因，后来换了不知道多少依赖才构建成功。现在想想，如果当时能直接锁定 <code>pnpm</code> 版本，可能就不至于绕那么远了。</p><p>顺手我还给 workflow 加了一个 <code>release_tag</code> 手动参数。这样如果以后要从 <code>dev</code> 分支重新出一版某个已有 tag 的产物，就不用为了重新发包再去硬 bump 一个版本号。</p><h2 id="这次踩坑给我的几个提醒"><a href="#这次踩坑给我的几个提醒" class="headerlink" title="这次踩坑给我的几个提醒"></a>这次踩坑给我的几个提醒</h2><h3 id="1-CI-里的包管理器版本，最好别写成浮动值"><a href="#1-CI-里的包管理器版本，最好别写成浮动值" class="headerlink" title="1. CI 里的包管理器版本，最好别写成浮动值"></a>1. CI 里的包管理器版本，最好别写成浮动值</h3><p>如果这个项目的发布链路依赖 <code>pnpm</code>、<code>npm</code>、<code>yarn</code> 或者别的工具输出的依赖树，那我现在的建议很明确：能锁就锁，别只写大版本。</p><p>本地正常不代表线上正常，尤其是这种会牵扯到打包器、符号链接、依赖图解析的链路。</p><h3 id="2-“依赖已经安装成功”不等于“依赖一定会进最终产物”"><a href="#2-“依赖已经安装成功”不等于“依赖一定会进最终产物”" class="headerlink" title="2. “依赖已经安装成功”不等于“依赖一定会进最终产物”"></a>2. “依赖已经安装成功”不等于“依赖一定会进最终产物”</h3><p>这次算是把这个坑踩得很彻底。</p><p>以前我更习惯从“磁盘上有没有这个包”去判断问题。但对 Electron 这类桌面应用来说，最终交付给用户的是安装包，不是仓库里的 <code>node_modules</code>。中间还隔着一层打包器的筛选逻辑。</p><p>只要这层逻辑吃的是“依赖树描述”而不是“磁盘实际状态”，那就有可能出现这种很别扭的情况：包明明装了，但最后还是没进去。</p><h3 id="3-出问题时，早点去看产物本身"><a href="#3-出问题时，早点去看产物本身" class="headerlink" title="3. 出问题时，早点去看产物本身"></a>3. 出问题时，早点去看产物本身</h3><p>我这次真正开始接近答案，是从直接检查 <code>app.asar</code> 开始的。</p><p>很多时候我们会先盯着源码、锁文件、安装日志来回看，结果越看越乱。可一旦把注意力放到“最终产物里到底有什么”，问题范围反而收得很快。</p><p>这个办法虽然土，但挺有效。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>这次 <code>2.5.3</code> 的问题，说大不大，说小也不小。它不是功能 bug，而是发布事故。更麻烦的是，它看起来还很像“偶发环境问题”，很容易让人绕远路。</p><p>不过也因为这次把链路追到底了，后面再看类似问题，心里会更有数一些：先别急着怀疑代码，有时候真是工具链在背后捅刀子。</p><p>如果你也在用 <code>pnpm + electron-builder</code> 这一套，尤其是 workflow 里还写着浮动版本，那我建议你现在就去看一眼。别等到包发出去了，用户替你做集成测试。</p>]]></content>
    
    
    <summary type="html">PicGo 2.5.3 发布后出现安装即报 `Cannot find module &#39;esprima&#39;` 的问题。最后查下来，不是依赖没装，而是 pnpm 小版本漂移后影响了 electron-builder 的依赖收集结果。</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="PicGo" scheme="https://molunerfinn.com/tags/PicGo/"/>
    
    <category term="Nodejs" scheme="https://molunerfinn.com/tags/Nodejs/"/>
    
  </entry>
  
  <entry>
    <title>极空间虚拟机安装 Ubuntu 24.04 踩坑记录</title>
    <link href="https://molunerfinn.com/zspace-vm-ubuntu-24-04-pitfalls/"/>
    <id>https://molunerfinn.com/zspace-vm-ubuntu-24-04-pitfalls/</id>
    <published>2026-02-16T00:00:00.000Z</published>
    <updated>2026-03-08T01:14:37.987Z</updated>
    
    <content type="html"><![CDATA[<p>最近想在极空间里装一个 Ubuntu 24.04 虚拟机，主要用来跑脚本和一些小工具。原本以为很快就能搞定，结果重装了几次才跑通。这里把关键步骤和坑点整理一下，尽量让你一次安装成功。</p><p>先说结论：安装阶段先不要加网卡，走离线安装；驱动安装那一步不要勾选额外驱动；系统装完后再把网卡加回去。</p><span id="more"></span><h2 id="1-先准备镜像和虚拟机参数"><a href="#1-先准备镜像和虚拟机参数" class="headerlink" title="1. 先准备镜像和虚拟机参数"></a>1. 先准备镜像和虚拟机参数</h2><p>镜像可以在 <a href="https://cn.ubuntu.com/download">Ubuntu 官网</a> 下载，也可以用清华等镜像源。</p><p>创建虚拟机之前，先把极空间网络设置改成网桥模式：</p><p><img src="https://pics.molunerfinn.com/blog/20260214234530046.png" alt="bridge"></p><p>新建虚拟机时，我这边用的是以下配置：</p><ul><li>CPU &#x2F; 内存：至少 2 核 4GB（资源够的话可以再加）</li><li>固件：UEFI</li><li>虚拟机框架：q35（默认即可）</li></ul><p><img src="https://pics.molunerfinn.com/blog/20260214232719808.png" alt="create new vm"></p><p>硬盘按用途分配就行，20GB 到 100GB 都常见。</p><p><img src="https://pics.molunerfinn.com/blog/20260214233234183.png" alt="hard disk"></p><h2 id="2-第一个关键坑：安装前先删掉网卡"><a href="#2-第一个关键坑：安装前先删掉网卡" class="headerlink" title="2. 第一个关键坑：安装前先删掉网卡"></a>2. 第一个关键坑：安装前先删掉网卡</h2><p>网卡这里建议先删掉，等系统装完再加回来。</p><p><img src="https://pics.molunerfinn.com/blog/20260214233353893.png" alt="network settings"></p><p>创建完成后，点击虚拟机里的「访问」，通过网页 VNC 进入 Ubuntu 安装界面：</p><p><img src="https://pics.molunerfinn.com/blog/20260214233550358.png" alt="visit"></p><h2 id="3-安装流程里要注意的点"><a href="#3-安装流程里要注意的点" class="headerlink" title="3. 安装流程里要注意的点"></a>3. 安装流程里要注意的点</h2><p>安装界面我没留太多截图，这里只记最容易出问题的步骤：</p><ol><li>语言中英文都可以。</li><li>因为前面删了网卡，所以会走离线安装，这样更稳也更快。</li><li>遇到提示版本较低、是否更新时，先选跳过。</li><li>安装方式选手动安装即可。</li><li>询问是否安装额外驱动（比如显卡、音视频）时，不要勾选。</li><li>其他步骤基本默认下一步即可。</li></ol><p>我之前失败就是因为联网安装，还勾了额外驱动。最后网络不稳定，安装收尾阶段报错，连引导都没写进去，重启后直接进不去系统。</p><h2 id="4-安装完成后先做两件事"><a href="#4-安装完成后先做两件事" class="headerlink" title="4. 安装完成后先做两件事"></a>4. 安装完成后先做两件事</h2><p>系统安装结束后先别急着用，先关机整理配置。</p><p>先点击强制关机：</p><p><img src="https://pics.molunerfinn.com/blog/20260214234132595.png" alt="force shutdown"></p><p>然后在编辑页面里做两件事：</p><ol><li>卸载安装镜像（否则可能再次进入安装引导）</li><li>把网卡加回来</li></ol><p><img src="https://pics.molunerfinn.com/blog/20260214234236020.png" alt="edit vm"></p><p><img src="https://pics.molunerfinn.com/blog/20260214234312189.png" alt="unmount iso"></p><p><img src="https://pics.molunerfinn.com/blog/20260214234356749.png" alt="add nic"></p><p>完成后再开机。</p><h2 id="5-可选优化：换国内软件源"><a href="#5-可选优化：换国内软件源" class="headerlink" title="5. 可选优化：换国内软件源"></a>5. 可选优化：换国内软件源</h2><p>系统进桌面后，可以把 Ubuntu 的软件源切到阿里云，更新会快很多。</p><p><img src="https://pics.molunerfinn.com/blog/20260214234817064.png" alt="software source"></p><p>下载源选择「其他」，然后选 <code>aliyun.com</code> 的镜像地址：</p><p><img src="https://pics.molunerfinn.com/blog/20260214234913985.png" alt="aliyun mirror"></p><p>关闭后会弹窗询问是否更新，这时候直接更新即可：</p><p><img src="https://pics.molunerfinn.com/blog/20260214235000165.png" alt="update prompt"></p><p>也可以在终端手动更新：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update &amp;&amp; <span class="built_in">sudo</span> apt upgrade -y</span><br></pre></td></tr></table></figure><p>到这里，系统就可以正常用了。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>这次最关键的经验就三条：</p><ol><li>安装时先去掉网卡，离线装最稳。</li><li>额外驱动先别装，避免安装尾声翻车。</li><li>装完先卸载镜像、加回网卡，再开机进系统。</li></ol><p>如果你也在极空间里装 Ubuntu 24.04，希望这篇能帮你少走几步弯路。</p>]]></content>
    
    
    <summary type="html">在极空间里装 Ubuntu 24.04 虚拟机时踩过的坑：离线安装、驱动避坑、装完再补网卡和换国内源。</summary>
    
    
    
    <category term="折腾记录" scheme="https://molunerfinn.com/categories/%E6%8A%98%E8%85%BE%E8%AE%B0%E5%BD%95/"/>
    
    
    <category term="极空间" scheme="https://molunerfinn.com/tags/%E6%9E%81%E7%A9%BA%E9%97%B4/"/>
    
    <category term="Ubuntu" scheme="https://molunerfinn.com/tags/Ubuntu/"/>
    
    <category term="虚拟机" scheme="https://molunerfinn.com/tags/%E8%99%9A%E6%8B%9F%E6%9C%BA/"/>
    
    <category term="NAS" scheme="https://molunerfinn.com/tags/NAS/"/>
    
  </entry>
  
  <entry>
    <title>PicGo 的签名与公证</title>
    <link href="https://molunerfinn.com/PicGo-Signature-Notarization/"/>
    <id>https://molunerfinn.com/PicGo-Signature-Notarization/</id>
    <published>2025-12-25T00:00:00.000Z</published>
    <updated>2026-03-08T01:14:37.981Z</updated>
    
    <content type="html"><![CDATA[<p>一直以来，PicGo 并没有做 macOS 的签名和公证，因为一直没注册苹果开发者账号（其实学生时代需要每年出 $99 确实有点贵），所以就会导致用户下载了 PicGo 之后，会遇到这个问题：</p><p><img src="https://pics.molunerfinn.com/blog/20251225104315062.png" alt="App 损坏提示"></p><p>这个其实也有办法绕过去。不过近几年的 macOS 更新之后，对于应用签名是越来越严格，以前的一行命令还不够，还需要到系统设置里放行比如所有来源的应用之类的。总之门槛越来越高。同时，Homebrew 将在 2026 年 9 月 1 日开始，对于没有通过签名校验的应用，将无法再通过 brew cask 下载安装。趁着这次打算给 PicGo 做点商业化的机会，我也决定注册苹果开发者账号，然后给 PicGo 上签名了。本篇文章就记录一下 Electron 应用在 macOS 上做签名和公证的过程。</p><span id="more"></span><h2 id="1-获取-Team-ID"><a href="#1-获取-Team-ID" class="headerlink" title="1. 获取 Team ID"></a>1. 获取 Team ID</h2><p>首先你得注册一个<a href="https://developer.apple.com/cn/programs/enroll/">苹果开发者账号</a>，而开发者账号的注册是另外一个话题了（我简单发了篇<a href="https://www.xiaohongshu.com/discovery/item/694c9864000000000d03c43c?source=webshare&xhsshare=pc_web&xsec_token=ABAOC_W3g3S6rpYBXGuLtqEMJZBJkjhhxb89_iEt1hRww=&xsec_source=pc_share">小红书笔记</a>可以参考），我是用我一直以来的苹果账号注册的，没啥问题，12 月 24 日下午申请注册，24 日晚上就收到通过邮件了。</p><p>通过之后，可以在<a href="https://developer.apple.com/account">开发者账号页面</a> 找到 Team ID，记录下来，后面有用。<br><img src="https://pics.molunerfinn.com/blog/20251225135339137.png"></p><h2 id="2-获取证书"><a href="#2-获取证书" class="headerlink" title="2. 获取证书"></a>2. 获取证书</h2><p>我们最终的目标，是导出一个被认证的 p12 文件。</p><ol><li>创建一个 csr 文件</li><li>上传 csr 文件，获得一个 cer 证书</li><li>将证书导入钥匙串，导出 p12 文件。</li></ol><p><img src="https://pics.molunerfinn.com/blog/20251225110811051.png"></p><p>然后邮件地址我都填的我苹果开发者账号注册的邮箱地址，request 选择存到本地，注意勾上最下面那个指定密钥对！<br><img src="https://pics.molunerfinn.com/blog/20251225110949259.png"></p><p>然后登录 <a href="https://developer.apple.com/account/resources/certificates/list">苹果开发者官网</a> 申请证书，我暂时还没打算上架 Mac App Store，先选择 Developer ID Application。<br><img src="https://pics.molunerfinn.com/blog/20251225112630318.png"></p><p>然后导入刚刚的 csr 文件，就能下载这个 cer 证书，证书是有有效期的，意味着到期之后要重新生成。<br><img src="https://pics.molunerfinn.com/blog/20251225112822355.png"></p><p>如果是申请的 Mac Store App 分发的证书，这里将会只有一年的有效期。<br><img src="https://pics.molunerfinn.com/blog/20251225111548504.png"></p><p>然后就可以双击这个文件导入你的证书到 keychain（钥匙链），注意导入到 login（登录） 中。如果发现导入后遇到证书不信任的问题，说明还有一些额外的证书需要先导入。你可以到刚刚在苹果开发者网站申请证书的底部找到这些证书，下载，双击安装到 login 中。</p><p><img src="https://pics.molunerfinn.com/blog/20251225122315244.png"><br>然后把你刚刚那个提示证书不信任的证书，删掉，然后重新导入。重新导入后，你在 Certifacates 或者 My Certificates 里就能看到已经确认的证书信息（注意有个小三角，点开，下面展示是你的私钥，这个很重要，我们需要用它导出 P12 文件）<br><img src="https://pics.molunerfinn.com/blog/20251225122949764.png"><br>然后右键证书，导出 P12 文件，这个导出的时候会要求你输入一个密码，这个密码你可以自己定，是用来保护这个 P12 文件的。</p><p>导出 P12 文件之后，将其 Base64 字符串导出到剪贴板里，可以复制到某个地方，后文会用到。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">base64</span> -i certificate.p12 | pbcopy</span><br></pre></td></tr></table></figure><h2 id="3-创建标识符-App-专有密码"><a href="#3-创建标识符-App-专有密码" class="headerlink" title="3. 创建标识符 &amp;&amp; App 专有密码"></a>3. 创建标识符 &amp;&amp; App 专有密码</h2><p>前往<a href="https://developer.apple.com/account/resources/identifiers/list/bundleId">苹果开发者账号</a>页面，创建标识符。<br><img src="https://pics.molunerfinn.com/blog/20251225133331752.png"></p><p>目前来说暂时不用申请什么权限，如果之后需要的话可以再加入就行。输入 Description 和 Bundle ID 即可继续注册。Bundle ID 自己取，独一无二即可，通常的格式页面里也告诉你了。<br><img src="https://pics.molunerfinn.com/blog/20251225134135591.png"></p><p>然后去苹果官方的<a href="https://account.apple.com/account/manage">账号管理页面</a>，申请一个 App Specific Passwords：<br><img src="https://pics.molunerfinn.com/blog/20251225134359709.png"></p><p>申请完成后，需要自己找个地方好好存起来，只会展示一次：<br><img src="https://pics.molunerfinn.com/blog/20251225134638662.png"></p><h2 id="4-签名和公证"><a href="#4-签名和公证" class="headerlink" title="4. 签名和公证"></a>4. 签名和公证</h2><p>签名和公证在很多时候会被混为一谈，实际上它们是有区别的：</p><ul><li><strong>签名 (Code Signing)</strong>：证明“这是我写的，且没被篡改过”。</li><li><strong>公证 (Notarization)</strong>：证明“苹果查过了，这软件没毒”。</li></ul><p>如果没有公证的话，就会遇到经典的 <code>PicGo.app 已损坏</code> 的弹窗。同时，公证的前提是需要有合法的签名。因此注册一个苹果开发者账号，交 <code>$99</code> 年费之后看来是每年必备了。</p><p>接下来就是整理和收集上面的各种信息，把它们放到环境变量里，并进行签名和公证了。我自己是在 PicGo 项目的本地创建了一个 <code>.env</code> 文件，内容如下</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 第 1 步获取的 TEAM ID</span></span><br><span class="line">APPLE_TEAM_ID=xxx</span><br><span class="line"><span class="comment"># 这个是你注册苹果开发者账号的邮箱</span></span><br><span class="line">APPLE_ID=xxx</span><br><span class="line"><span class="comment"># 这个是第 3 步获取的 APP 特定密码</span></span><br><span class="line">APPLE_APP_SPECIFIC_PASSWORD=xxx</span><br><span class="line"><span class="comment"># 这个是第 2 步在导出 P12 文件的时候要求输入的密码</span></span><br><span class="line">CSC_KEY_PASSWORD=xxx</span><br><span class="line"><span class="comment"># 这个是第 2 步导出的 P12 文件的 Base64 格式的字符串</span></span><br><span class="line">CSC_LINK=xxx</span><br></pre></td></tr></table></figure><p>然后因为我用的是 electron-builder，所以只要环境变量里有 <code>CSC_KEY_PASSWORD</code> 和 <code>CSC_LINK</code> 的话，构建的时候就会自动签名。</p><p>而公证需要在签名之后，把签名后的 app 文件上传到苹果服务器验证一圈，通过之后你的软件才不会被 gatekeeper 给拦截下来。所以公证我们需要利用 electron-builder config 里的一个 afterSign 的属性，执行一个公证的脚本（利用 <a href="https://github.com/electron/notarize">electron&#x2F;notarize</a> 这个包提供的能力），同时根据 <a href="https://github.com/electron/notarize">electron&#x2F;notarize</a> 的文档，还需要开启 hardenedRuntime 和一些权限，如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> config = &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="attr">afterSign</span>: <span class="string">&#x27;scripts/notarize.js&#x27;</span> <span class="comment">// 具体公证代码的脚本需要根据各自项目目录决定</span></span><br><span class="line">  <span class="attr">mac</span>: &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="attr">hardenedRuntime</span>: <span class="literal">true</span>, <span class="comment">// 苹果要求，必须开启强化运行时</span></span><br><span class="line">    <span class="attr">entitlements</span>: <span class="string">&quot;build/entitlements.mac.plist&quot;</span>, <span class="comment">// 必须配合 entitlements </span></span><br><span class="line">    <span class="attr">entitlementsInherit</span>: <span class="string">&quot;build/entitlements.mac.plist&quot;</span> <span class="comment">// 必须配合 entitlements</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> config</span><br></pre></td></tr></table></figure><p>然后公证的脚本如下，核心就是调用 @electron&#x2F;notarize 提供的能力，然后这里需要用到我们刚刚放到 <code>.env</code> 文件里的环境变量：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// scripts/notarize.js</span></span><br><span class="line"><span class="built_in">require</span>(<span class="string">&#x27;dotenv&#x27;</span>).<span class="title function_">config</span>()</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> &#123; notarize &#125; = <span class="built_in">require</span>(<span class="string">&#x27;@electron/notarize&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> &#123; <span class="variable constant_">APPLE_ID</span>, <span class="variable constant_">APPLE_TEAM_ID</span>, <span class="variable constant_">APPLE_APP_SPECIFIC_PASSWORD</span> &#125; = process.<span class="property">env</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">APP_BUNDLE_ID</span> = <span class="string">&#x27;com.molunerfinn.picgo&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">main</span>(<span class="params">context</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; electronPlatformName, appOutDir, packager &#125; = context</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (</span><br><span class="line">    electronPlatformName !== <span class="string">&#x27;darwin&#x27;</span> ||</span><br><span class="line">    !<span class="variable constant_">APPLE_ID</span> ||</span><br><span class="line">    !<span class="variable constant_">APPLE_APP_SPECIFIC_PASSWORD</span> ||</span><br><span class="line">    !<span class="variable constant_">APPLE_TEAM_ID</span></span><br><span class="line">  ) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Skip notarization.&#x27;</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> appName = packager.<span class="property">appInfo</span>.<span class="property">productFilename</span></span><br><span class="line">  <span class="keyword">const</span> appPath = <span class="string">`<span class="subst">$&#123;appOutDir&#125;</span>/<span class="subst">$&#123;appName&#125;</span>.app`</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> now = <span class="title class_">Date</span>.<span class="title function_">now</span>()</span><br><span class="line"></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Starting Apple notarization for&#x27;</span>, appPath)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">notarize</span>(&#123;</span><br><span class="line">    appPath,</span><br><span class="line">    <span class="attr">appBundleId</span>: <span class="variable constant_">APP_BUNDLE_ID</span>,</span><br><span class="line">    <span class="attr">appleId</span>: <span class="variable constant_">APPLE_ID</span>,</span><br><span class="line">    <span class="attr">appleIdPassword</span>: <span class="variable constant_">APPLE_APP_SPECIFIC_PASSWORD</span>,</span><br><span class="line">    <span class="attr">teamId</span>: <span class="variable constant_">APPLE_TEAM_ID</span></span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;Finished Apple notarization for&#x27;</span>, appPath, <span class="string">`in <span class="subst">$&#123;(<span class="built_in">Date</span>.now() - now) / <span class="number">1000</span>&#125;</span>s`</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = main</span><br></pre></td></tr></table></figure><p>同时还需要准备一个权限文件 <code>entitlements.mac.plist</code>，内容如下：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">plist</span> <span class="keyword">PUBLIC</span> <span class="string">&quot;-//Apple//DTD PLIST 1.0//EN&quot;</span> <span class="string">&quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">plist</span> <span class="attr">version</span>=<span class="string">&quot;1.0&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">dict</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!-- 允许 JIT (Electron 必须) --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">key</span>&gt;</span>com.apple.security.cs.allow-jit<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">true</span>/&gt;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">&lt;!-- 允许加载未签名的动态库 (插件、原生模块必须) --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">key</span>&gt;</span>com.apple.security.cs.disable-library-validation<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">true</span>/&gt;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">&lt;!-- 允许执行内存中可写的页 (部分 Electron 版本需要) --&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">key</span>&gt;</span>com.apple.security.cs.allow-unsigned-executable-memory<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">true</span>/&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">dict</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">plist</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然后就可以正常构建了，构建完成之后，electron-builder 会自动帮我们进行签名和公证。整个过程大概需要 10 分钟左右，具体时间取决于苹果服务器的响应速度。</p><p>最后，本地跑通后，将它们集成到 GitHub Actions 里即可，GitHub Actions 里需要把上面的环境变量都配置好即可。这样做我们就是完成了签名和公证，这样用户从网络上下载 PicGo 就不会再被拦下了：<br><img src="https://pics.molunerfinn.com/blog/5359b6e102cc84a6af154e3f320bdcda.png" alt="5359b6e102cc84a6af154e3f320bdcda.png"></p><p>后续应该会考虑一下如何上架到 Mac App Store，到时候再继续补充吧。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;一直以来，PicGo 并没有做 macOS 的签名和公证，因为一直没注册苹果开发者账号（其实学生时代需要每年出 $99 确实有点贵），所以就会导致用户下载了 PicGo 之后，会遇到这个问题：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pics.molunerfinn.com/blog/20251225104315062.png&quot; alt=&quot;App 损坏提示&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个其实也有办法绕过去。不过近几年的 macOS 更新之后，对于应用签名是越来越严格，以前的一行命令还不够，还需要到系统设置里放行比如所有来源的应用之类的。总之门槛越来越高。同时，Homebrew 将在 2026 年 9 月 1 日开始，对于没有通过签名校验的应用，将无法再通过 brew cask 下载安装。趁着这次打算给 PicGo 做点商业化的机会，我也决定注册苹果开发者账号，然后给 PicGo 上签名了。本篇文章就记录一下 Electron 应用在 macOS 上做签名和公证的过程。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="PicGo" scheme="https://molunerfinn.com/tags/PicGo/"/>
    
  </entry>
  
  <entry>
    <title>写在 PicGo 即将 8 周年之际</title>
    <link href="https://molunerfinn.com/8-years-of-PicGo/"/>
    <id>https://molunerfinn.com/8-years-of-PicGo/</id>
    <published>2025-11-23T00:00:00.000Z</published>
    <updated>2026-03-08T01:14:37.981Z</updated>
    
    <content type="html"><![CDATA[<p>2017 年 11 月 28 日，还在大学宿舍里的我，提交了 PicGo 的第一个 commit，距今已经 8 年。现在看来，时间是过得真快啊。</p><p><img src="https://pics.molunerfinn.com/blog/image.png"></p><p>如果你不知道 PicGo 是什么东西，也不会妨碍你阅读。从我提交它的第一个 commit 的那刻起，命运的齿轮就已经悄悄开始转动。我的研究生生涯、实习、工作都有它的影子，甚至我自己想要做的事情，我想学习的东西，也受到它很大的影响。</p><p>正巧前段时间，前微信的一个同事发的一篇文章<a href="https://mp.weixin.qq.com/s/avHKLvLqO-8AKDd00peTOw">《从广深到北美——本科毕业这四年》</a>也让我内心深深地触动。因为我跟他一样，我也问过我自己这个问题：“我想过一个什么样的人生”。</p><span id="more"></span><h2 id="2017-11-2020-6"><a href="#2017-11-2020-6" class="headerlink" title="2017.11 ~ 2020.6"></a>2017.11 ~ 2020.6</h2><h3 id="PicGo-的诞生"><a href="#PicGo-的诞生" class="headerlink" title="PicGo 的诞生"></a>PicGo 的诞生</h3><p>在写 PicGo 之前，我最主要的学习 &amp; 开源的项目是 <a href="https://github.com/Molunerfinn/hexo-theme-melody">hexo-theme-melody</a>，也就是我的博客所用的主题。在这个主题上，各种天马行空的想法，感觉很赞的交互、很棒的 UI 我都在这个主题上基于它的风格自己实现了一遍。而在写 PicGo 之后，由于经历有限，这个项目的维护就逐步滞后以至于最后基本不怎么维护了，并由此催生了 hexo 社区另一个很知名的主题 <a href="https://github.com/jerryc127/hexo-theme-butterfly">hexo-theme-butterfly</a> ，这个主题是基于 hexo-theme-melody 二次开发而来，也感谢作者把 hexo-theme-melody 放到致谢中。</p><p><img src="https://pics.molunerfinn.com/blog/image%25201.png"></p><p>我是在写博客的时候想要个好看点的主题，就自己写了上面那个开源项目。而你一旦开始写博客就无法避免的一个问题：你要如何往你的博客里贴图。</p><p>如果你是用一些平台发布文章，比如微信公众号，它们的后台本身提供了图片上传能力的就问题不大；但是如果你是用 markdown 写文章，那么一旦要传图片，就会比较麻烦。</p><p>通常来说你有几种选择：</p><ol><li>把图片跟着 markdown 一起上传，用相对路径，比如 <code>[img](./xxx.jpg)</code> 。这种方式的优点是比较简单，缺点是图片必须严格跟 markdown 文件放一起；一旦图片挪窝，就无法加载了；以及可能会因为图片较大，加载较慢。</li><li>把图片上传到远端服务器上获取图片的 URL 再写到 markdown 里，通常国内把这种存图的地方叫做图床。这种方式的优点是图片和 markdown 解耦了，可以在很多场合都可以复用；图片本身如果加上 CDN、图片压缩等处理方式的话，会加载更快。缺点是，大部分情况需要花钱买服务，还有可能遇到盗链的情况。但是瑕不掩瑜，优点还是远大于缺点的。</li><li>把图片转成 base64 编码内联到 markdown 文件里。优点是图片不会丢；缺点是图片文件如果很大，转成 base64 编码会更大；而且图片无法在其他地方复用。</li></ol><p>我自己是采用第二种方式。一开始其实还好，后面发现要传的图片一旦很多，就会遇到严重的效率问题。举个例子，我要在 markdown 里贴一张图片，需要如下几个步骤：</p><ol><li>打开我的图床服务商的网站</li><li>登录</li><li>找到上传图片的地方上传图片</li><li>等待上传成功后，获取上传后的图片 URL</li><li>粘贴到 markdown 里</li></ol><p>可以看到要在 markdown 里贴一张图片，至少需要 1～2 分钟。当时我就在想有没有什么办法能够提高这个效率呢？于是我就在网上找解决方案，还真给我找到了，有一类工具叫做图床工具，就是专门用来解决这个问题的。当时我在 mac 上发现了一款叫做 <a href="https://toolinbox.net/iPic/">iPic</a> 的工具非常吸引我：</p><p><img src="https://pics.molunerfinn.com/blog/006tKfTcgy1fewqw208xmg30j60aske8.gif"></p><p>通过简单的拖拽，就能快速上传一张图片，并且把图片链接放到你的剪贴板里。这样的话，写 markdown 传图这件事情就变得非常简单：</p><ol><li>上传图片</li><li>粘贴到 markdown 里</li></ol><p>从原本的 1 ～ 2 分钟，变成了秒级的操作。这真的让我非常兴奋。不过试用之后发现了一些缺点（当然可能是我的问题）：</p><ol><li>iPic 默认的免费图床只有微博图床一种，如果我想用自己的图床服务商，需要花钱升级</li><li>iPic 只支持 macOS 系统，我当时有台 macbook，还有实验室的 windows 机器，所以我两个平台都有这个诉求，iPic 只有一个平台支持，所以我第一条付费的话就感觉比较亏</li></ol><p>那怎么办？于是我在当时市面上又找了一圈，但始终没有找到更满意的方案。最后我就在想，这玩意应该也不难，我有前端方面的知识，我是否能够自己写一个这样的小工具自己使用呢？</p><p>于是我就开始调研写这个工具的方案 —— 最早本来想学下 macOS 或者 iOS 开发，用 Swift 来写，但是后来想想我之后还是要在 Windows 上使用，就得找跨平台的技术方案。也非常巧，那个时候 VSCode 已经小有名气，它底层的 Electron 框架也已经相对成熟。Electron 能解决我跨平台的需求，又是用我熟悉的前端技术栈来构建，同时发现大部分 iPic 实现的交互，在当时的 Electron 提供的 API 里我基本也都能实现，所以我很快就锁定了这个方案。</p><p>于是我就在 11.18 开始边学边写了第一版的 PicGo，现在看来它真的非常简陋，但是已经有了之后版本的基本雏形。</p><p><img src="https://pics.molunerfinn.com/blog/image%25202.png"></p><p>写完第一版之后，我自己感觉还是很好用的，就想着推广一下。我尝试了很多平台效果都比较一般。最后是在少数派上发了一篇<a href="https://sspai.com/post/42310">《PicGo：基于 Electron 的图片上传工具》</a> 之后，被推荐上了少数派的首页，效果非常明显。我记得很清楚，PicGo 的 GitHub Star 数在上完少数派首页后一下子就突破了 500。对当时的我来说真是无法想象，是非常有成就感的事情。</p><p>在接下来的一两年里，我除了在实验室打工以外，剩下的业余时间基本都花在迭代、更新 PicGo 上。同时我在开发 PicGo 的过程中，也在不断学习和总结。我把我的经验、遇到的问题，最终凝聚到了我自己的博客<a href="https://molunerfinn.com/electron-vue-1/">《Electron-vue开发实战》</a>中，这个系列全网的阅读量有几十万。当时国内市面上并没有系统性的 Electron 开发教程，大部分都是 API 文档搬运工。所以我的教程其实影响了很多后来学习 Electron 开发的人。比如 <a href="https://github.com/getgridea/gridea">gridea</a> 的作者就看过它并给了好评；比如 <a href="https://github.com/rubickCenter/rubick">rubick</a> 的作者就加过我的微信，他说他是跟着 PicGo 一步步成长的。时至今日，你在谷歌上搜索“electron vue 开发实战”，第一条依然是我的博客。</p><p><img src="https://pics.molunerfinn.com/blog/image%25203.png"></p><p>PicGo 1.x 版本是在不断支持新的图床（比如一开始只有微博和七牛图床）的过程中迭代。这个过程虽然能够一直有事情做，但是市面上的图床服务商那么多，每个都需要我手动做适配，后期出问题的情况下，维护成本高、精力无法跟上，而且 PicGo 会做成一个臃肿的图床服务商的集散中心。但是不做这些图床服务商的支持，又会让 PicGo 的用户群体受限。</p><p>正当我在苦恼的时候，一个划时代的 <a href="https://github.com/Molunerfinn/PicGo/issues/26#issuecomment-370105520">issue</a> 点醒了我，这个用户向我提的意见是「希望能把对各种图床的支持，做成 插件化 的管理。Core + Plugins 这样的做法是否可行？」。可行，当然可行。类似 VSCode 一样，本体管好基本的功能，其他额外的能力让插件做就行。</p><p><img src="https://pics.molunerfinn.com/blog/image%25204.png"></p><p>于是 PicGo 在我开发到 1.6 版本之后，就停止了内置图床服务商的开发，转向了如何实现一个 Core + Plugins 架构的道路上。我之前并没有做过任何插件系统的开发，但是我用过一些带有插件系统的工具，比如 Webpack、VSCode 等。一开始我想着是否能复用他们底层已经实现的能力，比如 Webpack 底层的 <a href="https://github.com/webpack/tapable">tapable</a> 。但是后来仔细想了之后，我觉得 PicGo 的上传图片流程没有那么复杂，用 tapable 的话有种杀鸡用牛刀的感觉。所以最终我还是决定自己实现一份，反正是我自己的项目，就当做学习，边学边写就是了。</p><p>开发插件系统之前，最重要的一件事情就是我把 PicGo 抽象出了一个底层的流水线模型，从 input 到 output，然后这里面的每个模块、事件钩子都是可以被插件化的，能够极大提升它的插件自由度。这个模型可以说是奠定了 PicGo 后续迭代的思路和方向，而最终的事实证明，它是成功的，这个我们可以放到后面说。</p><p><img src="https://pics.molunerfinn.com/blog/image%25205.png" alt="PicGo 底层模型"></p><p>想清楚这个模型之后，我就开始了边学边写之旅。这个过程其实真的要学习的东西非常多，写一个插件系统要考虑的事情不只是插件系统本身，要考虑很多东西，比如：</p><ol><li>插件系统在什么环境下可以被使用，CLI? GUI? Node.js 项目中？进而这个插件系统能否无缝接入我已经实现的 Electron 版本的 PicGo 中？</li><li>插件系统文档</li><li>插件如何分发、搜索、下载、启用</li><li>插件如何开发、调试、提供了什么 API</li><li>等等</li></ol><p>这里面的每一步都是我自己一点一点摸索出来的。终于，在 2019.6.13 我发布了基于 PicGo-Core 插件系统的 PicGo。在开发它的过程中，我还顺便拿到了当年前往广州微信暑期实习的机会。</p><h3 id="实习"><a href="#实习" class="headerlink" title="实习"></a>实习</h3><p>因为对象当时还在北邮读书的缘故（对象小我两届，我们之前约好了我在北京工作两年后，等她毕业找工作再一起看看是否要换城市），所以当年的暑期实习我就希望找北京的实习机会。后来就找了学长（非常感谢学长）内推了北京的微信。北京微信之前没怎么招前端实习，正好那年招，我还很开心，就去面试了。但是面完才了解到北京微信当时的那个部门前端就一两个人，思考再三我觉得还是想到一个大一点的前端团队里，所以我最后拒绝了那边的实习 offer。 然后微信的 HR 就找到我问我考不考虑广州微信的小程序前端团队，我在跟对象商量后，觉得实习反正就两个月，到时候回北京再找工作也可以。于是又面了两轮广州微信，接了广州微信的实习 offer。</p><p>面试的时候，PicGo 其实给到了非常好的背书，因为那个时候它已经有不少的 Star 了，作为一个在校生，这个是非常强有力的证明，关于它的诞生、迭代的思考我也能侃侃而谈。所以基本上面试官都会对这个项目非常满意——进而对我也很满意。</p><p>于是在 2019 年的 6 月底，我前往广州微信实习了。因为有 Electron 的开发经验，所以去广州的团队写微信开发者工具的时候，很多门槛其实对我来说就比较低。我记得当时入职的第一天下午就解决了一个 bug，提了第一个 PR，这也成了我整个实习生涯的一个缩影——天下武功，唯快不破，你就说写得快不快吧，哈哈。</p><p>我还记得很有意思的一件事情是，当时实习没多久（大概一周左右吧），组长找我 1-1 聊天，末了问我有啥需求、不满意的地方跟他说，我就说当时只给我配了台性能很一般的联想笔记本（当年实习生不知为何只配了 Windows 的笔记本），写开发者工具的话性能有点费劲。于是他 1-1 结束后立即帮我安排了台性能更好的台式机给我开发。现在想想，当时的我还真敢提需求哈哈。</p><p>到了 8 月份，微信的每个实习生都要进行一个转正答辩。上面说了，因为对象在北京读书，我当时一直是想着回北京找工作的，因此我“毅然决然“地拒绝了微信的转正答辩流程（现在回头看，我当时也是很牛的，拒绝了转正）。为此我的组长和我的 mentor 也没少找我谈心，希望把我留住；不过我最终还是决定回北京找工作。他们最后发现拗不过我，就祝福我了。在我实习最后，全组人还一起吃了顿饭来欢送我，当时真的觉得组里氛围真好，也挺不舍得。不过我还是按原计划结束实习，回到了北京。</p><h3 id="秋招"><a href="#秋招" class="headerlink" title="秋招"></a>秋招</h3><p>回北京后就开始紧锣密鼓地准备秋招，其实在微信实习之后，我觉得只是写页面的前端部门已经没啥意思了，我希望能够接触泛前端的一些技术栈（比如 Node.js 工具链、全栈、跨端等），而不只是做页面切图仔。现在想想，可能正好对上了后来前端很流行的一个词叫做「大前端」。所以北京很多公司的前端部门都被我 pass 了，我当时最想去的是字节飞书，去做 Lark 的桌面客户端。</p><p>面飞书前，我还面了猿辅导，面试过程其实很顺利。我记得在国庆前后吧，就拿到了 offer。但是当时给到的 offer 低于了我的预期，所以我其实并没有立即决定要去猿辅导。当年猿辅导给钱多，还 10-7-5，在北邮内部的口碑非常好。其实要是猿辅导当年给的 offer 达到我的预期，我可能真的就去猿辅导了hh（但是后来没过两年国家就出台教培相关的政策，猿辅导不幸中招，也裁员了很多，这是后话了）。</p><p>面完猿辅导之后，我就托了北邮的一个学姐（非常感谢！）帮我内推了飞书的部门，正好是我想去的主端的部门。飞书的技术面有三轮，在现场，一个下午全部面完。我记得挺清楚，我的一面答得挺不错，先笔试后面试，我感觉能给到 90 分。但是我的二面有好几个问题卡壳了，我感觉只能给 50 分，本来以为已经结束了，结果还有三面。三面的时候，我感觉答得中规中矩，又是 PicGo 的经历救了大命，这一轮我感觉可以给到 60 分吧。</p><p>于是面完飞书，就开始了漫长的 offer 等待。这过程中因为猿辅导已经发了 offer，但是因为低于预期我不想去，而字节还迟迟没出结果，内心其实是很焦虑的，考虑到二三面的情况，我担心字节的 offer 是凉了。我对象看在眼里其实也挺难受，所以我们在一起聊天的时候，她不断提起说「不然你再试试微信吧，那个团队你不是挺喜欢的么」，一开始我其实是拒绝的，因为我不想违背我们一开始的约定；而且微信我拒绝人家在先，我也实在没有什么脸舔回来。</p><p>不过这个世界就是这么神奇。在我等待字节的 offer 快一个月之际，微信的 HR 找上来问我是否还考虑之前实习的团队？当时我已经明确放弃了猿辅导的 offer，而字节的 offer 又迟迟等不来，身边的同学们都已经收到 offer 并且有些同学已经签了，这种感觉确实还挺煎熬的。</p><p>等待的过程中，之前认识的一个在快手（现在不在啦）的大佬 TX 非常希望我能去他们团队。我跟他说，他也知道我大概率两年后等我对象毕业后就会离开北京了，他说没问题，来面面先。经过他不断地游说，我还去面了快手，面试体验也不错。最后虽然非常抱歉，还是没有去快手，不过感谢 TX 一直以来的指导和鼓励，非常有帮助。</p><p>有一天晚上，我跟对象在学校操场散步，我提到说微信 HR 又找我问是否还考虑微信的事情，她就问我说你喜欢那个团队么？你想做那个团队做的事情么？那个晚上我们坐在操场旁边的看台聊了好久，我们最后都哭了，但是我们都认为微信那个机会挺好的，还是去争取一下吧。</p><p>于是第二天，我跟微信 HR 回复说“我考虑了可以再试试“。由于我之前实习的时候已经面试过微信，并且在组里实习表现也不错，所以这次我可以跳过组里的面试，直接走两轮面委（微信的面试委员会，都是级别比较高的大佬）。我记得面委面试的时候，没有想象中那么困难，更多地就是聊天，谈项目经历。可能我在微信的实习经历算是个比较大的背书，所以面委并没有什么很困难的问题，两轮面试在一天内就结束了。第二天，微信 HR 就马不停蹄地给我发 offer 了——还是非常有诚意的，给了当年的 SSP offer，并且入职就是 6 级工程师（腾讯当年一般本科生入职是 4 级，研究生入职是 5 级，我相当于高一级入职），收到 offer 我还是很开心的，跟对象分享了这个消息，她也非常开心，终于拿到了秋招满意的 offer 了。我当天就接受 offer 了。</p><p><strong>不过这个世界就是这么神奇（x2）。</strong> 在我接受微信 offer 的第二天，字节的 HR 也终于打来了电话，跟我说我的 offer 下来了。我当时的心情变成了 🤨 这样，我就跟她说了昨天微信已经给我发 offer 了，字节的 offer 等了一个多月，实在太久了。她说“因为字节今年内部有些调整所以很多 offer 审批就慢了。不过你也可以听听我们的 offer 吧。” 字节也给到了当年的 SSP offer，甚至能给到北京户口（当年还没有计划单列，所以北京户口是非常珍贵的）。不过因为我从一开始就不打算留北京（从上大学开始，我就不打算留北京），所以北京户口在我这里其实不是加分项，而是减分项——我一旦接受户口了，反而会让我离开的时候很麻烦（比如要交违约金、影响后面几届的学弟学妹等等）。从结果上来说，字节给的 offer 其实是比微信更多的，不过那个时候我心里有点纠结，因为在微信实习过，对于微信的团队我很喜欢，所以对于字节的 offer 我还有点犹豫。于是我跟 HR 沟通说，我可以去字节实习一段时间，再做决定，HR 很爽快就同意了。于是 2019 年底，我就进入字节的飞书部门开始实习了。</p><p>在当时，我已经知道飞书是一个很好用的办公软件，所以我还蛮期待在里面的工作体验。我入职的时候发现我的 mentor 就是我的一面面试官，还蛮惊喜。我的 mentor （后面简称他叫 FQ 吧）也是个很厉害的人，想要啥就自己去争取。他早年是去了国企，结果发现干着没啥意思，就自学编程结果来了字节。我入职的时候，他负责的部分是飞书的插件系统，这个是个还挺重要的部分。我作为他的小跟班，入职之后我就努力在做代码的阅读理解，因为他就坐我附近，所以很多时候有问题我就能立即请教他。他对我始终不缺夸奖，总是夸我上手快，做事情认真。以至于我们那个大组其他同事见到我都是说「听说你是 FQ 那个很厉害的实习生」，非常感谢他的认可和培养。</p><p>我记得很清楚，实习的时候还有个美国小哥坐我旁边，中文说得非常流利（印象中好像他是哈佛毕业的），来中国不能说是体验生活，但是来字节可以说是体验工作了哈哈，他拿着中国的工资过中国的生活，跟大家也相处得非常愉快。他跟我负责的东西不是一个方向的，不过我们平时还是会经常交流。他甚至还组织了（哈哈当然我不知道是他主动的还是被其他同事“怂恿“）一个英语角，每周都会有一个晚上，大家找一间会议室，然后边吃饭边用英文聊天。一开始我还不好意思，但是后来他鼓励我如果感兴趣也可以来参加，于是我后来也参加了好几次，还是挺有意思的。</p><p>在字节实习的时候，感觉其实也不错。不过有一点确实能感觉到部门太多，部门和部门之间的部门墙有点高。以我做的插件系统为例，我们需要接入飞书除了 IM 以外的其他模块，比如日历、邮件等等，他们会依赖我们的一些 API，所以一旦要做优化、改版之类的，就会频繁遇到上下游沟通协作的事情。当时的我还是实习生，感受不深，但是好几次 FQ 去开完会回来都是摇头、叹气。现在想想可能有很多事情他也无法左右吧。</p><h3 id="毕业季"><a href="#毕业季" class="headerlink" title="毕业季"></a>毕业季</h3><p>临近春节的时候，发生了一件后来影响全世界的事情——新冠疫情。我还记得当时我在飞书的工位上看到一则新闻，大概是武汉出现了不明传染病，那个时候还不知道这个事情会影响这么深远。以至于我那次从北京回家的时候，行李都没带多少。结果没过两天，武汉就封城了，学校后来也通知不要回去了；于是我就在家里改毕业论文、远程在字节实习。（划重点，这里是我第一次远程工作，以后要考） </p><p>疫情在家的那半年，每天就是上班、吃饭、睡觉。也无法出门，也没咋运动，真的是我有史以来长胖最快的一段时间，我体重一度到了 74 kg（我正常的体重大概是 65 kg 左右），整个人从轮廓上胖了一大圈。我一度怀疑我是不是从此以后就无法瘦回去了。事实证明我多虑了，我现在的体重又是稳定在 65 kg 左右了。</p><p>我在字节的实习也在 5 月份落下了帷幕。期间在字节的组长也找我聊过两三次，希望我能留下，不过最终思考再三，我还是决定去微信。在告别了字节的同事们之后，我就开始准备毕业相关的事项，以及广州租房的事情。</p><p>我依然联系的是实习的时候在广州租房的那个房东（他也是个二房东），而且还专门跑了广州一趟看看房子如何。2600 一个月，合租的一个单间，能看到广州塔；其实感觉是有点小贵，不过当时腾讯给应届生是有 1500 一个月的房补的，所以算下来就还行，能够接受，最后看完房子就签了。</p><p>后来临近我要入职的时候遇到一个坑爹的事情，那个房子因为地板漏水被楼下的业主投诉了，导致我租的那个房子里的租户都要搬走，业主要拆地板修漏水。临近入职遇到这个事情一下子打乱了我的节奏，如果没处理好我到广州可能还没地方住，非常闹心。二房东手上没有更好的房源了，于是我只能在网上看那种连锁的租房平台，比如自如和蛋壳。当时正好有个已经去广州的北邮同学（他也是入职微信），我只能不好意思请他帮我看看公司附近的一个蛋壳公寓上的房源如何。最后他通过微信视频带我远程看房，并最终敲定了，非常感谢他🙏。当然这里租的蛋壳公寓，半年后就暴雷了，这个后文再说。</p><p>那年因为疫情，无法返校，没有毕业典礼，甚至我的行李也只能拜托我的舍友帮我把最贵重的一些东西邮寄回来，其他的很多东西都被我舍弃了；也因为疫情，没法跟对象见一面。在这种有点遗憾的背景下，我只身前往了广州，正式开始了我的工作生涯。</p><h2 id="2020-7-2023-1"><a href="#2020-7-2023-1" class="headerlink" title="2020.7 ~ 2023.1"></a>2020.7 ~ 2023.1</h2><p>我在微信期间的经历，大致可以分为两个部分——一个上半场，一个下半场。上半场的工作虽然累，但是成就感非常高，我在其中也连续得了两次高绩效；下半场的工作，成就感开始逐步消失，取而代之的是对所做事情的价值的怀疑，以至于我最终做出了离开微信的决定。</p><h3 id="上半场"><a href="#上半场" class="headerlink" title="上半场"></a>上半场</h3><p>我很喜欢微信在 TIT 创意园的办公点，都是矮楼，不用等电梯；平时可以在园区里散步，风景很好（毕竟本身也算是个景点了），跟同事们饭后散步绕圈、聊天、八卦是当时工作之余非常向往的事情。</p><p>由于我当时入职的时候职级就比正常硕士生高，所以组长对我的要求和期待也会更高一些；不过当时入职之后，我发现比起实习的时候，要接触的事情、概念比我实习的时候多了很多，同时组长又交给我一个比较复杂的任务，于是第一个月我成了问题小伙，有不懂的东西就到处问。微信在文档落地这点其实做得不太好，大家都不爱写文档，很多东西都是口口相传，如果我不问，我可能就一直不知道。</p><p>不过做开发者工具的日子没过多久，我就跟另外两个小伙伴被调去支援另一个组的工作，他们那个时候做的小游戏引擎项目正在上升期，极度缺人。不过他们做的事情跟我们组做的开发者工具还是有较大差异的，所以当时其实我们三个人压力都挺大的，每天都要加班到挺晚，只为补足欠缺的知识，以及能够快速帮他们解决一些问题、做一些需求。</p><p>我其实有点忘了当时我是怎么度过那段时间的了，现在想想还是很不可思议。虽然解决问题的时候成就感还是很高的，不过确实不算是感兴趣的东西，所以我到现在已经忘记了很多当时做的项目、专有名词。我只记得当时做项目期间，跟同事一起追美股 GME 的惨痛回忆哈哈。第一次绩效考核的时候，由于在支援的项目做得不错，还拿了个高绩效。</p><p>这中间还发生了另一个坑爹的事情。10 月底的时候，有个同事问我说「皮蛋你是不是住的蛋壳公寓」，我说是的。然后他就转发了一个文章给我。我看了下是深圳的蛋壳公寓有些租户发现蛋壳公寓的一些服务（比如无线网络、定期保洁等）开始出问题了，客服也无法及时解决问题。它的微博也只是一味在说自己没问题之类的。当时我住的那个蛋壳公寓倒是还没遇到前面所说的那些问题，但是我看了不少深圳蛋壳的租户已经在网络上发帖各种问题，感觉还是有点风险。因为我也快到了续期的时候（此前我是交了半年的房租，因为当年有个活动是一次性交半年能免一个月房租，所以实际是交了 5 个月房租），想到一续期就是半年起步，万一这真的暴雷了，岂不是血本无归；于是我赶紧想着不续租了，看看不然换自如，因为当时看自如还是挺稳的。正好同小区有另一个房源自如正摆上了货架，价格跟我当时租的蛋壳差不太多。于是我马上约了看房，并且看完觉得还不错，所以我就立即做了决定要换租；当时我对象还觉得我做决定有点太草率了，我当时也觉得有点着急，但是我想着毕竟不能赌蛋壳没风险，就宁可亏半个月的房租，也要换租；因为是同小区，所以那次我就自己打包了所有的行李，叫上我几个好朋友一起帮我搬家，完了之后请他们吃了小区门口的陈记顺和，那顿陈记顺和是真好吃啊。后来蛋壳就真的暴雷了😂。</p><p>过完年回来之后，我就逐步从游戏引擎项目抽身，开始做小程序真机调试的一个架构升级（也就是后来的真机调试 2.0）。其实一开始这个东西，我们也不知道能不能做成（只有基础库的一个理论实现，但是没有实装），有没有什么坑，所以一开始组长只是让我去做做看。我们要做的架构升级，其实当时市面上并没有第二家做过，所以我是没法摸着石头过河，我只能自己做过河的人。这个项目涉及到跨好几个组、部门的同事。说实话，毕业不到一年，我需要自己去找各个上下游（尤其要协调微信客户端同事的资源）来跟我一起确定方案，联调，发版等等，现在想想我当时还挺牛。这中间的坑就不多说了，每一步都是坑。为了能够确保跟他们能联调上，有问题可以及时沟通，我甚至有段时间只要一旦他们开始能够跟我联调之后我就坐到他们旁边，跟他们结对编程。</p><p>我记得有次我跟 iOS 的客户端同学，查一个问题在 TIT 查到了凌晨。在最终基本定位到问题之后，我们还点了夜宵烧烤在工位上吃完才走。非常感谢 iOS 这个同学，他跟我一样当时都没有额外的怨言，我们就是想在那天把那个问题解决了，解决了之后 iOS 这侧就彻底跑通了。那种纯粹为了解决问题，解决了问题又得到纯粹的成就感的感受，到现在还印象深刻。</p><p>终于在这个项目开始 3 个月之后，终于跑通了第一个 MVP。后来经过一些性能优化、问题修复之后， 21 年 7 月正式发布到线上。到现在，这个真机调试 2.0 的整体架构还是没有发生非常大的变化，依旧还在运行。现在你所用的每个微信小程序，都需要经过它来进行真机调试。后来借此还去了趟 <a href="https://tweb.tencent.com/#/">T Web</a> 做了个微信小程序真机调试的技术分享，这也是我第一次做讲师做对外的技术分享。</p><p><img src="https://pics.molunerfinn.com/blog/20251122102143261.png" alt="T Web 的分享"></p><h3 id="下半场"><a href="#下半场" class="headerlink" title="下半场"></a>下半场</h3><p>22 年开始，可能是受疫情的影响，整个互联网的氛围突变，降本增效成为了当年的主旋律。这对于微信或者说对于我们部门的影响主要有两个：</p><ol><li>裁员，基本每个组都有裁员指标；甚至有些是整个部门、中心裁员。</li><li>各部门、各个中心需要「自负盈亏」，需要做可以盈利的项目</li></ol><p>我们组也有裁员，有个跟我挺要好的同事（当时跟我一起支援去做游戏引擎的一个同事）是正好自己想走了，所以拿着裁员补偿就走了。后来他玩了半年，也挺爽的。对于这种自己想走的，裁员反而不一定是坏事。</p><p>留下来的人就得经历上面说的第二个影响了：需要做可以盈利的项目。这对于我们这种做开发者生态的团队，在当时（如果当年 AI 发展像今天，可能就好说了）是非常难盈利的。开发者众所周知都是「一毛不拔」除非真的能解决他们的痛点。因此开发者工具、开发者社区等我们组负责的项目在当时就被认为是不赚钱的项目，就要被削减人力，减少维护成本，转而去做盈利的项目。</p><p>但是要做盈利项目谈何容易？这个要求是自上而下传达的，要做什么却变成是自下而上去想的。我不反对有些创意可以从一线员工发起，但是我认为在当时那种情况应该是掌舵人给出方向更合理。最终我们组拉了个大会，要做个付费的开发平台，里面聚合了几种看着毫不相关的子产品：</p><ol><li>多端开发（小程序转成 App）</li><li>微信网关</li><li>用户行为分析（无埋点）</li><li>身份管理（后来并入多端）</li></ol><p>同时为了跟微信撇开关系，这个平台的名字还不能跟微信有关系（我也没想懂这个是为啥，可能是觉得不跟微信沾边更能盈利？）。最后名字被定成了一个非常奇怪的 「Donut 开发平台」。</p><p>这个东西槽点太多了，<strong>我从一开始就觉得这玩意成不了，至少盈利不了</strong>。现在回头看，确实没成，它的研发成本远远远远大于用户付费的收入，以至于最后 Donut 开发平台现在已经不维护了；里面的一些能力被并入了现在的微信开发者平台，而且也取消了盈利的目标。</p><p>接下来我说说我自己粗浅的理解，为啥我从一开始就觉得这玩意成不了。因为我看到要做这个平台的时候，我问我自己的第一个问题就是：<strong>这个以盈利为目标的平台的目标用户到底是谁？</strong></p><p>把这个四个子产品拆解一下：</p><ol><li>多端开发。目标用户肯定不是大商家，因为大商家基本都会有自己的 App。<strong>所以只能是小商家。</strong></li><li>微信网关，目标用户肯定不是中小商家，因为微信网关收费很贵，<strong>所以只能是大型商家</strong>才能负担得起；同时也只有中大型商家才有对安全、速度等有相对高的要求；小商家用户量都没多少，为啥要花钱买这个？</li><li>用户行为分析，目标用户也肯定不是大商家，因为大商家一般都会有自己比较完善的埋点上报用来做用户行为分析。<strong>所以只能是小商家。</strong></li><li>身份管理，这个不表了，因为这个后来做着做着也非常怪就并入多端，作为多端项目登录的一个解决方案了。</li></ol><p>所以可以看到四个产品其实是各自为战的，并不是 1+1+1+1 &gt; 4 的，甚至是 1 + 1 + 1 + 1 &lt; 4 的感觉，我很难想象有哪个商家能够同时把四种服务都付费用起来。而且既然是做个盈利的项目，目标用户不清晰，就很难做商业化、推广。甚至我们也没有专门的售前、售后团队，微信一直没有客服基因，我实在想不出怎么能够把这个项目做到盈利。</p><p>但是一旦这个项目的定位不是做一个可盈利项目，而是作为微信开发者生态的补充，那么很多事情就能说得通了；它就变成了一个小商家，利用微信开发平台提供的能力，逐步成长为一个大商家的故事。可惜很遗憾，当时的定位决定了它做不成。事实也证明，这个平台如今已经被放弃维护，这里面的项目不再以盈利为目的，而是重新变成开发者生态的补充，并入了微信开发者平台。</p><p>因为 22 年降本增效，好几个同事要么被裁员，要么自己离职，导致有很多事情最终都交接到我这边。我一边要做新的开发平台，又要接手很多维护的事情。所以在 22 年一整年，我做的事情就非常杂，而且我也做得很不舒服。年初定的 OKR 根本没有参考意义，因为做的事情跟计划差距太大。</p><p>我记得 22 年上半年的时候，各地的健康码为了避免在大批量验码（比如做核酸检测）的时候宕机，都被要求 QPS 达到多少多少。因为当时我们有个小程序压测平台，他们就会用这个东西来测他们的健康码的性能。这个平台正好交接到我手上，所以我有好几个半夜（因为健康码只能在半夜进行压测）都得跟这些政府的健康码团队一起联调压测的问题。但是这些辛苦的付出，其实只有我自己知道，有次我跟组长提过一嘴，但是也只是得到了一声笑笑。</p><p>到 22 年底的时候，一方面我有太多要负责的杂事、一方面主线要做的项目又不能拉下，但是这个项目又让我很难认同用户价值，我的整个工作状态已经变得非常差。正好那个时候又遇上疫情反复，我们也都居家办公了，我发现居家办公的时候，我的效率相比在工位办公的时候还更好。我从那时起开始思考我是否要继续留在微信了。我抽空开始写简历，开始刷题练练手，但是我始终没有把我的简历投出去，因为我还想着等着年底答辩之后再说吧。</p><p>压死骆驼的最后一根稻草，便是 23 年 1 月初沟通绩效的时候。我觉得我这一整年在我负责的所有事情上（尤其我接手了好几个同学的事情）都做得很好（虽然主要的项目我不认可，但是我还是尽我所能做好它），应该能够拿一个好点的绩效去答辩（当年的答辩，绩效分占 60%）。那次不少其他的同事也觉得我应该能拿个好的绩效。结果组长沟通完，还是拿了个普通绩效。在我听到绩效结果的当天，我就决定去看看外面的机会，并投出了我的简历。</p><p>而这次投简历，我有两个原则：</p><ol><li>我希望是远程工作，因为我觉得我远程工作效率更高</li><li>我不想去大厂，我想去创业公司，我想了解业务、创业公司是怎么盈利的</li></ol><p>当然原则一可能已经限制了很多大厂是没法提供给我远程岗位的，正好匹配上第二条规则。其实一开始我对象听到我想找的是远程的工作，还是很担心我找不到的，她建议我如果找一段时间没找到的话，还是找正常的非远程的工作岗位吧。我跟她说就试试 1 月份能不能找到，如果找不到我就找非远程的岗位。其实一开始的一两周，投出去都没有什么水花；直到一天，我投递的 MoeGo 正好缺前端，他们也愿意给优秀的候选人远程工作的机会，于是我就抱着试一试的心态去面试了。</p><p>临近月底的时候，我已经拿到了 offer，并且薪资也是在我预期内的。拿完 offer 不久，就到了年终的沟通（腾讯的年终沟通在过年前），我发现我虽然拿的还是普通的绩效，但是我的年终可以说是垫底的水平也不为过（甚至不如我刚毕业的那年的年终）。因此从那刻起，我就下定决心要离开微信了，我觉得我的付出并没有得到认可。</p><p>于是，年后回来的第一天，我做足了心理准备，就向组长提出了离职。组长其实还挺惊讶，因为他从来没想过我会提出离职。他觉得大环境这么不好，能够在微信有份工作已经是很好的一件事情了。但是我觉得在这里继续待下去，我已经无法学到我想学的东西了。在微信，如果一个项目做黄了，再做另一个项目就是了，毕竟微信能养得起。但是我理解如果我们是个创业团队，我们可能活不过三个月，我想去了解创业公司是怎么思考问题的，怎么做到养活自己的，怎么做到盈利的。所以我在跟组长聊完，还没等他跟总监说的时候我就在系统里发起了离职申请。总监当然也很惊讶，也找我沟通，也想挽留我，甚至说可以争取保我这次晋升。但是同一时期我另一个同事他表现也很好（也是当时跟我一起去支援游戏引擎的另一个同事），晋升名额是有限的，我不希望因为我导致他这次又无法晋升成功（他上次已经失败一次了），所以我跟总监说，我不想要这个名额，把名额给他吧。不过最后这个同事也没晋升成功，他后来在我离职后不久也主动离职了，现在是另一个公司的扛把子，这就是后话了。</p><p>我离职的时候，我的工作拆散交接给了 8 个同事。二月底，我正式离开了我工作了 956 天的鹅厂。</p><p><img src="https://pics.molunerfinn.com/blog/BB2F6422-AE76-4212-A001-D4EC9E364B6F_4_5005_c.jpeg"></p><h3 id="开心的事"><a href="#开心的事" class="headerlink" title="开心的事"></a>开心的事</h3><p>当然，这几年里也有不少开心的事情。22 年 6 月的时候，我跟对象买房了，需要 24 年才交楼（现在看看可能是站在了最后一波高峰，哈哈），但是至少确定了我们的小家；国庆回家订了婚，然后回广州后就跟对象领证了（那个时候我们已经谈了 6 年了），终于可以叫老婆了。</p><p><img src="https://pics.molunerfinn.com/blog/E50886CC-24FB-4B10-9F5A-BF682D6D3036_1_102_o.jpeg"></p><p>PicGo 在这个期间里更新到了 2.3.1 版本，GitHub start 19k+，下载量达到了 640k+；成为了 Typora 、Marktext 等文本编辑工具官方支持的图片上传工具；腾讯云官方推荐的图床软件；社区贡献的插件数量也到了 50+；我想，作为一个我只能业余时间抽空开发的项目来说，它已经远远超出了我的预期。</p><h3 id="微信小结"><a href="#微信小结" class="headerlink" title="微信小结"></a>微信小结</h3><p>说实话，在微信这几年也学到了非常多东西。一个从来没接触过的东西，需要在很短的时间内上手、解决问题、开发需求等等这些能力，在微信这里我觉得还是得到了相当程度的锻炼。尤其在当时没有 AI 编程工具辅助的情况下，这些真的就是综合素质硬实力的体现。在前端领域，微信小程序的前端可以说确实算是国内前端工程师的天花板级别的水平（虽然对外的一些功能或者产品被用户吐槽甚多），研究的东西非常之深入，经常要跟客户端、Chrome 源码等打交道。在其他很多公司，遇到难题想到的一般是绕过去，但是在微信小程序这里遇到难题基本上是要跨过去。而且很多时候我们在做的事情，是市面上没有地方可以参考的。比如我做的真机调试 2.0，比如我们需要把 VSCode 这个 Electron based 的编辑器整合进 NW based 的微信开发者工具里。所以在这里，前端的技术深度是绝对够深。</p><p>不过可能也因为对于技术深度得过分追求，对于产品价值的把控，对于用户的使用体验等等就会有所欠缺。在微信小程序，前端如果只会写 Web 页面，是非常容易给低绩效的。在这里一定要做出有技术深度的东西才行。在早期，技术能够解决用户问题，能够带来实际价值，尤其小程序的很多创新需要技术支撑。但是后来随着时间的推移，low-hanging fruit 已经被摘得差不多了，就开始内卷了，大家不得不开始做一些为了追求技术深度、为了晋升答辩而做的事情。但是技术深度其实不等于用户体验、产品价值。这也是我在微信后半程做事情的时候发现越做越怀疑自己的一个原因。我们很多决策并不是建立在用户调研的基础上，而是「我觉得用户需要这个，我觉得用户会用这个」这种方式去做产品。我在做 PicGo 的时候发现真正好的产品，是愿意倾听用户的。所以在微信这个环境下，我发现我很难能够跟用户走得更近，而我下一阶段想要学习的东西，需要换个地方了。微信就像是个围城，里面的人想出来，外面的人想进去。</p><p>总之，感谢在微信的经历，在这里跟很多很厉害的大佬们学习了非常多知识，掌握了很多技术以及学习未知的方法；也感谢我自己愿意迈出这座城，我收获了很多同事的友谊，也得到了很多的帮助，感谢你们。</p><p><img src="https://pics.molunerfinn.com/blog/A399C7CF-A77F-4523-9D67-42FE68048AD2_1_102_o.jpeg"></p><h2 id="2023-2-2025-11"><a href="#2023-2-2025-11" class="headerlink" title="2023.2 ~ 2025.11"></a>2023.2 ~ 2025.11</h2><p>在美国，宠物服务的商家分成好多种类型。比如 Grooming 就是提供宠物剪毛、洗澡这类服务的商家。它们又分成了 Salon（开实体店的）和 Mobile（开着房车的，如下图），尤其后者在美国是一种非常特别的商家类型，买或者租一辆房车，就可以开始自己的 business。 MoeGo 做的事情就是给这些商家提供电子化的平台（Web &amp; App），方便他们管理自己的日程、员工排班、收发顾客的消息，以及对外提供在线预约的网站等。美国的人工很贵，做一次剪毛、洗澡可能就要大几十上百刀。而 MoeGo 的一个月订阅费也不便宜，也要几十到 200 多刀，在国内这个事情不敢想。但是如果使用了 MoeGo 之后能获得更多的订单，可能一单就能回本。所以从这样的角度看的话，商家用了之后生意变好了就会愿意继续续费下去。同时 MoeGo 不仅做 SaaS 订阅，还自己做支付方式，可以根据商家的交易流水抽点，所以商家生意做得越好，MoeGo 的收入也会越高。这些也是我认为 MoeGo 的商业模式能说服我的原因。</p><p><img src="https://pics.molunerfinn.com/blog/image%25206.png" alt="Mobile grooming"></p><p>从微信离职后没几天，我就入职了 MoeGo。根据一开始跟 HR 的协定，我需要去深圳驻场办公一个月，熟悉一下工作、同事等，然后才能回广州远程。公司那个时候人还不多，就几十个人，其中还有一部分同事是在美区的。因为人不多，所以深圳的办公点我们甚至一开始还是租用的类似共享办公室的场地。不过虽然硬件条件跟微信没法比，但是公司里每个人给我的感觉都是朝气蓬勃的，我想这个可能就是创业公司的氛围吧，我觉得我应该是来对地方了。</p><p>我来的前两天主要在熟悉公司的各种文档，以及了解我们的代码仓库、提交、发布流程等等。不过因为有微信的历练，所以这些东西我上手的速度很快。我在第三天的时候已经修复了一个线上问题，第九天的时候已经发布了一个需求，而这个需求是当时产品和设计师特地要等我入职后给我做的需求。我的上手速度远超她们的预期，以至于她们都没准备好我下个需求。所以在没有业务需求的时候，我就干起技术相关的东西。我发现 MoeGo 的一些基建有可以改善的地方，我就自己花时间做了一些技术重构。</p><p>其实来 MoeGo 之前，我的面试官（后来也是我的 mentor，不过在 MoeGo，mentor 叫做 buddy，会更亲切点）问过我一个问题「你是更愿意做技术，还是更愿意做业务呢？」我说其实我没有什么偏好，我在微信主要做技术，我来 MoeGo 我也愿意做业务。</p><h3 id="从-IC-到-Manager"><a href="#从-IC-到-Manager" class="headerlink" title="从 IC 到 Manager"></a>从 IC 到 Manager</h3><p>3 月初的一天，我的 leader 把我喊去 1-1（每个新人入职基本他都会 1-1 一下）。所以本来我以为就是普通的一个 1-1，聊聊入职感受啥的。但是没想到他说“这次呢还有一个事情，招你的时候，是打算招个前端 manager 的。” 我说我在面试和 offer 阶段都没有收到过这个消息，还是有点惊讶的。然后我也说我之前没有做过 manager，不确定能不能做好。他鼓励我说没关系，可以慢慢来，我们觉得你可以做好。然后就跟我聊了下我后续要负责的业务和团队，对我的预期等等。</p><p>1-1 后，我跟我老婆说了这事，其实当时还是蛮激动的。不过说实话我之前没有当过 manager，我也真不知道要怎么做。不过我想清楚的一个事情是，我不希望做成以前我遇到的 manager，我希望我能够对我的组员负责，能够帮助他们有所成长。</p><p>一开始我的团队加上我只有三个人，我们主要负责了两个方向，所以大概是其中一个人负责一个方向，我和另一外一个同学负责另一个方向，同时我还兼顾团队管理、项目管理、技术选型等一系列工作。这个阶段里，我其实对于 manager 的认知还没有那么深。因为人少，而且创业公司业务发展快，所以我并没有就做成了一个甩手掌柜，也是深度参与了一线开发。在 23 年一整年，我写的代码量，大概是整个微信时期的好几倍。当然因为业务不一样，所以代码行数并不能说明太多，但是至少那段时间我觉得我工作还是很开心的，我的产出也一直很高效。我觉得做的东西有用户在使用，并且每周我们还有内部的一些 post，能够看到用户真的很喜欢我们的新功能。那个时候成就感真的很足。</p><p>后来没多久，我就成了面试官，参与公司的前端岗位的候选人面试。这也是我第一次做面试官，为此还旁听了好几场的面试，最后大概了解了面试的时候需要考察候选人的内容、标准。23 年入职的前端工程师，大部分都是我面试通过后拿到 offer 的，为此我自己私下感觉还挺自豪。</p><p>一开始，公司还没有严格的绩效制度，也没有职级。正常表现的话，年底的年终就是按合同上的发满 3 个月，所以其实整个公司的氛围还是挺好的，很难和<strong>卷</strong>这个字眼挂上钩。但是不卷不意味着大家都躺平，相反，大家基本上都是很有责任心的，哪怕公司规定的下班时间是 6 点半，还是有不少同学因为项目进度会主动留下加班，只为项目能顺利上线。</p><p>后来出了个事情，让我对 manager 这个岗位，有了更深刻的认知。有段时间有其他团队的不同同学都通过直接或者间接的方式跟我反馈我团队里的 G 同学跟他们合作的时候表现不太好，他思维太过发散，经常很多开会决定的事情他在做的时候又要发散一下，导致做项目容易 delay，以至于有些同学一听要跟他合作就头疼。不过因为他跟我直接合作一起做过一个项目，我对他整体的技术背景、为人处事态度都有所了解。所以我觉得这个同学还是有机会能够改善的。于是我跟他开始了不定期的 1-1，跟他沟通了问题，我也给出了我的一些解决方案，然后我跟他一起尝试从后续每个项目开始做改进。同时，他自己也意识到了同样的问题，他对我提出的点也非常虚心的接受，也跟我一起想办法去做改善。半年后，他已经是大家认为有十足进步的同学；从 24 年到今年他已经拿到了多次高绩效。这个事情应该可以算是我在 MoeGo 当 Manager 的一个缩影，我跟 G 同学一起合作变好的这件事情给了我很大的鼓励，我觉得我真的帮助了我的组员变得更好，而且我也有这个能力。</p><p>再后来，有次去深圳跟组员们一起聚餐（在此之前我们公司聚餐基本都是先用当时一年一人 200 的经费团建，经费用完了就 AA），那一次我觉得作为组长，请大家吃顿饭应该是很正常的事情，所以我那天饭后就把单付了。当大家后来得知是我请客的时候，他们甚至觉得不应该让我出钱，应该让公司出钱😂，以至于他们最后甚至又重新把饭钱汇总了还给我，说「这次不算，大家之前都说好是 AA 的，下次你再请我们」。我老婆听到这事情之后，都震惊了，怎么还会有组员主动不要组长请客把钱退给组长的。那一刻，我还是挺感动的，我觉得我对大家真心的付出，大家收到后也会真的爱我。当然后来我跟他们说好了下次去深圳，请他们吃饭，最后我们一起吃了顿挺好吃的烧烤。</p><p>23 年下半年开始，随着公司不断有新人入职，组织架构变大，公司要开始研究绩效制度，开始研究职级、绩效制度。我们这些小组长有段时间每周都要被拉去开会，商量什么样的职级、什么样的绩效制度等等。因为大家之前也没啥经验，也基本就是依葫芦画瓢，然后再根据我们公司内部的情况，做一些调整。最后出来的方案，其实从我的视角来说还是个比较能让人接受的方案。不过最终在后来都被推翻了。</p><h3 id="持续大半年的项目"><a href="#持续大半年的项目" class="headerlink" title="持续大半年的项目"></a>持续大半年的项目</h3><p>23 年底，有个非常大的项目，关乎着 MoeGo 是否能从服务单店的模式扩展到服务多店甚至是连锁店、加盟商这种 enterprise 级别的商家。为此我们需要对整个系统，从底层的数据模型，到上层的 UI 界面都做非常大的重构，来支持 enterprise 级别的商家，而我被分配做这个项目的前端 owner。说实话，我一开始其实并没有想到这个东西的面积和复杂度会如此之大，以至于产研一开始的计划甚至是 24 年农历新年前能够完成。</p><p>一开始这个项目只有我一个前端在介入，因为其他人需要把手头的需求了结之后才能投进来。这个过程中我除了要做已经明确的重构需求之外，我还需要把整个系统需要改造的部分摸清楚，划分清楚哪些模块要做什么样的改造；从而到时候有其他同学能够来一起参与需求开发的时候能够知道做什么，怎么做。</p><p>这个对我来说其实还是蛮有挑战性的一件事情，因为以前大部分时候我基本上要么是自己独立开发，或者就是跟一两个同学一起合作开发，做的事情都比较独立或者聚焦。而这次我需要协调公司里几乎一半左右的前端同学一起完成这个非常大的项目。我们的项目里又经常有牵一发动全身的模块，改一个地方可能很多地方会受到影响，就像一张蜘蛛网，近看是根线，远看是个网。其实身处其中的时候还没有这种很强烈的感受，因为我就是当做一个需求在推进。等到做完第一个里程碑之后，我才回过神来，原来我已经做到了。</p><p>这个项目持续了大半年，我们最终分成好几个阶段交付。虽然跟一开始的预期差距有点大，但是至少我们在年前完成了关键的里程碑，意味着我们已经可以迁移用户到这套新的系统里了。于是年后回来到 7 月中，我们在做的事情就是不断迁移老的用户到新系统里，直到 7 月中，所有的用户都用上了新系统。其实在我加入 MoeGo 之前，也有过一次尝试，想做到类似这次的重构，但是那次因为人力的原因没有实现；而我这次把这个事情落地了，也因此拿到了 Outstanding 的绩效，还晋升涨薪了，这对我来说是个莫大的肯定。</p><p>而我还在这个项目收尾的阶段，就又被调去负责另一个会直接影响公司发展路线的项目——去支持新的商家运营模式，从 Grooming 到 Boarding &amp; Daycare。</p><p>总得来说，这段时间在 MoeGo 过得虽然也挺辛苦，但是很快乐，很有成就感，为此我在少数派上发表过一篇文章<a href="https://sspai.com/post/86982">《远程工作一年，是什么样的体验》</a>。</p><h3 id="从-Grooming-到-Boarding-Daycare"><a href="#从-Grooming-到-Boarding-Daycare" class="headerlink" title="从 Grooming 到 Boarding &amp; Daycare"></a>从 Grooming 到 Boarding &amp; Daycare</h3><p>Grooming 其实只是宠物服务领域一个比较垂直，比较小的类别。这种类别的大商家不多，大部分都是中小型商家。那么宠物服务领域的大商家在哪里呢？就是 Boarding &amp; Daycare 这类商家，翻译过来就是寄养、日托。这类商家基本上都是开实体店的，而且因为要寄养，场地、店面也会更大一些，每日的订单、流水也会更多。而且有些商家还是混合型的，也就是既能提供 Boarding &amp; Daycare 的服务，又能提供 Grooming 的服务，还有提供其他比如 Trainning 、Dog walking 等服务。总之，这类商家的体量更大，能提供的服务更多，收入也更高，市场地位也更强。所以 MoeGo 想要在宠物服务领域 SaaS 做大做强，只支持 Grooming 是肯定不够的，支持 Boarding &amp; Daycare（下文简称 BD） 乃至其他服务类型的商家是必然。</p><p>为了兼容原来的 Grooming 商家的体验和生态，我们一开始并没有办法做一套全新的系统，而是在 Grooming 已有的一些模块上继续增加 BD 相关的能力；有些模块是 Grooming 所没有的，那种模块倒是可以重新开始实现。</p><p>我们的 PM 和设计师还专门去美国出差，做用户调研，访谈，把用户的需求转成我们的产品功能，再通过研发把功能实现交付给客户。整个事情其实是挺合理的，不过我后来才知道，BD 的商家在用 MoeGo 之前，基本上都会使用的一个很成熟的软件叫做 Gingr。它已经在 BD 领域深耕十多年了，功能已经非常完备、成熟。而 MoeGo 对于 BD 的支持几乎可以说是从 0 开始，很多用户已经在 Gingr 里用得很顺手的功能，在 MoeGo 这边要么没有，要么得用一些 workaround 才能用起来。不过不管怎么样，我们在 24 年底通过近半年的努力，还是跑通了一个 BD 商家基本的日常操作流程，成功接入了第一个 BD 商家。另外，对于 MoeGo 而言有个好消息，Gingr 由于被收购，经过资本市场的一系列操作之后，它只剩下数量可怜的维护人员，基本不再迭代了。换句话说，只要我们稳扎稳打，Gingr 的用户迟早都会是 MoeGo 的，因为 MoeGo 已经完成了支持 enterprise 级别商家的基础，市面上除了 Gingr 以外其他的都还在早期阶段。</p><p>不过老板们不这么想，也可能资本市场时间就是金钱。总之在我们还在打磨 BD 在 MoeGo 生态里的各种功能、体验问题，还没做好吸纳市面的其他客户的时候，前线已经传来了我们已经在跟XX、YY等大商家接触、承诺、签单的消息了。很多东西我们还没做，就已经被承诺出去。导致原本我们是正排的时间线，突然之间变成了倒排。而且在当时的人力资源紧张下，大家只能加班加点，赶在 DDL 之前去完成这些需求。就在这种紧张的工作节奏下，我们即将迎来年底的绩效考核。</p><h3 id="裁员"><a href="#裁员" class="headerlink" title="裁员"></a>裁员</h3><p>25 年年初，MoeGo 迎来了第一次非常严格的绩效考核，每个团队的每个组员的绩效，组长给了多少分，为啥给这样的分，给多了还是少了，需要 HR、CTO 以及跨团队的另一个高职级的同学来一起做校准，这个校准的标准，是直接拿着字节的职级标准来过的。那次我记得我特地去了趟深圳，从下午 3 点，一直跟他们对到了晚上 8 点半。我有两个组员在非常严格的标准下，被降成 need improve（1-5 分制的 2 分）。在过程中，他们 push 到我的点是哪怕是 need，他们也想要直接裁员。我举了之前我跟 G 同学一起改善成功的例子，但是他们回绝我的说辞是「你的时间很宝贵，不应该浪费在这种事情上。每年毕业生、找工作的人那么多，再招个更好的就行了」，我当时内心非常震惊，因为他们确实有做的不够好的地方，但是完全不至于被打 2 分，更不至于被裁员，更何况我们的业务还在发展，还缺人呢。从这里开始，我发现我们在价值观上已经出现了一些偏差。</p><p>后来，MoeGo 就开始了一轮非常大规模的裁员，我组里那两个被打了 2 分的同学，最终也没有例外。这次裁员有几个很明显的点，尤其像非业务团队，比如 QA，就是裁员到留下能够勉强 cover 公司业务的人数；「性价比不够高」的同学是重灾区；我们组的 N 同学，被裁员的当天，他在收拾东西的时候笑着说好难过；K 同学，被通知要裁员的时候直接大脑一片空白，许久才说了一句话。我那天在广州的屏幕前也哭了。由于那次的年终还没发放，我就问 HR 说他们的年终还能发放么？HR 说决定权在我手上，我是愿意发他们的年终，还是把这笔钱省下来发给留下来的人。我没有犹豫，我说我觉得他们应该拿到这笔年终，哪怕 2 星只有一半的年终。所以他们应该是当年那波被裁的同学里，少数的既拿到了年终又拿到了裁员补偿的同学，我想这是我能为他们做的最后的一件事情了。</p><p>除了年初这波大规模裁员以外，后面陆陆续续还有裁员，这些裁员就是非常突然的了。我记得一个非常让人哭笑不得的事情是，某个团队某个同学 Q，被裁员的当天下午 5、6 点才突然被 HR 拉着告知被裁员，以至于她手上当天还有待发布的项目没得发；而且事情非常突然导致她的 PM 也找不到她，还不知道发生了啥，在群里急着找人，其他同学也不知道发生了啥，人就不见了，真是让人哭笑不得。</p><p>最后说说我自己的绩效吧，我本来以为至少能有个 meet（3 分），毕竟 24 年我做了那两个特别大的项目，还是有业务产出的。结果是 2 分，然后还有两个月的年终被扣了。CTO 跟我说了一堆我还有待改善的点（比如什么我毕竟刚当 manager，当然还有很多需要 improve 的地方巴拉巴拉），同时还给了几个任务，说是完成了任务之后，那两个月年终就能拿回来。当时我并没有多想，因为听说 24 年，BD 的业务结果并不好，Manager 要承担业务结果，我能理解，我认为 25 年应该会做得更好点，到时候再拿回来就是了；不过后来我才知道，几乎所有的 manager 这年的年终都被扣了一部分，说辞也都大同小异，这就很好玩了。</p><h3 id="330-530-1230？"><a href="#330-530-1230？" class="headerlink" title="330 - 530 - 1230？"></a>330 - 530 - 1230？</h3><p>经历完年初的裁员风波之后，我们的业务还是要继续。很快来到 3 月份，之前说的给用户承诺的东西逐步要开始兑现了，但是我们发现很多东西都没实现或者只有个 DEMO 版本。因此整个公司开始动员，用 330（3 月 30 日）作为一个时间节点，鼓动大家争取在 330 之前完成一些必要功能的实现。同时，因为 BD 这边的 PM 设计师以及后端的组长去美国出差，国内办公室需要有人带，所以我还去深圳待了 3 周。</p><p>本来以为 330 是终点，做完 330 的需求后我们可以重新按自己的规划来走。结果在 4 月 27 的时候开了个会，CTO 拉着产研团队，说现在事情非常紧急，来自美区的 CEO\GTM 团队发现大商家需要的很多功能，我们还没实现，我们必须要把大商家他们高优需要的功能实现了，才能避免他们离开我们的平台。</p><p>为此我们发动了 530 的「战役」，要在 5 月 30 日之前，完成大商家所需要的需求（当时说时间窗口只有一个月）。其实我们的整体规划里，很多东西其实不是不做，而是当下这个时间点会做其他更高优的需求；但是大商家的需求一来就变成了最高优先级，频繁插入打断我们原本的节奏。530 期间，CTO 提出的点是要把大商家的需求跟我们原本的需求 merge 成一条线；又因为大商家之前基本都是用的 Gingr，所以我们需要把 Gingr 里大商家需要，但是我们又没有的功能，立即抄过来。</p><p>所以我取消了原本规划的五一出行，甚至五一期间我还加了几天班，就是为了跟 PM、其他组长确定我们五一回来之后，需要做哪些需求，哪些需求谁来做，需要做多久等等。其实我们当时拿到最初的需求清单，第一眼反应就是根本做不完。最终的结果也是，我们在一系列已经被认为是高优的需求里，再次筛选出一波高优需求，在现有的人力资源的情况下，5 月份我们加班加点最终也没完全做完那些需求。有些需求甚至到 7、8 月份才最终做完。</p><p>在 530 开始前，我就在想这个东西真的能做完吗？要是做不完又会怎么样呢，公司会因此倒闭了么？为什么要给用户承诺那么多我们还没有的能力？然后用户进来了以后发现跟承诺不一样又生气想要离开，这种不是对我们平台伤害更大么？这也是第一次，我萌生了想要离开 MoeGo 的想法，因为我觉得这个阶段公司的种种行为、决策，我已经开始无法理解了。不过说来也巧，跟我一直以来配合的后端组长，也来找我聊这个事情，他也想走了。我劝他说可以再留一个月看看 530 之后会变成啥样。很快，他跟 CTO 提了离职申请，CTO 拉着他痛哭流涕最终把他成功劝下（结果没想到最后他留下了，我走了，哈哈），他说他被 CTO 画的未来愿景给说服了。</p><p>530 这个日子过去之后，530 的战役并没有立即结束。我们虽然没有完成所有的东西，但是还是完成了一些非常高优的需求，而这些完成的需求又暂时缓解了前线跟用户之间的冲突。所以我们在 530 之后继续补作业。虽然没有名义上的 630，但是整个团队的节奏依然还是非常紧张。</p><p>就在这个节骨眼上，发生了一件让我非常难以接受的事情，也是这件事情让我最终决定离开 MoeGo。有一次 CTO 在听产品汇报 demo 的时候，问了一下某个功能花了多少人日做完的，他认为只要几天，结果发现花了前端两人周，后端一人周，就很生气，就来问前端为啥需要花这么久的时间。实际上，需求本身看着很简单，但是背后的逻辑很复杂，而且我们经过长时间紧张的开发，已经积累了相当多的技术债。而且这两个前端同学我知道他们都不是摸鱼划水的同学，他们需要做这么久的时间也都是有原因的。我解释了一下大致的情况，但是 CTO 并不买单，拉了更多的人想看看到底怎么回事。于是我陷入了自证陷阱，拉着两个同学晚上紧急做了个复盘。我之所以这么着急是因为我不希望这件事情，影响到他们即将开始的年中绩效评估。结果 CTO 依然不买单。最终我拉上了 CTO 比较信任的另一个前端组长来一起证明这个东西确实需要花这么长的时间，才最终算是告一段落。</p><p>其实这个时候，我觉得在 MoeGo 工作已经不是很开心了，我每迈一个步子都得非常小心翼翼。但是为了即将开始的年中绩效评估，我还是想着给组员们站好最后一班岗再走。谁曾想，这个绩效评估原本是 8 月底结束，结果一直持续到了 9 月底。我跟每个人都仔细过了他们的自评材料，需要晋升答辩的同学我来回跟她对晋升材料 5、6 次。结果有些同学的评分，我还是没能争取到满意的结果。甚至有的同学大家都认为应该晋升的没晋升；大家都认为不应该晋升的结果晋升了；这里面的门道我就不多说了。总之，此时的我已经失去了继续往前跑的动力了。</p><p>而且我的身体情况确实在今年也变差了很多，年中体检的时候依然查出了高血脂（去年也有）；另外头上有颗痣，去年到今年变大了好多，最终去医院查了查，医生建议要做手术切掉。国庆回来后，我就请假去做了手术，把头上的痣切了。做完手术没多久，我就提出了离职申请。离职原因我说了主要是身体原因，我想休息一段时间。在预料之中，我的离职申请很顺利，也没有什么挽留，没有什么阻碍，甚至还被要求写一封全员信告知我要离职的原因，让大家不要有太多「恐慌」。</p><p>其实从 530 开始到现在，我感觉公司整体上的运作还是没有发生本质的变化，依然是被大商家的需求 push，而不是用做 SaaS 的方式去做软件。离职前，听说又要 “1230” 了。</p><p>25 年 11 月 21 日，我离开了即将待满 3 年的 MoeGo。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>在 MoeGo 的日子，其实跟微信有点像，也能分上半场和下半场。只不过上半场持续的时间会更久一些，原本我甚至觉得能够在 MoeGo 跟公司一起干到上市。虽然技术深度和技术难度，MoeGo 比不上微信，但是在这里我确实看到了一个产品是如何解决了用户的痛点，并且用户愿意为此买单，从而能够 cover 我们的成本，并最终实现盈利的。这个也正是我从微信离职的时候，想要在下一家公司里学习到的东西。MoeGo 的商业模式我是认可的，所以这也是我愿意从微信离职入职这样一家创业公司的原因。</p><p>我在 MoeGo 做的两个最显著的贡献，一个是帮助 MoeGo 可以从服务单店到扩展到多店、连锁店、加盟商的模式；第二个是帮助 MoeGo 可以从仅支持 Grooming 到支持了 Boarding &amp; Daycare 等运营类型的商家。而这两个结合起来，则成为了我们今年能够去争取 Boarding &amp; Daycare 大商家的资本。早期我也看到了，创业公司并不是什么事情都能做得正确，也会存在一些试错的项目，不过大家的想法都是想让公司更好，哪怕做错了，也不会有责怪，而是好好反思之后如何能做得更好。公司虽然存在一些问题，但是大家的冲劲、跟着公司一起成长的感受能够弥补这些缺陷。</p><p>不过随着时间的推进，公司规模的扩大，很多事情就开始变味了。24 年初的时候，HR 还在公司内部发了全员的 NPS 调研。我记得当时国内的 NPS 分很低，HR 说以后 NPS 会定期做，公司收到了大家的反馈，会一点点变好的。但是后来再也没有做第二次 NPS，之前承诺要变好的东西也一直没有落实。我一直觉得公司其实对内也算是在经营一款产品，它的目标用户应该是公司的员工。当我看到 Glassdoor 上美区的员工给到公司非常低分的评价，公司并没有因此做出一些改变的时候；当我看到国内的氛围在逐步变差，而公司的行动总是慢好几拍的时候，我感觉很难受。因为我觉得，如果一家公司如果员工都没法 serve 很好的话，它的员工做出的产品大概率也不会带有爱。尤其我们的产品跟宠物有关，感性的成分其实应该不能被忽视。以前我刚加入的时候每周还有公司提供的运动经费，鼓励大家去运动，后来取消了；以往每年 5 月 20 日还有个节日福利，可以让大家给家人买礼物、请家人吃大餐等，公司报销 520 元，今年也无声无息取消了，而且正好卡在 530 紧张的节点，大家正需要一些鼓励、一些关怀的时候。虽然后面通过其他方式「回来」了，但是重点在于那次无声无息的消失。</p><p>回想这几年，数不清多少次半夜被微信群、oncall 电话吵醒；数不清多少个节假日出门在外还必须带着电脑只为随时能解决用户问题；数不清为了解决用户反馈的哪怕很小的问题，花了多久时间去排查。其实解决用户的问题，得到用户的好评的时候，还是会很有成就感的。这些其实都是因为热爱，大家希望把事情做好，也希望以后公司能够看到大家的努力，能够认可大家的付出。</p><p>当然，作为老板站的角度跟我可能不一样。他更关心的是整个公司在资本市场上的占有率，关心投资回报率。公司活下去、增长是第一要素。CTO 的一些做法是个双刃剑，老板选择了接受其中的一部分，就必然要舍弃另一部分。所以从价值观上，我已经开始无法能够接受，于是我选择了主动离开。我觉得这个事情没有对错，只有适不适合。我觉得创业公司本来就是一个不断试错的过程，适合我的公司，我就留下来；不适合我的公司，我就离开。</p><p>不过在 MoeGo 的这一段时光，也还是很难忘。很多同事成了很好的朋友；很多事情，也让我成长许多。未来，我不会再为了所谓的职级、晋升这种事情被 PUA 了，这其实只是管理手段罢了。我想如果我之后要自己创业、做自己的事情，我看待事物的角度，应该比两三年前的我，会更丰富些。</p><p>最后，感谢在 MoeGo 认识的朋友们，感谢你们的支持与帮助，我自认为我做得还不错，就是遗憾无法跟你们一起走到最后了。离职前，很多同学送了我礼物，也让我非常感动，谢谢你们🙏</p><p><img src="https://pics.molunerfinn.com/blog/A66CAE9F-454F-41B6-A267-EC8B66DEA359_1_105_c.jpeg" alt="组员送的乐高"></p><p><img src="https://pics.molunerfinn.com/blog/64F02D3A-47CD-4F90-A3BC-83C6E2821F44_1_105_c.jpeg" alt="组员们集资送我的 3D 打印机"></p><p><img src="https://pics.molunerfinn.com/blog/91cb0e22b8cd9d7c495a0da6926f1724.jpg" alt="小伙伴送的盲盒"></p><p><img src="https://pics.molunerfinn.com/blog/7cbddd730addb5c7ee9849a40ec759c8.jpg" alt="小伙伴送的显示器挂灯"></p><p><img src="https://pics.molunerfinn.com/blog/5c47b72934f2b17eff8b4dc42ec66273.jpg" alt="公司送的按摩枕"></p><h3 id="开心的事-1"><a href="#开心的事-1" class="headerlink" title="开心的事"></a>开心的事</h3><p>跟在微信时期一样，在 MoeGo 的这几年里，生活中还是有很多事情是很开心的。</p><p>23 年的五一假期，我们去了成都，看到了胖乎乎的熊猫，跟在电视里看到的感觉还是有很大不同，非常可爱，我们甚至还拍到了 5 只熊猫排排坐吃竹子哈哈。</p><p><img src="https://pics.molunerfinn.com/blog/257715F8-AFE6-465B-9B73-1329B284D779_4_5005_c.jpeg"></p><p>同月，跟老婆去拍了婚纱照</p><p><img src="https://pics.molunerfinn.com/blog/LZ2304080025-0078.jpg"></p><p>7 月，老婆神级的手速抢到了蔡依林的演唱会。时隔多年去听演唱会，现场的感觉还是很棒的。</p><p><img src="https://pics.molunerfinn.com/blog/00E9121E-D1FF-4F11-9E9D-BAA82FAC0229_1_105_c.jpeg"></p><p>9 月，我们还一起去听了西城男孩的演唱会，My love 一出口，就知道有没有啊。</p><p><img src="https://pics.molunerfinn.com/blog/EDF39FA2-5EFB-4DD7-A9F2-F15EF7E3AE17_1_102_o.jpeg"></p><p>10 月去听了五月天的演唱会，第一次在内场听演唱会，其实体验很不好，因为全程只能被迫站着了 hhh。</p><p><img src="https://pics.molunerfinn.com/blog/FEAE74CE-542F-42E1-8A25-1356F94A917B_1_105_c.jpeg"></p><p>24 年 1 月初，在老家办了婚礼，去参加的同学都说是吃过的最好吃的婚宴，不过我们两个自己都没咋吃到，哈哈。</p><p>24 年 4 月，我们买了自己的🚗。因为新房 6 月就要收楼了，我们在此之前要到处去看材料、家具之类的，有辆车还是比较方便的，所以就买啦。</p><p>6 月，跑去惠州听了杨千嬅的演唱会，现场还来了山鸡哥，值回票价！（这一年来真的听了好多演唱会呀）</p><p><img src="https://pics.molunerfinn.com/blog/DF91818F-1C86-4F36-8CAB-CE469CF61302_1_102_o.jpeg"></p><p>6 底月到 10 月，因为新房收楼啦，我们就不得不三天两头往新家跑，监督装修，选材料，选家具家电啥的，最终在 10 月份把装修都弄完了。装修的坑，如果之后我有时间，也可以再专门写写。</p><p>9 月份的时候，趁着中秋假期，请了几天假，跟老婆去了趟新疆（南疆），我们先落地喀什玩了两天，然后自驾前往塔县，在喀什往返塔县的路上再顺路玩一些其他的景点。除了人文、美食，我觉得南疆的风景也很👍。</p><p><img src="https://pics.molunerfinn.com/blog/IMG_2495.jpg" alt="不知道是什么雪山，在路途中看到的，很壮观"></p><p>去盘龙古道的时候，有个标语感觉很赞。希望人生尽是坦途吧，哈哈。</p><p><img src="https://pics.molunerfinn.com/blog/CBE070A8-F214-4AFD-A7C9-BBE2029C4D59_1_105_c.jpeg"></p><p>也是这次旅行，让我感觉我真的是很喜欢开车，我很享受开车给我带来的放松的感觉。</p><p>25 年 3 月 8 日，我们正式搬入了新家啦。（不过我刚搬进新家，第二天我就被叫去深圳出差三周了。）</p><p><img src="https://pics.molunerfinn.com/blog/8197A1A7-9F9D-41CF-B4E5-23F707A091A9_1_102_o.jpeg"></p><p>6 月份的时候，终于瞅准一个空隙，请了一周的假去了趟西藏。除了有点高反以外，其他真的都很棒，西藏的风光也是独一档。我真的好喜欢开车啊！</p><p><img src="https://pics.molunerfinn.com/blog/C5849684-BA81-4FCC-B947-C7E844FF5722_1_102_a.jpeg" alt="羊卓雍错，原图直出"></p><p><img src="https://pics.molunerfinn.com/blog/73A7FF7C-48FF-4391-B706-AC7A51C387BC_1_102_a.jpeg" alt="巴松措，原图直出"></p><p>25 年 10 月，Warp （一个很好用的终端工具）发来一个要赞助 PicGo 的邮件，让我非常惊喜，我没想到 PicGo 这个其实还没怎么做海外推广的项目，居然能收到来自海外团队的赞助。</p><p><img src="https://pics.molunerfinn.com/blog/image%25207.png"></p><p>后来我跟他们简单交流了一下，愉快地接受了他们的<a href="https://github.com/sponsors/Molunerfinn">赞助</a>（不多，但是得到的肯定是无价），前三个月会是个试用期，先看看效果再决定继不继续。为此我还需要开通 Stripe 和 GitHub Sponsor 功能，这个我可以后续放到一个单独的文章里说。</p><p>最后，PicGo 在 MoeGo 期间只发布了一堆 beta 版本，现在我终于有时间来发布它的正式版了。</p><h2 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h2><p>我的<a href="https://molunerfinn.com/">博客</a>从 2020.05.30 之后再也没有更新，今天是 5 年半以来第一次更新，好多以前想单独写的事情，因为各种原因没写成，最后都放到这篇文章里了。</p><p>回顾这 8 年，有高光也有低谷。PicGo 从 0 开始，达到了非常多的里程碑： 25k 的 GitHub Star； 100 多万次的下载（仅 GitHub Release）；接近 100 个社区贡献的插件、周边； Typora、Marktext 等文本编辑器官方支持的图片上传工具； Warp 的 Sponsor 等等。现在，一旦你遇到了需要图片上传获取链接的场景，PicGo 依然是你的第一选择。</p><p>而 “我想过一个什么样的人生”？我好像还没有一个非常清晰的答案，但是我逐渐想清楚的事情是，我不想因为所谓的职级、所谓的晋升、所谓的大厂光环这些东西去奋斗一辈子。其实这次跳脱出来，我发现程序员这个身份，很多时候是自己给自己上的枷锁——世界那么大，有那么多有趣的事情，有那么多有意义的事情，我为何要在这里或者那里浪费我仅有的一次生命？我 gap 又如何，我找不到下一份程序员工作又如何？职级是 P7 是 P8 又如何？只为这些人为划定的标签去奉献你的青春，想想其实有的时候还挺可悲。有好多事情我想做，但是一直没机会做——我想开车自驾漫无目的，我想把英语口语练好，我想学钢琴，我想学会做很多好吃的菜…… 说真的，世界上不是只有互联网一个领域，也不是只有大厂员工的身份才足够体面。这些套在头上的紧箍咒往往当你开悟的那刻，它就不再能束缚你的自由——我愿意从微信跳槽去当时不到 50 人的创业公司，我也愿意舍弃即将年底要发放的年终奖、创业公司给我的期权，给自己放个假，毕竟生命只有一次，开心最重要。</p><p>我很喜欢孙燕姿《开始懂了》里的一句歌词：<strong>开始懂了，快乐是选择。</strong></p><p>今天，在 PicGo 即将迎来 8 岁生日之际，我发布了它的 2.4.0 正式版；而我于今天也迎来了我 30 岁的农历生日。30岁，对我来说是个新的起点，我得好好地想想我想做什么。在这里非常感谢我的老婆、我的家人、我的朋友能够在我做各种抉择的时候支持我的决定。人生的长跑，终点都一样，但是我可以有机会选择不同的起点。而你，我的朋友，未来的我们又会在哪里相遇呢？</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;2017 年 11 月 28 日，还在大学宿舍里的我，提交了 PicGo 的第一个 commit，距今已经 8 年。现在看来，时间是过得真快啊。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://pics.molunerfinn.com/blog/image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果你不知道 PicGo 是什么东西，也不会妨碍你阅读。从我提交它的第一个 commit 的那刻起，命运的齿轮就已经悄悄开始转动。我的研究生生涯、实习、工作都有它的影子，甚至我自己想要做的事情，我想学习的东西，也受到它很大的影响。&lt;/p&gt;
&lt;p&gt;正巧前段时间，前微信的一个同事发的一篇文章&lt;a href=&quot;https://mp.weixin.qq.com/s/avHKLvLqO-8AKDd00peTOw&quot;&gt;《从广深到北美——本科毕业这四年》&lt;/a&gt;也让我内心深深地触动。因为我跟他一样，我也问过我自己这个问题：“我想过一个什么样的人生”。&lt;/p&gt;</summary>
    
    
    
    <category term="日志" scheme="https://molunerfinn.com/categories/%E6%97%A5%E5%BF%97/"/>
    
    
    <category term="随笔" scheme="https://molunerfinn.com/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>Vite 原理浅析</title>
    <link href="https://molunerfinn.com/learn-vite/"/>
    <id>https://molunerfinn.com/learn-vite/</id>
    <published>2020-05-03T10:08:00.000Z</published>
    <updated>2026-03-08T01:14:37.985Z</updated>
    
    <content type="html"><![CDATA[<p>已经好久没有写博客了。本文不说 Vue3.0 了，相信已经有很多文章在说它了。而前一段时间尤大开源的 <a href="https://github.com/vuejs/vite">Vite</a> 则是一个更加吸引我的东西，它的总体思路是很不错的，早期源码的学习成本也比较低，于是就趁着假期学习一番。</p><p>本文撰写于 Vite-0.9.1 版本。</p><span id="more"></span><h2 id="什么是-Vite"><a href="#什么是-Vite" class="headerlink" title="什么是 Vite"></a>什么是 Vite</h2><p>借用作者的原话：</p><blockquote><p>Vite，一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports，在服务器端按需编译返回，完全跳过了打包这个概念，服务器随起随用。同时不仅有 Vue 文件支持，还搞定了热更新，而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打包。虽然现在还比较粗糙，但这个方向我觉得是有潜力的，做得好可以彻底解决改一行代码等半天热更新的问题。</p></blockquote><p>注意到两个点：</p><ul><li>一个是 Vite 主要对应的场景是开发模式，原理是拦截浏览器发出的 ES imports 请求并做相应处理。（生产模式是用 rollup 打包）</li><li>一个是 Vite 在开发模式下不需要打包，只需要编译浏览器发出的 HTTP 请求对应的文件即可，所以热更新速度很快。</li></ul><p>因此，要实现上述目标，需要要求项目里只使用原生 ES imports，如果使用了 require 将失效，所以要用它完全替代掉 Webpack 就目前来说还是不太现实的。上面也说了，生产模式下的打包不是 Vite 自身提供的，因此生产模式下如果你想要用 Webpack 打包也依然是可以的。从这个角度来说，Vite 可能更像是替代了 webpack-dev-server 的一个东西。</p><h3 id="modules-模块"><a href="#modules-模块" class="headerlink" title="modules 模块"></a>modules 模块</h3><p>Vite 的实现离不开现代浏览器原生支持的 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">模块功能</a>。如下：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;module&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; a &#125; <span class="keyword">from</span> <span class="string">&#x27;./a.js&#x27;</span></span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>当声明一个 <code>script</code> 标签类型为 <code>module</code> 时，浏览器将对其内部的 <code>import</code> 引用发起 <code>HTTP</code> 请求获取模块内容。比如上述，浏览器将发起一个对 <code>HOST/a.js</code> 的 HTTP 请求，获取到内容之后再执行。</p><p>Vite 劫持了这些请求，并在后端进行相应的处理（比如将 Vue 文件拆分成 <code>template</code>、<code>style</code>、<code>script</code> 三个部分），然后再返回给浏览器。</p><p>由于浏览器只会对用到的模块发起 HTTP 请求，所以 Vite 没必要对项目里所有的文件先打包后返回，而是只编译浏览器发起 HTTP 请求的模块即可。这里是不是有点按需加载的味道？</p><h3 id="编译和打包的区别"><a href="#编译和打包的区别" class="headerlink" title="编译和打包的区别"></a>编译和打包的区别</h3><p>看到这里，可能有些朋友不免有些疑问，编译和打包有什么区别？为什么 Vite 号称「热更新的速度不会随着模块增多而变慢」？</p><p>简单举个例子，有三个文件 <code>a.js</code>、<code>b.js</code>、<code>c.js</code></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// a.js</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">a</span> = (<span class="params"></span>) =&gt; &#123; ... &#125;</span><br><span class="line"><span class="keyword">export</span> &#123; a &#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// b.js</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">b</span> = (<span class="params"></span>) =&gt; &#123; ... &#125;</span><br><span class="line"><span class="keyword">export</span> &#123; b &#125;</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// c.js</span></span><br><span class="line"><span class="keyword">import</span> &#123; a &#125; <span class="keyword">from</span> <span class="string">&#x27;./a&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; b &#125; <span class="keyword">from</span> <span class="string">&#x27;./b&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">c</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">a</span>() + <span class="title function_">b</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> &#123; c &#125;</span><br></pre></td></tr></table></figure><p>如果以 c 文件为入口，那么打包就会变成如下（结果进行了简化处理）：（假定打包文件名为 <code>bundle.js</code>)</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// bundle.js</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">a</span> = (<span class="params"></span>) =&gt; &#123; ... &#125;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">b</span> = (<span class="params"></span>) =&gt; &#123; ... &#125;</span><br><span class="line"><span class="keyword">const</span> <span class="title function_">c</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">a</span>() + <span class="title function_">b</span>()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> &#123; c &#125;</span><br></pre></td></tr></table></figure><p><strong>值得注意的是，打包也需要有编译的步骤。</strong></p><p>Webpack 的热更新原理简单来说就是，一旦发生某个依赖（比如上面的 <code>a.js</code> ）改变，就将这个依赖所处的 <code>module</code> 的更新，并将新的 <code>module</code> 发送给浏览器重新执行。由于我们只打了一个 <code>bundle.js</code>，所以热更新的话也会重新打这个 <code>bundle.js</code>。试想如果依赖越来越多，就算只修改一个文件，理论上热更新的速度也会越来越慢。</p><p>而如果是像 Vite 这种只编译不打包会是什么情况呢？</p><p>只是编译的话，最终产出的依然是 <code>a.js</code>、<code>b.js</code>、<code>c.js</code> 三个文件，只有编译耗时。由于入口是 <code>c.js</code>，浏览器解析到 <code>import { a } from &#39;./a&#39;</code> 时，会发起 HTTP 请求 <code>a.js</code> （b 同理），就算不用打包，也可以加载到所需要的代码，因此省去了合并代码的时间。</p><p>在热更新的时候，如果 <code>a</code> 发生了改变，只需要更新 <code>a</code> 以及用到 <code>a</code> 的 <code>c</code>。由于 <code>b</code> 没有发生改变，所以 Vite 无需重新编译 <code>b</code>，可以从缓存中直接拿编译的结果。这样一来，修改一个文件 <code>a</code>，只会重新编译这个文件 <code>a</code> 以及浏览器当前用到这个文件 <code>a</code> 的文件，而其余文件都无需重新编译。所以理论上热更新的速度不会随着文件增加而变慢。</p><p>当然这样做有没有不好的地方？有，初始化的时候如果浏览器请求的模块过多，也会带来初始化的性能问题。不过如果你能遇到初始化过慢的这个问题，相信热更新的速度会弥补很多。当然我相信以后尤大也会解决这个问题。</p><h2 id="Vite-运行-Web-应用的实现"><a href="#Vite-运行-Web-应用的实现" class="headerlink" title="Vite 运行 Web 应用的实现"></a>Vite 运行 Web 应用的实现</h2><p>上面说了这么多的铺垫，可能还不够直观，我们可以先跑一个 Vite 项目来实际看看。</p><p>按照官网的说明，可以输入如下命令（<code>&lt;project-name&gt;</code> 为自己想要的目录名即可）</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ npx create-vite-app &lt;project-name&gt;</span><br><span class="line">$ <span class="built_in">cd</span> &lt;project-name&gt;</span><br><span class="line">$ npm install</span><br><span class="line">$ npm run dev</span><br></pre></td></tr></table></figure><p>如果一切都正常你将在 <code>localhost:3000</code>（Vite 的服务器起的端口） 看到这个界面：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/20200503152836.png"></p><p>并得到如下的代码结构：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">.</span><br><span class="line">├── App.vue // 页面的主要逻辑</span><br><span class="line">├── index.html // 默认打开的页面以及 Vue 组件挂载</span><br><span class="line">├── node_modules</span><br><span class="line">└── package.json</span><br></pre></td></tr></table></figure><h3 id="拦截-HTTP-请求"><a href="#拦截-HTTP-请求" class="headerlink" title="拦截 HTTP 请求"></a>拦截 HTTP 请求</h3><p>接下来开始说一下 Vite 实现的核心——拦截浏览器对模块的请求并返回处理后的结果。</p><p>我们知道，由于是在 <code>localhost:3000</code> 打开的网页，所以浏览器发起的第一个请求自然是请求 <code>localhost:3000/</code>，这个请求发送到 Vite 后端之后经过静态资源服务器的处理，会进而请求到 <code>/index.html</code>，此时 Vite 就开始对这个请求做拦截和处理了。</p><p>首先，<code>index.html</code> 里的源码是这样的：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;app&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;module&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> &#123; createApp &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&#x27;./App.vue&#x27;</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="title function_">createApp</span>(<span class="title class_">App</span>).<span class="title function_">mount</span>(<span class="string">&#x27;#app&#x27;</span>)</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>但是在浏览器里它是这样的：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/20200503153404.png"></p><p>注意到什么不同了吗？是的， <code>import { createApp } from &#39;vue&#39;</code> 换成了 <code>import { createApp } from &#39;/@modules/vue</code>。</p><p>这里就不得不说浏览器对 <code>import</code> 的模块发起请求时的一些局限了，平时我们写代码，如果不是引用相对路径的模块，而是引用 <code>node_modules</code> 的模块，都是直接 <code>import xxx from &#39;xxx&#39;</code>，由 Webpack 等工具来帮我们找这个模块的具体路径。但是浏览器不知道你项目里有 <code>node_modules</code>，它只能通过相对路径去寻找模块。</p><p>因此 Vite 在拦截的请求里，对直接引用 <code>node_modules</code> 的模块都做了路径的替换，换成了 <code>/@modules/</code> 并返回回去。而后浏览器收到后，会发起对 <code>/@modules/xxx</code> 的请求，然后被 Vite 再次拦截，并由 Vite 内部去访问真正的模块，并将得到的内容再次做同样的处理后，返回给浏览器。</p><h3 id="imports-替换"><a href="#imports-替换" class="headerlink" title="imports 替换"></a>imports 替换</h3><h4 id="普通-JS-import-替换"><a href="#普通-JS-import-替换" class="headerlink" title="普通 JS import 替换"></a>普通 JS import 替换</h4><p>上面说的这步替换来自 <code>src/node/serverPluginModuleRewrite.ts</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 只取关键代码：</span></span><br><span class="line"><span class="comment">// Vite 使用 Koa 作为内置的服务器</span></span><br><span class="line"><span class="comment">// 如果请求的路径是 /index.html</span></span><br><span class="line"><span class="keyword">if</span> (ctx.<span class="property">path</span> === <span class="string">&#x27;/index.html&#x27;</span>) &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">const</span> html = <span class="keyword">await</span> <span class="title function_">readBody</span>(ctx.<span class="property">body</span>)</span><br><span class="line">  ctx.<span class="property">body</span> = html.<span class="title function_">replace</span>(</span><br><span class="line">    <span class="regexp">/(&lt;script\b[^&gt;]*&gt;)([\s\S]*?)&lt;\/script&gt;/gm</span>, <span class="comment">// 正则匹配</span></span><br><span class="line">    <span class="function">(<span class="params">_, openTag, script</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="comment">// also inject __DEV__ flag</span></span><br><span class="line">      <span class="keyword">const</span> devFlag = hasInjectedDevFlag ? <span class="string">``</span> : devInjectionCode</span><br><span class="line">      hasInjectedDevFlag = <span class="literal">true</span></span><br><span class="line">       <span class="comment">// 替换 html 的 import 路径</span></span><br><span class="line">      <span class="keyword">return</span> <span class="string">`<span class="subst">$&#123;devFlag&#125;</span><span class="subst">$&#123;openTag&#125;</span><span class="subst">$&#123;rewriteImports(</span></span></span><br><span class="line"><span class="subst"><span class="string">        script,</span></span></span><br><span class="line"><span class="subst"><span class="string">        <span class="string">&#x27;/index.html&#x27;</span>,</span></span></span><br><span class="line"><span class="subst"><span class="string">        resolver</span></span></span><br><span class="line"><span class="subst"><span class="string">      )&#125;</span>&lt;/script&gt;`</span></span><br><span class="line">    &#125;</span><br><span class="line">  )</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果并没有在 <code>script</code> 标签内部直接写 <code>import</code>，而是用 <code>src</code> 的形式引用的话如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;script type=<span class="string">&quot;module&quot;</span> src=<span class="string">&quot;/main.js&quot;</span>&gt;&lt;/script&gt;</span><br></pre></td></tr></table></figure><p>那么就会在浏览器发起对 <code>main.js</code> 请求的时候进行处理：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 只取关键代码：</span></span><br><span class="line"><span class="keyword">if</span> (</span><br><span class="line">  ctx.<span class="property">response</span>.<span class="title function_">is</span>(<span class="string">&#x27;js&#x27;</span>) &amp;&amp;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">) &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">const</span> content = <span class="keyword">await</span> <span class="title function_">readBody</span>(ctx.<span class="property">body</span>)</span><br><span class="line">  <span class="keyword">await</span> initLexer</span><br><span class="line">  <span class="comment">// 重写 js 文件里的 import</span></span><br><span class="line">  ctx.<span class="property">body</span> = <span class="title function_">rewriteImports</span>(</span><br><span class="line">    content,</span><br><span class="line">    ctx.<span class="property">url</span>.<span class="title function_">replace</span>(<span class="regexp">/(&amp;|\?)t=\d+/</span>, <span class="string">&#x27;&#x27;</span>),</span><br><span class="line">    resolver,</span><br><span class="line">    ctx.<span class="property">query</span>.<span class="property">t</span></span><br><span class="line">  )</span><br><span class="line">  <span class="comment">// 写入缓存，之后可以从缓存中直接读取</span></span><br><span class="line">  rewriteCache.<span class="title function_">set</span>(content, ctx.<span class="property">body</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>替换逻辑 <code>rewriteImports</code> 就不展开了，用的是 <code>es-module-lexer</code> 来进行的语法分析获取 <code>imports</code> 数组，然后再做的替换。</p><h4 id="vue-文件的替换"><a href="#vue-文件的替换" class="headerlink" title="*.vue 文件的替换"></a>*.vue 文件的替换</h4><p>如果 <code>import</code> 的是 <code>.vue</code> 文件，将会做更进一步的替换：</p><p>原本的 <code>App.vue</code> 文件长这样：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">h1</span>&gt;</span>Hello Vite + Vue 3!<span class="tag">&lt;/<span class="name">h1</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span>Edit ./App.vue to test hot module replacement (HMR).<span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">p</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">span</span>&gt;</span>Count is: &#123;&#123; count &#125;&#125;<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">button</span> @<span class="attr">click</span>=<span class="string">&quot;count++&quot;</span>&gt;</span>increment<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">p</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">data</span>: <span class="function">() =&gt;</span> (&#123; <span class="attr">count</span>: <span class="number">0</span> &#125;),</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">style</span> <span class="attr">scoped</span>&gt;</span><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-tag">h1</span> &#123;</span></span><br><span class="line"><span class="language-css">  <span class="attribute">color</span>: <span class="number">#4fc08d</span>;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"></span></span><br><span class="line"><span class="language-css"><span class="selector-tag">h1</span>, <span class="selector-tag">p</span> &#123;</span></span><br><span class="line"><span class="language-css">  <span class="attribute">font-family</span>: Arial, Helvetica, sans-serif;</span></span><br><span class="line"><span class="language-css">&#125;</span></span><br><span class="line"><span class="language-css"></span><span class="tag">&lt;/<span class="name">style</span>&gt;</span></span><br></pre></td></tr></table></figure><p>替换后长这样：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// localhost:3000/App.vue</span></span><br><span class="line"><span class="keyword">import</span> &#123; updateStyle &#125; <span class="keyword">from</span> <span class="string">&quot;/@hmr&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 抽出 script 逻辑</span></span><br><span class="line"><span class="keyword">const</span> __script = &#123;</span><br><span class="line">  <span class="attr">data</span>: <span class="function">() =&gt;</span> (&#123; <span class="attr">count</span>: <span class="number">0</span> &#125;),</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 将 style 拆分成 /App.vue?type=style 请求，由浏览器继续发起请求获取样式</span></span><br><span class="line"><span class="title function_">updateStyle</span>(<span class="string">&quot;c44b8200-0&quot;</span>, <span class="string">&quot;/App.vue?type=style&amp;index=0&amp;t=1588490870523&quot;</span>)</span><br><span class="line">__script.<span class="property">__scopeId</span> = <span class="string">&quot;data-v-c44b8200&quot;</span> <span class="comment">// 样式的 scopeId</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 将 template 拆分成 /App.vue?type=template 请求，由浏览器继续发起请求获取 render function</span></span><br><span class="line"><span class="keyword">import</span> &#123; render <span class="keyword">as</span> __render &#125; <span class="keyword">from</span> <span class="string">&quot;/App.vue?type=template&amp;t=1588490870523&amp;t=1588490870523&quot;</span></span><br><span class="line">__script.<span class="property">render</span> = __render <span class="comment">// render 方法挂载，用于 createApp 时渲染</span></span><br><span class="line">__script.<span class="property">__hmrId</span> = <span class="string">&quot;/App.vue&quot;</span> <span class="comment">// 记录 HMR 的 id，用于热更新</span></span><br><span class="line">__script.<span class="property">__file</span> = <span class="string">&quot;/XXX/web/vite-test/App.vue&quot;</span> <span class="comment">// 记录文件的原始的路径，后续热更新能用到</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> __script</span><br></pre></td></tr></table></figure><p>这样就把原本一个 <code>.vue</code> 的文件拆成了三个请求（分别对应 <code>script</code>、<code>style</code> 和<code>template</code>） ，浏览器会先收到包含 <code>script</code> 逻辑的 <code>App.vue</code> 的响应，然后解析到 <code>template</code> 和 <code>style</code> 的路径后，会再次发起 HTTP 请求来请求对应的资源，此时 Vite 对其拦截并再次处理后返回相应的内容。</p><p>如下：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/20200503171228.png"></p><p>不得不说这个思路是非常巧妙的。</p><p>这一步的拆分来自 <code>src/node/serverPluginVue.ts</code>，核心逻辑是根据 URL 的 query 参数来做不同的处理（简化分析如下）：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 如果没有 query 的 type，比如直接请求的 /App.vue</span></span><br><span class="line"><span class="keyword">if</span> (!query.<span class="property">type</span>) &#123;</span><br><span class="line">  ctx.<span class="property">type</span> = <span class="string">&#x27;js&#x27;</span></span><br><span class="line">  ctx.<span class="property">body</span> = <span class="title function_">compileSFCMain</span>(descriptor, filePath, publicPath) <span class="comment">// 编译 App.vue，编译成上面说的带有 script 内容，以及 template 和 style 链接的形式。</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">etagCacheCheck</span>(ctx) <span class="comment">// ETAG 缓存检测相关逻辑</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果 query 的 type 是 template，比如 /App.vue?type=template&amp;xxx</span></span><br><span class="line"><span class="keyword">if</span> (query.<span class="property">type</span> === <span class="string">&#x27;template&#x27;</span>) &#123;</span><br><span class="line">  ctx.<span class="property">type</span> = <span class="string">&#x27;js&#x27;</span></span><br><span class="line">  ctx.<span class="property">body</span> = <span class="title function_">compileSFCTemplate</span>( <span class="comment">// 编译 template 生成 render function</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  )</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">etagCacheCheck</span>(ctx)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果 query 的 type 是 style，比如 /App.vue?type=style&amp;xxx</span></span><br><span class="line"><span class="keyword">if</span> (query.<span class="property">type</span> === <span class="string">&#x27;style&#x27;</span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> index = <span class="title class_">Number</span>(query.<span class="property">index</span>)</span><br><span class="line">  <span class="keyword">const</span> styleBlock = descriptor.<span class="property">styles</span>[index]</span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> <span class="title function_">compileSFCStyle</span>( <span class="comment">// 编译 style</span></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  )</span><br><span class="line">  <span class="keyword">if</span> (query.<span class="property">module</span> != <span class="literal">null</span>) &#123; <span class="comment">// 如果是 css module</span></span><br><span class="line">    ctx.<span class="property">type</span> = <span class="string">&#x27;js&#x27;</span></span><br><span class="line">    ctx.<span class="property">body</span> = <span class="string">`export default <span class="subst">$&#123;<span class="built_in">JSON</span>.stringify(result.modules)&#125;</span>`</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123; <span class="comment">// 正常 css</span></span><br><span class="line">    ctx.<span class="property">type</span> = <span class="string">&#x27;css&#x27;</span></span><br><span class="line">    ctx.<span class="property">body</span> = result.<span class="property">code</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="modules-路径解析"><a href="#modules-路径解析" class="headerlink" title="@modules&#x2F;* 路径解析"></a>@modules&#x2F;* 路径解析</h2><p>上面只涉及到了替换的逻辑，解析的逻辑来自 <code>src/node/serverPluginModuleResolve.ts</code>。这一步就相对简单了，核心逻辑就是去 <code>node_modules</code> 里找有没有对应的模块，有的话就返回，没有的话就报 404：（省略了很多逻辑，比如对 <code>web_modules</code> 的处理、缓存的处理等）</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> file = <span class="title function_">resolve</span>(root, id) <span class="comment">// id 是模块的名字，比如 axios</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">serve</span>(id, file, <span class="string">&#x27;node_modules&#x27;</span>) <span class="comment">// 从 node_modules 中找到真正的模块内容并返回</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">error</span>(</span><br><span class="line">    chalk.<span class="title function_">red</span>(<span class="string">`[vite] Error while resolving node_modules with id &quot;<span class="subst">$&#123;id&#125;</span>&quot;:`</span>)</span><br><span class="line">  )</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">error</span>(e)</span><br><span class="line">  ctx.<span class="property">status</span> = <span class="number">404</span> <span class="comment">// 如果没找到就 404</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Vite-热更新的实现"><a href="#Vite-热更新的实现" class="headerlink" title="Vite 热更新的实现"></a>Vite 热更新的实现</h2><p>上面已经说完了 Vite 是如何运行一个 Web 应用的，包括如何拦截请求、替换内容、返回处理后的结果。接下来说一下 Vite 热更新的实现，同样实现的非常巧妙。</p><p>我们知道，如果要实现热更新，那么就需要浏览器和服务器建立某种通信机制，这样浏览器才能收到通知进行热更新。Vite 的是通过 <code>WebSocket</code> 来实现的热更新通信。</p><h3 id="客户端"><a href="#客户端" class="headerlink" title="客户端"></a>客户端</h3><p>客户端的代码在 <code>src/client/client.ts</code>，主要是创建 <code>WebSocket</code> 客户端，监听来自服务端的 HMR 消息推送。</p><p>Vite 的 WS 客户端目前监听这几种消息：</p><ul><li><code>connected</code>: WebSocket 连接成功</li><li><code>vue-reload</code>: Vue 组件重新加载（当你修改了 script 里的内容时）</li><li><code>vue-rerender</code>: Vue 组件重新渲染（当你修改了 template 里的内容时）</li><li><code>style-update</code>: 样式更新</li><li><code>style-remove</code>: 样式移除</li><li><code>js-update</code>: js 文件更新</li><li><code>full-reload</code>: fallback 机制，网页重刷新</li></ul><p>其中针对 Vue 组件本身的一些更新，都可以直接调用 <code>HMRRuntime</code> 提供的方法，非常方便。其余的更新逻辑，基本上都是利用了 <code>timestamp</code> 刷新缓存重新执行的方法来达到更新的目的。</p><p>核心逻辑如下，我感觉非常清晰明了：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">HMRRuntime</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span> <span class="comment">// 来自 Vue3.0 的 HMRRuntime</span></span><br><span class="line"></span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;[vite] connecting...&#x27;</span>)</span><br><span class="line"></span><br><span class="line">declare <span class="keyword">var</span> <span class="attr">__VUE_HMR_RUNTIME__</span>: <span class="title class_">HMRRuntime</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> socket = <span class="keyword">new</span> <span class="title class_">WebSocket</span>(<span class="string">`ws://<span class="subst">$&#123;location.host&#125;</span>`</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Listen for messages</span></span><br><span class="line">socket.<span class="title function_">addEventListener</span>(<span class="string">&#x27;message&#x27;</span>, <span class="function">(<span class="params">&#123; data &#125;</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; type, path, id, index, timestamp, customData &#125; = <span class="title class_">JSON</span>.<span class="title function_">parse</span>(data)</span><br><span class="line">  <span class="keyword">switch</span> (type) &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;connected&#x27;</span>:</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[vite] connected.`</span>)</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;vue-reload&#x27;</span>:</span><br><span class="line">      <span class="keyword">import</span>(<span class="string">`<span class="subst">$&#123;path&#125;</span>?t=<span class="subst">$&#123;timestamp&#125;</span>`</span>).<span class="title function_">then</span>(<span class="function">(<span class="params">m</span>) =&gt;</span> &#123;</span><br><span class="line">        __VUE_HMR_RUNTIME__.<span class="title function_">reload</span>(path, m.<span class="property">default</span>)</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[vite] <span class="subst">$&#123;path&#125;</span> reloaded.`</span>) <span class="comment">// 调用 HMRRUNTIME 的方法更新</span></span><br><span class="line">      &#125;)</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;vue-rerender&#x27;</span>:</span><br><span class="line">      <span class="keyword">import</span>(<span class="string">`<span class="subst">$&#123;path&#125;</span>?type=template&amp;t=<span class="subst">$&#123;timestamp&#125;</span>`</span>).<span class="title function_">then</span>(<span class="function">(<span class="params">m</span>) =&gt;</span> &#123;</span><br><span class="line">        __VUE_HMR_RUNTIME__.<span class="title function_">rerender</span>(path, m.<span class="property">render</span>)</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[vite] <span class="subst">$&#123;path&#125;</span> template updated.`</span>) <span class="comment">// 调用 HMRRUNTIME 的方法更新</span></span><br><span class="line">      &#125;)</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;style-update&#x27;</span>:</span><br><span class="line">      <span class="title function_">updateStyle</span>(id, <span class="string">`<span class="subst">$&#123;path&#125;</span>?type=style&amp;index=<span class="subst">$&#123;index&#125;</span>&amp;t=<span class="subst">$&#123;timestamp&#125;</span>`</span>) <span class="comment">// 重新加载 style 的 URL</span></span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(</span><br><span class="line">        <span class="string">`[vite] <span class="subst">$&#123;path&#125;</span> style<span class="subst">$&#123;index &gt; <span class="number">0</span> ? <span class="string">`#<span class="subst">$&#123;index&#125;</span>`</span> : <span class="string">``</span>&#125;</span> updated.`</span></span><br><span class="line">      )</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;style-remove&#x27;</span>:</span><br><span class="line">      <span class="keyword">const</span> link = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">`vite-css-<span class="subst">$&#123;id&#125;</span>`</span>)</span><br><span class="line">      <span class="keyword">if</span> (link) &#123;</span><br><span class="line">        <span class="variable language_">document</span>.<span class="property">head</span>.<span class="title function_">removeChild</span>(link) <span class="comment">// 删除 style</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;js-update&#x27;</span>:</span><br><span class="line">      <span class="keyword">const</span> update = jsUpdateMap.<span class="title function_">get</span>(path)</span><br><span class="line">      <span class="keyword">if</span> (update) &#123;</span><br><span class="line">        <span class="title function_">update</span>(timestamp) <span class="comment">// 用新的时间戳加载并执行 js，达到更新的目的</span></span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`[vite]: js module reloaded: `</span>, path)</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">error</span>(</span><br><span class="line">          <span class="string">`[vite] got js update notification but no client callback was registered. Something is wrong.`</span></span><br><span class="line">        )</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;custom&#x27;</span>:</span><br><span class="line">      <span class="keyword">const</span> cbs = customUpdateMap.<span class="title function_">get</span>(id)</span><br><span class="line">      <span class="keyword">if</span> (cbs) &#123;</span><br><span class="line">        cbs.<span class="title function_">forEach</span>(<span class="function">(<span class="params">cb</span>) =&gt;</span> <span class="title function_">cb</span>(customData))</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">&#x27;full-reload&#x27;</span>:</span><br><span class="line">      location.<span class="title function_">reload</span>()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="服务端"><a href="#服务端" class="headerlink" title="服务端"></a>服务端</h3><p>服务端的实现位于 <code>src/node/serverPluginHmr.ts</code>。核心是监听项目文件的变更，然后根据不同文件类型（目前只有 <code>vue</code> 和 <code>js</code>）来做不同的处理：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">watcher.<span class="title function_">on</span>(<span class="string">&#x27;change&#x27;</span>, <span class="title function_">async</span> (file) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> timestamp = <span class="title class_">Date</span>.<span class="title function_">now</span>() <span class="comment">// 更新时间戳</span></span><br><span class="line">  <span class="keyword">if</span> (file.<span class="title function_">endsWith</span>(<span class="string">&#x27;.vue&#x27;</span>)) &#123;</span><br><span class="line">    <span class="title function_">handleVueReload</span>(file, timestamp)</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (file.<span class="title function_">endsWith</span>(<span class="string">&#x27;.js&#x27;</span>)) &#123;</span><br><span class="line">    <span class="title function_">handleJSReload</span>(file, timestamp)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>对于 <code>Vue</code> 文件的热更新而言，主要是重新编译 <code>Vue</code> 文件，检测 <code>template</code> 、<code>script</code> 、<code>style</code> 的改动，如果有改动就通过 WS 服务端发起对应的热更新请求。</p><p>简单的源码分析如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">handleVueReload</span>(<span class="params"></span></span><br><span class="line"><span class="params">    file: string,</span></span><br><span class="line"><span class="params">    timestamp: number = <span class="built_in">Date</span>.now(),</span></span><br><span class="line"><span class="params">    content?: string</span></span><br><span class="line"><span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> publicPath = resolver.<span class="title function_">fileToRequest</span>(file) <span class="comment">// 获取文件的路径</span></span><br><span class="line">  <span class="keyword">const</span> cacheEntry = vueCache.<span class="title function_">get</span>(file) <span class="comment">// 获取缓存里的内容</span></span><br><span class="line"></span><br><span class="line">  <span class="title function_">debugHmr</span>(<span class="string">`busting Vue cache for <span class="subst">$&#123;file&#125;</span>`</span>)</span><br><span class="line">  vueCache.<span class="title function_">del</span>(file) <span class="comment">// 发生变动了因此之前的缓存可以删除</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> descriptor = <span class="keyword">await</span> <span class="title function_">parseSFC</span>(root, file, content) <span class="comment">// 编译 Vue 文件</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> prevDescriptor = cacheEntry &amp;&amp; cacheEntry.<span class="property">descriptor</span> <span class="comment">// 获取前一次的缓存</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!prevDescriptor) &#123;</span><br><span class="line">    <span class="comment">// 这个文件之前从未被访问过（本次是第一次访问），也就没必要热更新</span></span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 设置两个标志位，用于判断是需要 reload 还是 rerender</span></span><br><span class="line">  <span class="keyword">let</span> needReload = <span class="literal">false</span></span><br><span class="line">  <span class="keyword">let</span> needRerender = <span class="literal">false</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果 script 部分不同则需要 reload</span></span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">isEqual</span>(descriptor.<span class="property">script</span>, prevDescriptor.<span class="property">script</span>)) &#123;</span><br><span class="line">    needReload = <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果 template 部分不同则需要 rerender</span></span><br><span class="line">  <span class="keyword">if</span> (!<span class="title function_">isEqual</span>(descriptor.<span class="property">template</span>, prevDescriptor.<span class="property">template</span>)) &#123;</span><br><span class="line">    needRerender = <span class="literal">true</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> styleId = <span class="title function_">hash_sum</span>(publicPath)</span><br><span class="line">  <span class="comment">// 获取之前的 style 以及下一次（或者说热更新）的 style</span></span><br><span class="line">  <span class="keyword">const</span> prevStyles = prevDescriptor.<span class="property">styles</span> || []</span><br><span class="line">  <span class="keyword">const</span> nextStyles = descriptor.<span class="property">styles</span> || []</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果不需要 reload，则查看是否需要更新 style</span></span><br><span class="line">  <span class="keyword">if</span> (!needReload) &#123;</span><br><span class="line">    nextStyles.<span class="title function_">forEach</span>(<span class="function">(<span class="params">_, i</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (!prevStyles[i] || !<span class="title function_">isEqual</span>(prevStyles[i], nextStyles[i])) &#123;</span><br><span class="line">        <span class="title function_">send</span>(&#123;</span><br><span class="line">          <span class="attr">type</span>: <span class="string">&#x27;style-update&#x27;</span>,</span><br><span class="line">          <span class="attr">path</span>: publicPath,</span><br><span class="line">          <span class="attr">index</span>: i,</span><br><span class="line">          <span class="attr">id</span>: <span class="string">`<span class="subst">$&#123;styleId&#125;</span>-<span class="subst">$&#123;i&#125;</span>`</span>,</span><br><span class="line">          timestamp</span><br><span class="line">        &#125;)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果 style 标签及内容删掉了，则需要发送 `style-remove` 的通知</span></span><br><span class="line">  prevStyles.<span class="title function_">slice</span>(nextStyles.<span class="property">length</span>).<span class="title function_">forEach</span>(<span class="function">(<span class="params">_, i</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">send</span>(&#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;style-remove&#x27;</span>,</span><br><span class="line">      <span class="attr">path</span>: publicPath,</span><br><span class="line">      <span class="attr">id</span>: <span class="string">`<span class="subst">$&#123;styleId&#125;</span>-<span class="subst">$&#123;i + nextStyles.length&#125;</span>`</span>,</span><br><span class="line">      timestamp</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果需要 reload 发送 `vue-reload` 通知</span></span><br><span class="line">  <span class="keyword">if</span> (needReload) &#123;</span><br><span class="line">    <span class="title function_">send</span>(&#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;vue-reload&#x27;</span>,</span><br><span class="line">      <span class="attr">path</span>: publicPath,</span><br><span class="line">      timestamp</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125; <span class="keyword">else</span> <span class="keyword">if</span> (needRerender) &#123;</span><br><span class="line">    <span class="comment">// 否则发送 `vue-rerender` 通知</span></span><br><span class="line">    <span class="title function_">send</span>(&#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;vue-rerender&#x27;</span>,</span><br><span class="line">      <span class="attr">path</span>: publicPath,</span><br><span class="line">      timestamp</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于热更新 <code>js</code> 文件而言，会递归地查找引用这个文件的 <code>importer</code>。比如是某个 <code>Vue</code> 文件所引用了这个 <code>js</code>，就会被查找出来。假如最终发现找不到引用者，则会返回 <code>hasDeadEnd: true</code>。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> vueImporters = <span class="keyword">new</span> <span class="title class_">Set</span>&lt;string&gt;() <span class="comment">// 查找并存放需要热更新的 Vue 文件</span></span><br><span class="line"><span class="keyword">const</span> jsHotImporters = <span class="keyword">new</span> <span class="title class_">Set</span>&lt;string&gt;() <span class="comment">// 查找并存放需要热更新的 js 文件</span></span><br><span class="line"><span class="keyword">const</span> hasDeadEnd = <span class="title function_">walkImportChain</span>(</span><br><span class="line">  publicPath,</span><br><span class="line">  importers,</span><br><span class="line">  vueImporters,</span><br><span class="line">  jsHotImporters</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>如果 <code>hasDeadEnd</code> 为 <code>true</code>，则直接发送 <code>full-reload</code>。如果 <code>vueImporters</code> 或 <code>jsHotImporters</code> 里查找到需要热更新的文件，则发起热更新通知：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (hasDeadEnd) &#123;</span><br><span class="line">  <span class="title function_">send</span>(&#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&#x27;full-reload&#x27;</span>,</span><br><span class="line">    timestamp</span><br><span class="line">  &#125;)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  vueImporters.<span class="title function_">forEach</span>(<span class="function">(<span class="params">vueImporter</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">send</span>(&#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;vue-reload&#x27;</span>,</span><br><span class="line">      <span class="attr">path</span>: vueImporter,</span><br><span class="line">      timestamp</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line">  jsHotImporters.<span class="title function_">forEach</span>(<span class="function">(<span class="params">jsImporter</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="title function_">send</span>(&#123;</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;js-update&#x27;</span>,</span><br><span class="line">      <span class="attr">path</span>: jsImporter,</span><br><span class="line">      timestamp</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="客户端逻辑的注入"><a href="#客户端逻辑的注入" class="headerlink" title="客户端逻辑的注入"></a>客户端逻辑的注入</h3><p>写到这里，还有一个问题是，我们在自己的代码里并没有引入 <code>HRM</code> 的 <code>client</code> 代码，Vite 是如何把 <code>client</code> 代码注入的呢？</p><p>回到上面的一张图，Vite 重写 <code>App.vue</code> 文件的内容并返回时：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/20200503171228.png"></p><p>注意这张图里的代码区第一句话 <code>import { updateStyle } from &#39;/@hmr&#39;</code>，并且在左侧请求列表中也有一个对 <code>@hmr</code> 文件的请求。这个请求是啥呢？</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/20200503201312.png"></p><p>可以发现，这个请求就是上面说的客户端逻辑的 <code>client.ts</code> 的内容。</p><p>在 <code>src/node/serverPluginHmr.ts</code> 里，有针对 <code>@hmr</code> 文件的解析处理：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> hmrClientFilePath = path.<span class="title function_">resolve</span>(__dirname, <span class="string">&#x27;./client.js&#x27;</span>)</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> hmrClientId = <span class="string">&#x27;@hmr&#x27;</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> hmrClientPublicPath = <span class="string">`/<span class="subst">$&#123;hmrClientId&#125;</span>`</span></span><br><span class="line"></span><br><span class="line">app.<span class="title function_">use</span>(<span class="title function_">async</span> (ctx, next) =&gt; &#123;</span><br><span class="line">  <span class="keyword">if</span> (ctx.<span class="property">path</span> !== hmrClientPublicPath) &#123; <span class="comment">// 请求路径如果不是 @hmr 就跳过</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">next</span>()</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">debugHmr</span>(<span class="string">&#x27;serving hmr client&#x27;</span>)</span><br><span class="line">  ctx.<span class="property">type</span> = <span class="string">&#x27;js&#x27;</span></span><br><span class="line">  <span class="keyword">await</span> <span class="title function_">cachedRead</span>(ctx, hmrClientFilePath) <span class="comment">// 返回 client.js 的内容</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>至此，热更新的整体流程已经解析完毕。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>这个项目最近在以惊人的速度迭代着，因此没过多久以后再回头看这篇文章，可能代码、实现已经过时。不过 Vite 的整体思路是非常棒的，在早期源码不多的情况下，能学到更贴近作者原始想法的东西，也算是很不错的收获。希望本文能给你学习 Vite 一些参考，有错误也欢迎大家指出。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;已经好久没有写博客了。本文不说 Vue3.0 了，相信已经有很多文章在说它了。而前一段时间尤大开源的 &lt;a href=&quot;https://github.com/vuejs/vite&quot;&gt;Vite&lt;/a&gt; 则是一个更加吸引我的东西，它的总体思路是很不错的，早期源码的学习成本也比较低，于是就趁着假期学习一番。&lt;/p&gt;
&lt;p&gt;本文撰写于 Vite-0.9.1 版本。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Vue" scheme="https://molunerfinn.com/tags/Vue/"/>
    
    <category term="Vite" scheme="https://molunerfinn.com/tags/Vite/"/>
    
  </entry>
  
  <entry>
    <title>Typora 支持 PicGo 来上传图片了</title>
    <link href="https://molunerfinn.com/typora-supports-picgo/"/>
    <id>https://molunerfinn.com/typora-supports-picgo/</id>
    <published>2020-03-01T21:25:00.000Z</published>
    <updated>2026-03-08T01:14:37.987Z</updated>
    
    <content type="html"><![CDATA[<p>Typora 最近的一次更新支持图片自定义图片上传服务了，增加了对 <a href="https://github.com/gee1k/uPic">uPic</a>，<a href="https://github.com/Molunerfinn/PicGo">PicGo</a> 以及自定义上传命令的支持。其中针对 PicGo 和 PicGo-Core 都做了兼容，可以说非常有诚意了。本文会简单介绍一下如何配置并使用。</p><span id="more"></span><h2 id="自定义图片上传服务的设置"><a href="#自定义图片上传服务的设置" class="headerlink" title="自定义图片上传服务的设置"></a>自定义图片上传服务的设置</h2><p>更新 Typora 的最新版，可以在设置-图像处找到自定义图片上传服务的设置区域：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/typora-image-setting.png" alt="image-20200226105152275"></p><p>Typora 官方关于图像自定义上传相关的配置、介绍的页面 <a href="https://support.typora.io/Upload-Image/">在这里</a>。</p><p>如上图，你可以选择自己喜欢用的图片上传工具，可选的工具如下图：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/upload.png" alt="image-20200226114210451"></p><p>同时 Typora 提供了上传测试功能，如下图你可以找到 <code>Test Uploader</code> 按钮来测试你的上传功能是否正常：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/typora-test-upload.png" alt="image-20200226114658030"></p><p>Typora 会上传的图片就是它家的 Logo 了，如下：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/image-20200226192722744.png" alt="image-20200226192722744"></p><p>当测试成功之后，还别忘了开启图片自动上传功能：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/typora-image-settings.png" alt="image-20200226120345146"></p><p>注意选则第三项，即允许通过读取 YAML 配置来决定是否自动上传图片。经过测试，在 macOS 上必须开启这个选项，同时在文章的顶部写下如下的 YAML 配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">typora-copy-images-to:</span> <span class="string">upload</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><p>这样才可以开启自动上传图片的功能。应该是 Typora 的一个 bug，后续版本不知道会不会修复。</p><h2 id="自动上传图片的效果"><a href="#自动上传图片的效果" class="headerlink" title="自动上传图片的效果"></a>自动上传图片的效果</h2><p>说了这么多，Typora 里引入图片即上传的效果是怎么样的呢？我录制了一个 gif：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/typora-upload-image-gif-v2.gif" alt="typora-upload-image-gif-v2"></p><p>可以说整体效果还是比较流畅的。</p><p>同时如果你未开启自动上传图片的功能，把图片拖入 Typora 或者粘贴到 Typora，右键图片看到一个上传图片的选项：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/upload-image-with-context-menu.png" alt="upload-image-with-context-menu.png"></p><p>这样也能根据你配置的上传服务来上传图片。</p><h2 id="使用-PicGo-上传的相关说明"><a href="#使用-PicGo-上传的相关说明" class="headerlink" title="使用 PicGo 上传的相关说明"></a>使用 PicGo 上传的相关说明</h2><p>Typora 支持了两种 PicGo 的上传模式，作为 PicGo 的开发者，我觉得有有必要跟朋友们说说区别。Typora 支持的两种 PicGo 上传模式分别是：PicGo-Core（命令行）以及 PicGo.app（图形界面）</p><h3 id="1-PicGo-app"><a href="#1-PicGo-app" class="headerlink" title="1. PicGo.app"></a>1. PicGo.app</h3><p><a href="https://github.com/Molunerfinn/PicGo">PicGo.app</a> 就是用户平时经常使用的图形化界面的 PicGo。而 Typora 对接的上传服务来自于 PicGo v2.2.0+提供的 <a href="https://picgo.github.io/PicGo-Doc/zh/guide/advance.html#picgo-server">PicGo-Server</a> 的功能，它是一个小型的 HTTP 服务器，会默认开启 36677 端口来监听上传的请求。而 Typora 则会往 36677 端口发送请求来上传图片。所以如果你的 PicGo 版本过低或者 PicGo-Server 功能没有开启，或者端口不是 36677，都无法通过 Typora 的这个功能上传图片。</p><h3 id="2-PicGo-Core"><a href="#2-PicGo-Core" class="headerlink" title="2. PicGo-Core"></a>2. PicGo-Core</h3><p>这个是 PicGo 底层依赖的 <a href="https://github.com/PicGo/PicGo-Core">核心库</a>，是 PicGo 上传图片、插件机制的核心。它是一个 npm 包，意味着你可以通过 npm 全局安装来实现上传。同时 Typora 也提供了预编译的二进制文件，它是把 PicGo-Core 所有依赖都打包成了一个可执行的文件。</p><p>Typora 对这两种 PicGo-Core 的用法都支持，官方的文档对此有详细的 <a href="https://support.typora.io/Upload-Image/#config-picgo-core">配置说明</a>。不过需要注意的是 macOS 由于系统的原因，不支持预编译的二进制文件那个使用方法，而只能使用 npm 全局安装的方式，再通过 <code>custom command</code> 自定义命令的方式来使用 PicGo-Core：</p><p><img src="https://cdn.jsdelivr.net/gh/Molunerfinn/test/blog/custom.png" alt="custom"></p><h3 id="3-二者的区别"><a href="#3-二者的区别" class="headerlink" title="3. 二者的区别"></a>3. 二者的区别</h3><p>官方 <a href="https://support.typora.io/Upload-Image/#difference-between-picgoapp-and-picgo-core-command-line">文档</a> 里对二者的区别有做出描述，我觉得写得挺到位的。不过还是跟大家聊聊这二者的区别：</p><ol><li>使用 PicGo.app 模式上传意味着 PicGo 需要开启常驻后台。如果对性能要求比较高的用户可能不太能接受。</li><li>用 PicGo-Core 来上传只有运行时的消耗，上传结束后会自动销毁进程，性能方面会更好。</li><li>PicGo-Core 上传的配置跟 PicGo 用的不是同一个文件，因此如果需要用 PicGo-Core 来上传需要重新配置一遍。</li><li>PicGo 提供了更多的功能，比如上传前重命名、上传的历史记录等</li><li>PicGo 的一些插件只有 GUI 版本支持，而不支持 PicGo-Core，所以如果需要使用插件功能，更推荐使用 PicGo。不过 PicGo 只在语言设定为中文版的 Typora 里才能使用，因为目前 PicGo 没有英文文档、英文界面。</li></ol><p><strong>跪求 T T 有兴趣的小伙伴一起来翻译，如果对 PicGo 的国际化有意向的小伙伴，可以加入官方 <a href="https://gitter.im/picgo-all/PicGo?utm_source=share-link&utm_medium=link&utm_campaign=share-link">gitter</a> 频道一起来聊。</strong></p><p>就我自己的使用来说，我是更喜欢直接用 PicGo 来上传的，因为配置什么的不用再调了，可视化界面也更容易操作~</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>前不久 PicGo2.0 发布的时候，PicGo-Core 还收到了来自 Typora 官方的 PR。我以为需要好几个月的时间才能支持自定义图床，没想到支持来得这么快。我觉得对于一个 Markdown 编辑器而言，图片的管理、上传一定是一种刚需。而此次开放了自定义上传的功能，想必也是戳中了很多 Typora 用户的痛点。另外这次 PicGo 能够作为官方指定的上传工具，我觉得非常开心，同时它也是 Typora 三个平台都支持的上传工具（uPic 和 iPic 都很棒，不过只支持 macOS），希望有了这个功能以后能够给你们带来更好码字体验~</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Typora 最近的一次更新支持图片自定义图片上传服务了，增加了对 &lt;a href=&quot;https://github.com/gee1k/uPic&quot;&gt;uPic&lt;/a&gt;，&lt;a href=&quot;https://github.com/Molunerfinn/PicGo&quot;&gt;PicGo&lt;/a&gt; 以及自定义上传命令的支持。其中针对 PicGo 和 PicGo-Core 都做了兼容，可以说非常有诚意了。本文会简单介绍一下如何配置并使用。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    <category term="随笔" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="Vue" scheme="https://molunerfinn.com/tags/Vue/"/>
    
  </entry>
  
  <entry>
    <title>用setTimeout和clearTimeout简单实现setInterval与clearInterval</title>
    <link href="https://molunerfinn.com/setTimeout-hack-setInterval/"/>
    <id>https://molunerfinn.com/setTimeout-hack-setInterval/</id>
    <published>2019-05-08T10:21:00.000Z</published>
    <updated>2026-03-08T01:14:37.986Z</updated>
    
    <content type="html"><![CDATA[<p>这个问题其实是前一段时间舍友的一道面试题。我觉得类似用<code>reduce实现map</code>、用<code>xxx实现yyy</code>的题目其实都挺有意思，考察融会贯通的本领。不过相比之下这道题可能更有实际意义。比如我们经常会用 <code>setTimeout</code> 来实现倒计时。下面来说说我对这个问题的思考。</p><span id="more"></span><h2 id="简单版本"><a href="#简单版本" class="headerlink" title="简单版本"></a>简单版本</h2><p>首先我们先用 <code>setTimeout</code> 实现一个简单版本的 <code>setInterval</code>。</p><p><code>setInterval</code> 需要不停循环调用，这让我们想到了递归调用自身：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">mySetInterval</span> = (<span class="params">cb, time</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">fn</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">cb</span>() <span class="comment">// 执行传入的回调函数</span></span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">fn</span>() <span class="comment">// 递归调用自己</span></span><br><span class="line">    &#125;, time)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="built_in">setTimeout</span>(fn, time)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>让我们来写段代码测试一下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">mySetInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="keyword">new</span> <span class="title class_">Date</span>())</span><br><span class="line">&#125;, <span class="number">1000</span>)</span><br></pre></td></tr></table></figure><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/setTimeout-1.gif" alt="setTimeout-1"></p><p>嗯，没啥问题，实现了我们想要的功能。。。等一下，怎么停下来？总不能执行了就不管了吧。。。</p><h2 id="clearInterval的实现"><a href="#clearInterval的实现" class="headerlink" title="clearInterval的实现"></a>clearInterval的实现</h2><p>平时如果用到了 <code>setInterval</code> 的同学应该都知道 <code>clearInterval</code> 的存在（不然你怎么停下 <code>interval</code> 呢）。</p><p><code>clearInterval</code> 的用法是 <code>clearInterval(id)</code>。而这个 <code>id</code> 是 <code>setInterval</code>的返回值，通过这个 <code>id</code> 值就能够清除指定的定时器。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> id = <span class="built_in">setInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;, <span class="number">1000</span>)</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="built_in">clearInterval</span>(id)</span><br></pre></td></tr></table></figure><p>不过你有没有想到 <code>clearInterval</code> 是如何实现的？回答这个问题之前，我们需要先实现 <code>mySetInterval</code> 的返回值。</p><h3 id="mySetInterval的返回值"><a href="#mySetInterval的返回值" class="headerlink" title="mySetInterval的返回值"></a>mySetInterval的返回值</h3><p>回到我们简单版本的 <code>mySetInterval</code>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">mySetInterval</span> = (<span class="params">cb, time</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">fn</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">cb</span>() <span class="comment">// 执行传入的回调函数</span></span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">fn</span>() <span class="comment">// 递归调用自己</span></span><br><span class="line">    &#125;, time)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="built_in">setTimeout</span>(fn, time)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在它的返回值因为没有显示指定，所以是 <code>undefined</code>。因此第一步，我们先要返回一个 <code>id</code> 出去。</p><p>那么直接 <code>return setTimeout(fn, time)</code> 可以吗？因为我们知道 <code>setTimeout</code> 也会返回一个id，那么初步构想就是通过 <code>setTimeout</code> 返回的 <code>id</code>，然后调用 <code>clearTimeout(id)</code> 来实现我们的 <code>myClearInterval</code>。</p><p>如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">mySetInterval</span> = (<span class="params">cb, time</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">fn</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">cb</span>() <span class="comment">// 执行传入的回调函数</span></span><br><span class="line">    <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">// 第二个、第三个...</span></span><br><span class="line">      <span class="title function_">fn</span>() <span class="comment">// 递归调用自己</span></span><br><span class="line">    &#125;, time)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">setTimeout</span>(fn, time) <span class="comment">// 第一个setTimeout</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> id = <span class="title function_">mySetInterval</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="keyword">new</span> <span class="title class_">Date</span>())</span><br><span class="line">&#125;, <span class="number">1000</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">// 2秒后清除定时器</span></span><br><span class="line">  <span class="built_in">clearTimeout</span>(id)</span><br><span class="line">&#125;, <span class="number">2000</span>)</span><br></pre></td></tr></table></figure><p>这显然是不行的。因为 <code>mySetInterval</code> 返回的 <code>id</code> 是第一个 <code>setTimeout</code> 的 <code>id</code>，然而2秒后，要 <code>clearTimeout</code> 时，递归执行的第二个、第三个 <code>setTimeout</code>  等等的 <code>id</code> 已经不再是第一个 <code>id</code> 了。因此此时无法清除。</p><p>所以我们需要每次执行 <code>setTimeout</code>的时候把新的 <code>id</code> 存下来。怎么存？我们应该会想到用闭包：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">mySetInterval</span> = (<span class="params">cb, time</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> timeId</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">fn</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">cb</span>() <span class="comment">// 执行传入的回调函数</span></span><br><span class="line">    timeId = <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">// 闭包更新timeId</span></span><br><span class="line">      <span class="title function_">fn</span>() <span class="comment">// 递归调用自己</span></span><br><span class="line">    &#125;, time)</span><br><span class="line">  &#125;</span><br><span class="line">  timeId = <span class="built_in">setTimeout</span>(fn, time) <span class="comment">// 第一个setTimeout</span></span><br><span class="line">  <span class="keyword">return</span> timeId</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很不错，到这步我们已经能够将 <code>timeId</code> 进行更新了。不过还有问题，那就是执行 <code>mySetInterval</code> 的时候返回的 <code>id</code> 依然不是最新的 <code>timeId</code>。因为 <code>timeId</code> 只在 <code>fn</code> 内部被更新了，在外部并不知道它的更新。那有什么办法让 <code>timeId</code> 的更新也让外部知道呢？</p><p>有的，答案就是用全局变量。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> timeId <span class="comment">// 全局变量</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">mySetInterval</span> = (<span class="params">cb, time</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">fn</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">cb</span>() <span class="comment">// 执行传入的回调函数</span></span><br><span class="line">    timeId = <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">// 闭包更新timeId</span></span><br><span class="line">      <span class="title function_">fn</span>() <span class="comment">// 递归调用自己</span></span><br><span class="line">    &#125;, time)</span><br><span class="line">  &#125;</span><br><span class="line">  timeId = <span class="built_in">setTimeout</span>(fn, time) <span class="comment">// 第一个setTimeout</span></span><br><span class="line">  <span class="keyword">return</span> timeId</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是这样有个问题，由于 <code>timeId</code> 是<code>Number</code>类型，当我们这样使用的时候：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> id = <span class="title function_">mySetInterval</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">// 此处id是Number类型，是值的拷贝而不是引用</span></span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="keyword">new</span> <span class="title class_">Date</span>())</span><br><span class="line">&#125;, <span class="number">1000</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">// 2秒后清除定时器</span></span><br><span class="line">  <span class="built_in">clearTimeout</span>(id)</span><br><span class="line">&#125;, <span class="number">2000</span>)</span><br></pre></td></tr></table></figure><p>由于 <code>id</code> 是 <code>Number</code> 类型，我们拿到的是全局变量 <code>timeId</code> 的值拷贝而不是引用，所以上面那段代码依然无效。不过我们已经可以通过全局变量 <code>timeId</code> 来清除计时器了：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123; <span class="comment">// 2秒后清除定时器</span></span><br><span class="line">  <span class="built_in">clearTimeout</span>(timeId) <span class="comment">// 全局变量 timeId</span></span><br><span class="line">&#125;, <span class="number">2000</span>)</span><br></pre></td></tr></table></figure><p>但是上面的实现，不仅与我们平时使用的 <code>clearInterval</code> 的用法有所出入，并且由于 <code>timeId</code> 是一个 <code>Number</code> 类型的变量，导致同一时刻全局只能有一个 <code>mySetInterval</code> 的 <code>id</code> 存在，也即无法做到清除多个 <code>mySetInterval</code> 的计时器。</p><p>所以我们需要一种类型，既能支持多个 <code>timeId</code> 存在，又能实现 <code>mySetInterval</code> 返回的 <code>id</code> 能够被我们的 <code>myClearInterval</code> 使用。你应该能想到，我们要用一个全局的 <code>Object</code> 来做。</p><p>修改代码如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> timeMap = &#123;&#125;</span><br><span class="line"><span class="keyword">let</span> id = <span class="number">0</span> <span class="comment">// 简单实现id唯一</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">mySetInterval</span> = (<span class="params">cb, time</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> timeId = id <span class="comment">// 将timeId赋予id</span></span><br><span class="line">  id++ <span class="comment">// id 自增实现唯一id</span></span><br><span class="line">  <span class="keyword">let</span> <span class="title function_">fn</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    <span class="title function_">cb</span>()</span><br><span class="line">    timeMap[timeId] = <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">      <span class="title function_">fn</span>()</span><br><span class="line">    &#125;, time)</span><br><span class="line">  &#125;</span><br><span class="line">  timeMap[timeId] = <span class="built_in">setTimeout</span>(fn, time)</span><br><span class="line">  <span class="keyword">return</span> timeId <span class="comment">// 返回timeId</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们的 <code>mySetInterval</code> 依然返回了一个 <code>id</code> 值。只不过这个 <code>id</code> 值是全局变量 <code>timeMap</code> 里的一个键的内容。</p><p>我们每次更新 <code>setTimeout</code> 的 <code>id</code> 并不是去更新 <code>timeId</code>，相应的，我们去更新 <code>timeMap[timeId]</code> 里的值。</p><p>这样实现后，我们调用 <code>mySetInterval</code> 虽然获取到的 <code>timeId</code> 是不变的，但是我们通过 <code>timeMap[timeId]</code> 获取到的真正的 <code>setTimeout</code> 的 <code>id</code> 值是会一直更新的。</p><p>另外为了保证 <code>timeId</code> 的唯一性，在这里我简单用了一个自增的全局变量 <code>id</code> 来保证唯一。</p><p>好了，<code>id</code> 值有了，剩下的就是 <code>myClearInterval</code> 的实现了。</p><h3 id="myClearInterval实现"><a href="#myClearInterval实现" class="headerlink" title="myClearInterval实现"></a>myClearInterval实现</h3><p>由于我们的 <code>mySetInterval</code> 返回的 <code>timeId</code> 并不是真正的 <code>setTimeout</code> 返回的 <code>id</code> ，所以并不能简单地通过 <code>clearTimeout(timeId)</code> 来清除计时器。</p><p>不过其实原理也是很类似的，我们只要能拿到真正的 <code>id</code> 就行了：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">myClearInterval</span> = (<span class="params">id</span>) =&gt; &#123;</span><br><span class="line">  <span class="built_in">clearTimeout</span>(timeMap[id]) <span class="comment">// 通过timeMap[id]获取真正的id</span></span><br><span class="line">  <span class="keyword">delete</span> timeMap[id]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试一下：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/setTimeout-2.gif"></p><p>没毛病~</p><p>至此我们就用 <code>setTimeout</code> 和 <code>clearTimeout</code> 简单实现了 <code>setInterval</code> 与<code>clearInterval</code>。当然本文说的是简单实现，毕竟还有一些东西没有完成，比如<code>setTimeout</code> 的 <code>args</code> 参数、Node和浏览器端的 <code>setTimeout</code> 差异等等。也只是一个抛砖引玉，重点在一步步如何实现。感谢阅读~</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这个问题其实是前一段时间舍友的一道面试题。我觉得类似用&lt;code&gt;reduce实现map&lt;/code&gt;、用&lt;code&gt;xxx实现yyy&lt;/code&gt;的题目其实都挺有意思，考察融会贯通的本领。不过相比之下这道题可能更有实际意义。比如我们经常会用 &lt;code&gt;setTimeout&lt;/code&gt; 来实现倒计时。下面来说说我对这个问题的思考。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    <category term="随笔" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="随笔" scheme="https://molunerfinn.com/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>我的2019春招（暑期实习）记录</title>
    <link href="https://molunerfinn.com/my-2019-interview-of-summer-internship/"/>
    <id>https://molunerfinn.com/my-2019-interview-of-summer-internship/</id>
    <published>2019-04-23T20:32:00.000Z</published>
    <updated>2026-03-08T01:14:37.985Z</updated>
    
    <content type="html"><![CDATA[<p>今年的春招（暑期实习）批已经过去大半了，相信不少同学已经拿到了心仪的offer了~本来打算暑假有空再写写这段经历，不过今天晚上正好有空就记录一下吧，希望能给正在或者今后要找前端实习、工作的同学一点点启发和建议。（由于我妹子在北京读书，所以实习的话我只想着申请北京的实习机会，这是本文的大前提）。</p><p>我自己是北邮研二的学生，「主修」前端。我自己的面试经历不多，从1月份到现在总共只面了3家：头条，腾讯·微信和蚂蚁金服·支付宝，很幸运都拿到了offer。其实我觉得主要还是内推对我的帮助特别大，没有内推的话我估计也很难拿offer了。所以经验第一条：<strong>能找内推尽量通过内推来获取面试资格</strong>。帮你内推的学长学姐一般会帮你查看（甚至修改）简历，有的可以直接部门直推给leader，等于省去了HR筛简历的步骤，所以能找到内推就尽量走内推而不是单纯走网申吧。</p><span id="more"></span><h2 id="头条"><a href="#头条" class="headerlink" title="头条"></a>头条</h2><p>1月份的时候有个头条的学长通过邮件联系到我，对我的做的<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>很感兴趣。跟我要了非常简陋的简历，就把我内推了。</p><p>不过后来面试邮件发来后我才知道给我的推的职位是<code>iOS研发工程师</code>。他们组是移动端的组，要招前端，但是可能没有前端名额，就用<code>iOS</code>的职位给我内推了。然后我也就稀里糊涂的去面试了。说实话毕竟是第一次面试，并且当时周边的同学也都没有开始找实习，在仅有的几天时间里我准备的特别不充分。</p><p>头条总共面了我三面，都是视频面。其中一面二面是连着的（一面一结束，马上二面面试官来面我）。由于这个组的性质比较特殊，来面我的面试官都不是写前端的，因此问的网络、计算机相关的问题会更多点。我事后（3月份面完微信和蚂蚁之后）才觉得当时1月份面头条的时候简直回答得一塌糊涂。</p><p>不过感觉自己做的很正确的一件事就是面试完马上把问题记下来了。从中也看出三家公司的侧重点不同。</p><h3 id="头条一面"><a href="#头条一面" class="headerlink" title="头条一面"></a>头条一面</h3><p>头条一面是个年轻的小哥，是做移动端的。先问了我的项目，因为都是前端的他也没太了解，就开始问问题了：</p><ul><li>UTF-8 UTF-16 和 Unicode 什么关系 【当时不会 | 编码规则和字符集】</li><li>TCP三次握手可以理解，为什么需要四次挥手 【四次挥手是2+2】</li><li>常见的HTTP状态码 【相信你们都会】</li><li>GET\POST请求区别 【常规问题】</li><li>HTTP报文分为几部分，分别说了啥，头部尤其重要 【当时说的不全 | 3个部分，以及请求和响应报文的区别】</li><li>HTTPS与TLS或者SSL有了解么，加密是对称加密还是非对称加密 【当时不是特别了解 | 二者都有】</li><li>JS内存管理机制 【标记-清除】</li><li>数组和链表区别和应用场景 【查找-操作】</li><li>动态数组如何实现，查找和插入哪个代价大【当时不会】</li><li>Electron的原理，简单描述 【Node.js+NativeApi+Chromium】</li><li>Vue的原理，简单描述 【Object.defineProperty + Dep + Watcher】</li><li>平时怎么学习新知识，从什么渠道 【博客、掘金、GitHub、StackOverflow等】</li><li>开发项目中遇到最困难、最有挑战的事 【PicGo的插件系统】</li><li>算法题1，求数组里最大和的子数组 【思路说了，没写出来】</li></ul><p>一面算法题虽然思路说对了，但是没写出来的时候我觉得自己已经凉了。结果居然面试官说「你等一下，我去叫二面面试官」。</p><h3 id="头条二面"><a href="#头条二面" class="headerlink" title="头条二面"></a>头条二面</h3><p>和一面就隔了3分钟。二面是交叉面，是另外一个部门的面试官来面的。这个面试官年龄一看就比一面面试官大。简单自我介绍之后，他就开始问我问题了：</p><ul><li>如果我是一个Leader，想要把APP里某个页面的首次开屏渲染时间降低，如何协调前端、服务端、客户端同学。【我说了缓存相关的，不过面试官说不够】</li><li>从输入一个URL到最终用户看到界面，经历那些步骤【常规题】</li><li>对HTTP&#x2F;2有没有了解，以及QUIC协议【都了解过，稍微说了一下我的认知】</li><li>HTTP缓存，304状态码如何而来【常规题】</li><li>算法题2，单链表是否交叉，如何算重叠个数【说了一下思路，但是不是最优解】</li></ul><p>算法题2现在看来真的超级简单。当时我真的没刷过题，平时对算法训练也很少，所以说的思路能过但是不是最优解。面试官说「行吧」（当时觉得凉了哈哈）。</p><h3 id="头条三面"><a href="#头条三面" class="headerlink" title="头条三面"></a>头条三面</h3><p>三面和二面隔了大概几天吧。其实面完二面觉得还是很悬，结果还是收到HR的三面约时间电话。三面面试官是部门leader了。这个面试相对来说最轻松，基本没有问什么复杂的问题：</p><ul><li>谈项目 【简历上的，主要是PicGo】</li><li>Electron原理 【一面说过了】</li><li>markdown渲染原理 【正则匹配】</li><li>对前端框架的想法 【从开发效率到后期维护还有工程化角度说了自己的认知】</li><li>有什么想问的 【问了以后要做啥】</li></ul><p>由于卡着1月底快过年了，所以HR那边在年前给了我口头offer，年后回来就给我发了正式的offer。</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/blog/bytedance.png"></p><p>作为人生中第一份offer，还是挺激动的。不过不是 <code>前端开发</code> 的职位让我心里一直有点不舒服。我想去的其实是专业的前端团队，以及之后入职后做的东西也不是自己特别喜欢的，所以我在想着年后回学校再找找有没有自己更喜欢的实习岗位。当然头条这个岗位也很棒了！</p><p>经验总结二：<strong>算法、数据结构和计算机、网络基础知识很重要，哪怕是前端研发工程师，也是一名工程师。</strong> 所以我寒假回去后就开始针对自己薄弱的算法和数据结构部分开始了恶补。</p><h2 id="腾讯·微信"><a href="#腾讯·微信" class="headerlink" title="腾讯·微信"></a>腾讯·微信</h2><p>腾讯今年春招（暑期实习）开始的时间特别早，从2月底就开始能网申、内推了。尤其3月份一整个月是提前批，并且4月1号之前没走完流程的同学，都要必须参加4月份的笔试。所以理论上是越早内推越好，越到后面简历越多而且万一4月前流程没走完，就得参加笔试了。</p><p>本来我想着先面几家小一点的公司攒攒经验再去投腾讯和蚂蚁金服的，毕竟这两家门槛还是相当高的。原本打算投北京微信的前端岗，但是问了上一届的一个学长说北京的微信不招前端，于是我的重心就放在蚂蚁金服的北京实习了。不过有件事情的发生打破了我原本的规划。</p><p>我有个在微信工作的学长，听说了我的情况之后帮我从微信HR那边问到北京微信今年招前端的情况，但是HC很少。我一听，呀，好机会。赶紧修改了简历发给了学长。内推后没两天我就收到了北京微信的现场面试邀请，心里还是很忐忑的，毕竟那可是微信啊。而且也是我的第一次现场面试。</p><h3 id="微信一面"><a href="#微信一面" class="headerlink" title="微信一面"></a>微信一面</h3><p>到现场后，有个年龄跟我相仿的学长找到了我，说「我是你的一面面试官」。微信的现场面试没有我想象中那么拘谨（两个人一间小屋子那种），是在开阔的大厅里，有很多小圆桌，光线也很好。总之面试体验还是很好的。同时我还看到了很多其他来面试的人。</p><p>一面面试官说他也是北邮毕业的，一下子就感觉放松了不少。接下去就基本是他拿着我的简历开始问问题了。</p><ol><li>问实习做了啥 【研究生导师的公司】</li><li>PicGo做了啥 【简述了一下诞生的过程和它的作用】</li><li>Electron是啥，为什么选择用Electron 【用的人多】</li><li>MVVM与Vue的理解 【数据驱动】</li><li>vue的双向绑定的原理 【Object.defineProperty + Dep + Watcher】</li><li>vue的生命周期以及做了啥，用来干嘛的 【beforeCreate、created、mounted等】</li><li>讲讲Virtual Dom 【稍微说了一下理解】</li><li>写过render函数么，跟template有啥区别 【写过，说了一下区别】</li><li>vue的服务端渲染和客户端渲染区别是啥，服务端渲染作用是啥 【SEO友好，首屏渲染速度等】</li><li>讲讲this指针和箭头函数 【常规题】</li><li>const和let与var的区别 【常规题】</li><li>说说webpack、rollup的tree shanking 【说了tree shanking是啥以及如何实现的】</li><li>webpack的loader和plugin区别是啥 【loader处理某一类文件而plugin可以做「任何」事】</li><li>promise的finally如何实现 【说了一下我的想法，但是后来想想有点不对】</li><li>浏览器和Node端的事件循环的区别 【说了一下我的印象，与setTimeout有关】</li></ol><p>一面的问题基本都答上来了，面试官也觉得很满意，就让我等会，叫来了二面面试官，跟我说是专门搞算法的。（心里一凉）</p><h3 id="微信二面"><a href="#微信二面" class="headerlink" title="微信二面"></a>微信二面</h3><p>面试官跟我说他是北师毕业的，跟我的学校（北邮）很近（哈哈）。然后说，「我们来到简单的算法题吧，不需要你写，只需要你说说思路」</p><ol><li>算法题：m*n的矩阵，只有0、1，找出最大的只包含1的矩形面积。【说了最蠢的解法…面试官一直引导我也没想出怎么实现更优解】</li><li>PicGo做了啥，为啥star这么多 【讲了一下作用】</li></ol><p>算法题又是没做出来（虽然说了最蠢的解法）心里又是一凉，感觉gg。结果面试官说「小伙子思维还挺灵活」（有么！）然后让我等会，叫来了三面面试官。</p><h3 id="微信三面"><a href="#微信三面" class="headerlink" title="微信三面"></a>微信三面</h3><p>三面是个女leader，她对我说「前面的面试官对你的评价很高啊」。于是开始问我的个人经历和项目相关。最后问了我什么时候能来？我一听奇怪，我不是投的暑期实习么？然后她说最近有个项目急着要上线，所以缺人，就额外要了一个前端的HC。我说我实验室暑假前并不放人…所以需要再考虑一下。并且这个时候我听闻他们组实际是做AI的，而前端如果我去了也只有两个人。到这时我感觉有点不对劲，不过leader说之后还有一个广州的电面要我准备一下。</p><h3 id="微信四面"><a href="#微信四面" class="headerlink" title="微信四面"></a>微信四面</h3><p>没过两天就是4面，也是我第一次电话面试。四面就纯粹围绕着我做的项目PicGo开始说了。问的比较注重的部分是我对于PicGo的思考。从开发者和使用者和产品的角度去说明。比如如何维护、如何打磨产品，遇到的问题如何克服，与用户的意见不同时如何应对等等。我感觉更考量我对PicGo的认知和未来的规划，到底是一个用心做的产品还仅仅只是一个star收集者。</p><p>四面面完，没两天三面的leader就打电话过来问我啥时候能去实习。然而在四面面完的这几天里，我就决定了不去了。首先实验室6月底前放不了人；第二个跟我预期的有所出入，我以为是微信的前端团队招实习生（但不是），因为我其实想在前端这块能继续做深入一些，所以就还是把这个offer给拒了。当时想法是如果北京微信这边没有喜欢的岗位，那也没事，好好准备一下蚂蚁金服的面试就好。</p><p>回宿舍我跟舍友一说我把微信的offer拒了，他们只丢过来一句「暴殄天物」。舍得舍得，有舍才有得，后面会再说。</p><h2 id="蚂蚁金服·支付宝"><a href="#蚂蚁金服·支付宝" class="headerlink" title="蚂蚁金服·支付宝"></a>蚂蚁金服·支付宝</h2><p>在面微信的面试阶段前，有个支付宝的北邮师兄通过微信联系上我。他说关注我的<a href="https://github.com/Molunerfinn">GitHub</a>好久了，想给我内推到支付宝的前端团队那边。我自然是开心地答应了。不过我当时想着先完善简历+先把微信面完。不然一下子准备两个大厂的面试，压力大不说，万一时间撞上了反而更尴尬。在拒了微信后我把简历发给了师兄，开始了支付宝那边的内推。</p><p>支付宝这边技术面总共三面+HR一面。全程电话面试。</p><h3 id="支付宝一面"><a href="#支付宝一面" class="headerlink" title="支付宝一面"></a>支付宝一面</h3><p>内推没多久，一面面试官就通过微信联系我，跟我约好了面试时间（第二天晚上7点半）并说「我这一面很轻松的」。在面试之前我有听说蚂蚁金服的面试是比较难的，虽然师兄说很简单但也是做好了被挂的准备。</p><p>7点半准时电话响起。面试官说他也是北邮毕业的，让我稍稍有所放松。然后接下来的问题就让我冷汗直冒。</p><ol><li>介绍一下做的项目 【实验室项目+个人项目】</li><li>前端工程化的理解 【流程+规范+自动化等】</li><li>对Webpack做了哪些配置来提速 【很多，具体可以参考我<a href="https://molunerfinn.com/Webpack-Optimize/">这篇文章</a>】</li><li>一段代码输入babel，把结果再输入babel，结果一样么 【我说应该不一样，但是没说出为什么】</li><li>配置过babel哪些属性 【presets，plugins，env等】</li><li>PicGo的插件如何发布、安装，如何确保插件安全性 【通过npm发布，安全性没有考虑很完全，然后跟面试官聊了安全方面的考量】</li><li>Electron如何实现跨进程通信。还有哪些其他跨进程通信的例子 【ipcMain和ipcRenderer，跨进程通信的比如socket等，我了解的不多】</li><li>Electron打包体积、编译速度相关如何考量，怎么优化或者怎么做的 【我是通过CI打包，没通过自己的机器。所以没有特别考虑这方面的。不过不需要用babel转译能节省一些时间】</li><li>为什么选TypeScript来开发，说说对TS的理解 【静态类型、语法检查等】</li><li>TypeScript的interface编译后会占用空间么，enum呢？（运行时和开发时不一样）【前者答出来了，后者不确定】</li><li>说说什么是服务端渲染以及Vue的服务端渲染如何实现 【直出HTML，通过render函数将VirtualDom渲染模板】</li><li>如果Vue2没有实现VirtualDOM，可以做到服务端渲染吗 【可以】</li><li>Vue的diff算法如何实现 【说了一下之前自己看过的实现】</li><li>【算法题】求两个序列里的最长公共子序列 【稀里糊涂说了一通，好像没错，后来想想其实不对】</li><li>简单说说Vue的响应式原理 【Object.defineProperty + Dep + Watcher】</li><li>你有什么要问我的吗 【主要做什么？答：蚂蚁森林，蚂蚁庄园等】</li></ol><p>一面的难度应该是面的这三个大厂以来最难的。面试过程中我还是比较紧张的，不过一开始确实紧张，后面说开了就好多了。面试官面完之后说等二面联系我吧。二面面试官是他们部门的leader。</p><h3 id="支付宝二面"><a href="#支付宝二面" class="headerlink" title="支付宝二面"></a>支付宝二面</h3><p>一面面完的第二天面试官就加我了，直接约了当天晚上7点半的电面。（等于昨天一面今天二面…）事前我从内推我的师兄那里了解到二面面试官是很厉害的一个人，所以难度应该会比一面面试官高。听到这个消息不觉咽了一下口水，难受。</p><p>7点半准时电话响起。二面面试官的声音和语气给我的感觉是一开始比较低沉的，感觉比较严肃。然后后面的问题果然「没让我失望」地难。</p><ol><li>学前端的经历？ 【15年开始自学，简单说了一下】</li><li>对计算机的体系结构的认知 【懵了，不知道说啥】</li><li>有没有经历过jQuery时代 【有】</li><li>Webpack优化是怎么做的 【跟一面说的差不多】</li><li>上述的优化是基于什么方向去做的 【从cache、减少文件搜索路径、多进程优化等做的】</li><li>上述的优化有没有量化出问题（比如看看每块耗时多久等等）再针对性地做优化 【用了profile查看了开发阶段的编译耗时，做了一个简单的<a href="https://github.com/Molunerfinn/webpack-dev-compile-optimize">插件</a>做了开发阶段的速度提升，但是原理我也没说地太清楚】</li><li>vue-hot-reload原理是啥 【我只打上来websocket+jsonp做的更新，但是实际上更复杂】</li><li>在vue项目里如果我更新了一个js脚本但是页面不更新，我要怎么让vue-hot-reload去更新 【真不会】</li><li>在vue项目里如果我更新了一个js，但是不想让页面重新刷新，而只是更新我js的执行部分，我要怎么让vue-hot-reload去更新【真不会】</li><li>vue的template是如何转换成render functions的 【说了一下正则匹配，AST，但是不知道是如何有机串起来的】</li><li>接上一问，光是正则匹配是无法解决所有问题的，还需要啥，然后怎么做，要哪些阶段？（AST）【说了大概，但是不知道是如何有机串起来的】</li><li>AST相关知识掌握程度是多少，去哪里了解的 【不多，相关博客，跑了一些DEMO】</li><li>写Electron的时候遇到了哪些问题（解决的或者没解决的都说说）【系统级别的右键菜单实现、插件系统等】</li><li>base64怎么编码的 【常规题】</li><li>从输入一个地址到浏览器展现网页的过程 【常规题】</li><li>DNS查询用TCP来做可以么 【可以，但是慢】</li><li>HTTPS握手加密过程 【常规题，这次会了，面头条的时候还不全会】</li><li>setTimeout和Promise的异步的区别，在浏览器和Node下的区别 【事件循环的区别，我说了具体的例子】</li><li>如何用Jest做的Koa和Vue的测试 【对Koa做了api的测试，对Vue做了界面的单元测试】</li><li>CSS和JS哪个更熟悉？【JS】</li><li>接上问，CSS的transform有哪些属性 【rotate，translate等】</li><li>接上问如何实现一个div既平移又变色？transform的矩阵有了解过么 【没答出transform可以带多个属性，知道矩阵，没写过】</li><li>Vue的响应式原理，以及如果一个变量不在页面上出现过（或者使用过），响应式系统是怎么应对的 【render Watcher没有get到这个变量就不会收集它的依赖】</li><li>父子组件如何分开收集依赖，或者说父子组件如何确保父组件只收集自己的依赖，子组件只收集自己的依赖 【父子组件有自己的生命周期】</li><li>在Watcher内再new一个Watcher后，如何保证依赖收集不会出错 【同一时刻只有一个Watcher在工作】</li><li>问一下算法和数据结构掌握程度 → 说一下快排吧 【说了一下快排原理】</li><li>你上面跟前端无关的知识都是从哪里获取的 【实验室项目、同学、自己捣鼓、博客等】</li><li>你有什么要问我的吗 【这个组杭州和北京的部门做的东西一样么，做什么】</li></ol><p>面完感觉很凉，问题的深度是真的深。之前的面试很少有完全答不上来的，而这次二面对 <code>vue-hot-reload</code> 的问题就基本没有答上来。面试官最后给我的反馈大概还是不错的，所以我就在忐忑中等待三面的通知。</p><h3 id="支付宝三面"><a href="#支付宝三面" class="headerlink" title="支付宝三面"></a>支付宝三面</h3><p>过了几天，三面面试官通过电话跟我约了时间，听声音还是很和善的。不过，问题还是依然很有难度啊！问题不多，总共问了三个问题，但是第一问就让我很难受：</p><ol><li>【算法+前端】给定一定数目的粒子，每个粒子有4个属性【位置坐标，半径，速度，加速度】。求问在如下数量下，用什么方式绘制这些运动粒子，用什么数据结构来存储。<ol><li>20个 【DOM，Canvas】</li><li>500个 【DOM，Canvas】</li><li>20000个 → 200w个 【Canvas，但是不够，因为没有必要把200w个点都渲染出来，只需要渲染可视区的。所以问题的关键是如何找到只在可视区出现的圆，这是一道数据结构+算法题。】</li></ol></li><li>接上一题，如果是500个用DOM来绘制的粒子，请问使用Vue或者React的VirtualDom技术来实现，对比用原生操作DOM（假设极致优化）来实现，哪种方案的性能更好。【我说了原生操作，并给出VirtualDom不适合这个例子的理由】</li><li>给定一个APP内的营销页面，用户可能在离线状态下打开APP。如果营销页面的图片已经过期了，应该要被撤下，否则会引起歧义。请问用什么办法能够撤下。如果不能用JS，如果用户修改了客户端时间呢？【问题难度一步步加深，先问常规离线模式实现，然后开始不让用JS，并且客户端时间不准确怎么做。没答全。】</li></ol><p>这个面试总共只有45分钟不到，面试官说不能太长否则影响我的评价。我就说我第一题答得不够好。面试官说「不是不够好兄弟，是很不好！你第二题答得很不错，第三题有所偏差，但是你第一题答地太差了」</p><p>哈哈，当时听完觉得应该是凉了吧~然后面试官最后说了一句，「等之后HR会联系你」。噫，所以还是有戏？</p><p>经验总结三：<strong>只知其然不知其所以然是不行的，要对原理了解更深才能更好地解决问题。</strong></p><p>不过人生总是有所波澜。</p><h2 id="微信·小程序"><a href="#微信·小程序" class="headerlink" title="微信·小程序"></a>微信·小程序</h2><p>在我面支付宝结束前后，微信那边的HR小姐姐联系到我问我为什么把北京的岗位拒了。我说了之前我考虑的理由（主要是团队不符合预期啊啥的）。本来以为跟微信的缘分就这样了。然后HR小姐姐不死心，帮我联系了广州微信小程序的前端部门，问我去不去那边实习。我跟妹子商量了一下，暑期实习去广州两个月也能接受。于是就答应了。不过小程序那边还需要加面<br>。小程序这个部门做的是小程序开发者工具的，我觉得很合我的胃口，正好我也比较喜欢写工具类。</p><p>一波三折，在等待支付宝HR给我电话的这段时间里，我在两天内就拿到了微信小程序的offer。</p><h3 id="微信·小程序一面"><a href="#微信·小程序一面" class="headerlink" title="微信·小程序一面"></a>微信·小程序一面</h3><p>三月最后一周的周一下午，我记得很清楚。3点开始一面。面试官给了我一个链接，让我一小时内做完题然后他再跟我电话聊。</p><p>一个小时总共两道题，这两个笔试题做完，面试官电话就过来了，简单问了一些问题：</p><ol><li>对着笔试题的一些提问，比如第一题的this指针问题，第二题的思路问题 【一遍过】</li><li>HTTPS建立连接过程 【常规题】</li><li>前端缓存的认知 【常规题，缓存的类型，不同缓存的作用等等】</li><li>前端安全的认知 【XSS，CSRF等】</li><li>有什么想问的吗 【为啥小程序开发者工具用NW.js而不是Electron】</li></ol><p>面试官问了大概半小时，就说之后二面的leader会联系我。由于笔试题都做出来，所以感觉还是比较良好的。只是不知道二面来得这么快。</p><h3 id="微信·小程序二面"><a href="#微信·小程序二面" class="headerlink" title="微信·小程序二面"></a>微信·小程序二面</h3><p>二面面试官隔了大概半小时就打电话来了，主要就看着我的<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>这个项目在问，可能是因为技术栈（Electron）和小程序开发者工具（NW.js）比较接近吧。</p><ol><li>为什么选择Electron而不是用网页实现PicGo 【因为需要做配置、插件化、需要用到Node.js的API等】</li><li>介绍一下PicGo</li><li>PicGo如何做的更新策略，如何实现静默更新，如何实现代码级别热更新，如何在读写文件权限不够的情况下热更新 【更新策略其实很简单，后面面试官问的更新策略是我未实现的，但是跟他一起谈了一下思路】</li><li>写PicGo遇到的最大的问题 【插件系统】</li><li>插件系统如何实现 【读配置、加载、生命周期函数等等】</li><li>写PicGo遇到过安全相关的问题么，如何处理 【插件的安全相关】</li><li>写PicGo遇到过性能相关的问题么 【有，相册页图片多会卡顿，说了如何处理等】</li><li>有什么想问的吗 【没啥了】</li></ol><p>面试官的语气非常和善，跟我探讨的时候也是基本以商量的语气。末了还夸了一下这个项目做得还是挺完整的。（其实还有一个很重要的「测试」部分没写。。。）考察的重点问题已经不是功能问题，而且类似安全、更新策略等这些平时可能写东西的时候不会太注意的问题。所以如果只是一个玩具项目，可能确实谈不上来。还好之前很多坑自己踩过，所以跟面试官聊起来也比较愉快。</p><p>经验总结四：<strong>一个好的（开源）项目非常加分。好的意思不是star多，而是你对它的思考、实践多。</strong><br>经验总结五：<strong>如果你有一个做得很好的项目，一定要让面试官看到，并引导他问你的项目来把你熟悉的东西说出来。</strong></p><p>第二天收到HR电话联系说已经通过面试了，第三天就发了Offer。</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/blog/wechat-offer.png"></p><p>由于小程序这个组做的东西是开发者工具，很合我的胃口，于是我就接了这个Offer，而此时我还没接到支付宝的HR电话。微信的这个「抢人」速度是真的快。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>支付宝HR电话在后面好久才打来。此时我已经接了小程序的offer了，于是暑期就没办法去支付宝实习了。我说了一下我暑假可能没法去实习，但是秋招还要回北京秋招。所以问能否保留秋招终面资格（跟去年一样）。支付宝的HR给我的反馈就是不一定，不好说。我想想反正如果不保留资格，到时候回北京再面就是了。</p><p>于是前两天终于发来的offer，也只能拒掉了。同时我也只能跟头条的HR说了一下情况，真的很不好意思，秋招还有机会。</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/blog/alipay-offer-1.png"></p><p>我的春招（暑期实习）之旅也就这样结束。其实我大可接受支付宝的offer实习然后直接转正，不过我想着既然有一个更喜欢的机会去尝试一下又何尝不可呢。其实从第一次拒绝微信的offer到后面又接了小程序的offer，我觉得都是因为我想做自己喜欢做的事吧。</p><p>最后经验总结六：<strong>Do what you love, love what you do.</strong></p><p>希望这份经历也能给你带来帮助。</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><p>我的<a href="https://github.com/Molunerfinn">GitHub</a>，我的<a href="https://molunerfinn.com/">博客</a></p><p>我自己的主要开源项目</p><ul><li><a href="https://github.com/Molunerfinn/PicGo">PicGo</a> 4328star</li><li><a href="https://github.com/Molunerfinn/hexo-theme-melody">hexo-theme-melody</a> 634star</li><li><a href="https://github.com/Molunerfinn/vue-koa-demo">vue-koa-demo</a> 587star</li><li><a href="https://github.com/Molunerfinn/node-github-profile-summary">node-github-profile-summary</a> 243star</li></ul><p>以及<a href="https://github.com/PicGo">PicGo-Group</a>的项目。</p><p>我参与的开源项目</p><ul><li><a href="https://github.com/vuejs/vue-cli">vue-cli</a> 【哈哈只是改了一下文档】</li><li><a href="https://github.com/aioutecism/amVim-for-VSCode">amVim-for-VSCode</a> 【加了一些<code>:</code>命令支持】</li><li><a href="https://github.com/PicGo/vs-picgo">vs-picgo</a> 【PicGo的VSCode版】</li></ul><p>等等。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;今年的春招（暑期实习）批已经过去大半了，相信不少同学已经拿到了心仪的offer了~本来打算暑假有空再写写这段经历，不过今天晚上正好有空就记录一下吧，希望能给正在或者今后要找前端实习、工作的同学一点点启发和建议。（由于我妹子在北京读书，所以实习的话我只想着申请北京的实习机会，这是本文的大前提）。&lt;/p&gt;
&lt;p&gt;我自己是北邮研二的学生，「主修」前端。我自己的面试经历不多，从1月份到现在总共只面了3家：头条，腾讯·微信和蚂蚁金服·支付宝，很幸运都拿到了offer。其实我觉得主要还是内推对我的帮助特别大，没有内推的话我估计也很难拿offer了。所以经验第一条：&lt;strong&gt;能找内推尽量通过内推来获取面试资格&lt;/strong&gt;。帮你内推的学长学姐一般会帮你查看（甚至修改）简历，有的可以直接部门直推给leader，等于省去了HR筛简历的步骤，所以能找到内推就尽量走内推而不是单纯走网申吧。&lt;/p&gt;</summary>
    
    
    
    <category term="日志" scheme="https://molunerfinn.com/categories/%E6%97%A5%E5%BF%97/"/>
    
    <category term="随笔" scheme="https://molunerfinn.com/categories/%E6%97%A5%E5%BF%97/%E9%9A%8F%E7%AC%94/"/>
    
    
    <category term="随笔" scheme="https://molunerfinn.com/tags/%E9%9A%8F%E7%AC%94/"/>
    
    <category term="笔记" scheme="https://molunerfinn.com/tags/%E7%AC%94%E8%AE%B0/"/>
    
  </entry>
  
  <entry>
    <title>Electron-vue开发实战7——命令行调用与系统级别右键菜单的实现</title>
    <link href="https://molunerfinn.com/electron-vue-8/"/>
    <id>https://molunerfinn.com/electron-vue-8/</id>
    <published>2019-04-16T15:50:00.000Z</published>
    <updated>2026-03-08T01:14:37.984Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>前段时间，我用<a href="https://github.com/SimulatedGREG/electron-vue">electron-vue</a>开发了一款跨平台（目前支持主流三大桌面操作系统）的免费开源的图床上传应用——<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>，在开发过程中踩了不少的坑，不仅来自应用的业务逻辑本身，也来自electron本身。在开发这个应用过程中，我学了不少的东西。因为我也是从0开始学习electron，所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历，用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。</p><p>预计将会从几篇<a href="https://molunerfinn.com/tags/Electron-vue/">系列文章</a>或方面来展开：</p><ol><li><a href="https://molunerfinn.com/electron-vue-1/">electron-vue入门</a></li><li><a href="https://molunerfinn.com/electron-vue-2/">Main进程和Renderer进程的简单开发</a></li><li><a href="https://molunerfinn.com/electron-vue-3/">引入基于Lodash的JSON database——lowdb</a></li><li><a href="https://molunerfinn.com/electron-vue-4/">跨平台的一些兼容措施</a></li><li><a href="https://molunerfinn.com/electron-vue-5/">通过CI发布以及更新的方式</a></li><li><a href="https://molunerfinn.com/electron-vue-6/">开发插件系统——CLI部分</a></li><li><a href="https://molunerfinn.com/electron-vue-7/">开发插件系统——GUI部分</a></li><li><a href="https://molunerfinn.com/electron-vue-8/">命令行调用与系统级别右键菜单的实现</a></li><li>想到再写…</li></ol><h2 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h2><p><code>PicGo</code>是采用<code>electron-vue</code>开发的，所以如果你会<code>vue</code>，那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如<code>react</code>、<code>angular</code>，那么纯按照本教程虽然在render端（可以理解为页面）的构建可能学习到的东西不多，不过在main端(<code>Electron</code>的主进程）应该还是能学习到相应的知识的。</p><p>如果之前的文章没阅读的朋友可以先从<a href="https://molunerfinn.com/tags/Electron-vue/">之前的文章</a>跟着看。本文主要是基于PicGo v2.1.0版本更新的重要内容做的讲述。</p><span id="more"></span><h2 id="命令行调用"><a href="#命令行调用" class="headerlink" title="命令行调用"></a>命令行调用</h2><p>我们在使用一些<code>Electron</code>开发的应用程序的时候，可以发现有些程序是可以通过命令行唤起的。比如<code>VSCode</code>，在macOS的<code>.bash_profile</code>里可以设置<code>alias code=&#39;/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/code&#39;</code>，这样就可以在命令行里通过<code>code xxx.js</code>来调用VSCode打开文件了。如果想打开当前目录，可以通过<code>code .</code>，如果想打开某个目录<code>code xxx</code>等等。</p><p>命令行调用里其实还涉及到一个问题，有的时候我们的应用是个「单例应用」，也就是不能「多开」。如何在只能单开的应用里，也实现命令行调用呢？比如<code>PicGo</code>，在软件打开的时候，命令行调用它也能上传图片，而不是打开一个新的<code>PicGo</code>窗口。没事，下面会详细说明。</p><h3 id="实现命令行调用"><a href="#实现命令行调用" class="headerlink" title="实现命令行调用"></a>实现命令行调用</h3><p>首先我们要来实现命令行调用。其实<code>Electron</code>的命令行调用没有什么特殊的地方，与在<code>Node.js</code>端很类似。我以<code>PicGo</code>举例：</p><p>当我们在Windows下安装好了<code>PicGo</code>之后，可以在安装目录里找到<code>PicGo.exe</code>。你有没有想过在命令行里运行这个<code>exe</code>会怎么样呢？在安装目录里打开<code>powershell</code>，输入<code>.\PicGo.exe</code>，你会发现<code>PicGo</code>已经被打开了。如果我是加了一些参数打开会怎么样呢<code>.\PicGo.exe upload</code></p><p>我们可以在<code>main</code>进程里的<code>ready</code>事件里把命令行参数打印出来：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">app.<span class="title function_">on</span>(<span class="string">&#x27;ready&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(process.<span class="property">argv</span>) <span class="comment">// [&#x27;D:\\PicGo.exe&#x27;, &#x27;upload&#x27;]</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>关键出现了，我们可以通过<code>process.argv</code>这个在<code>Node.js</code>端获取命令行参数的关键变量同样获得<code>Electron</code>被命令行打开后的命令行参数。那么我们就可以在<code>main</code>进程的<code>ready</code>阶段通过获取的<code>process.argv</code>参数来实现我们对应的功能。</p><p>对于PicGo而言，如果通过命令行打开它，并且传递了<code>upload xxx.jpg</code>的话，我们就可以认为用户需要调用PicGo来实现上传一张图片。那么我们可以这么做（以下是实例代码）：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">getUploadFiles</span> = (<span class="params">argv = process.argv, cwd = process.cwd()</span>) =&gt; &#123;</span><br><span class="line">   files = argv.<span class="title function_">slice</span>(<span class="number">2</span>) <span class="comment">// 过滤[&#x27;D:\\PicGo.exe&#x27;, &#x27;upload&#x27;]这两个参数，直接获取需要上传的图片路径</span></span><br><span class="line">   <span class="keyword">let</span> result = []</span><br><span class="line">   <span class="keyword">if</span> (files.<span class="property">length</span> &gt; <span class="number">0</span>) &#123; <span class="comment">// 如果图片列表不为空</span></span><br><span class="line">     result = files.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> &#123;</span><br><span class="line">       <span class="keyword">if</span> (path.<span class="title function_">isAbsolute</span>(item)) &#123; <span class="comment">// 如果是绝对路径</span></span><br><span class="line">         <span class="keyword">return</span> &#123;</span><br><span class="line">           <span class="attr">path</span>: item</span><br><span class="line">         &#125;</span><br><span class="line">       &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">         <span class="keyword">let</span> tempPath = path.<span class="title function_">join</span>(cwd, item) <span class="comment">// 如果是相对路径，就拼接</span></span><br><span class="line">         <span class="keyword">if</span> (fs.<span class="title function_">existsSync</span>(tempPath)) &#123; <span class="comment">// 判断文件是否存在</span></span><br><span class="line">           <span class="keyword">return</span> &#123;</span><br><span class="line">             <span class="attr">path</span>: tempPath</span><br><span class="line">           &#125;</span><br><span class="line">         &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">           <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line">         &#125;</span><br><span class="line">       &#125;</span><br><span class="line">     &#125;).<span class="title function_">filter</span>(<span class="function"><span class="params">item</span> =&gt;</span> item !== <span class="literal">null</span>) <span class="comment">// 排除为null的路径</span></span><br><span class="line">   &#125;</span><br><span class="line">   <span class="keyword">return</span> result <span class="comment">// 返回结果</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>拿到图片列表后就执行自带的上传逻辑即可。下面说说单开应用的命令行调用注意事项。</p><h3 id="实现单例应用的命令行调用"><a href="#实现单例应用的命令行调用" class="headerlink" title="实现单例应用的命令行调用"></a>实现单例应用的命令行调用</h3><p><code>Electron</code>的发展很快，本文讲述的<code>Electron</code>版本为当前最新的<code>v4.1.4</code>，所以关于实现单例应用的<code>api</code>也是跟随<a href="https://electronjs.org/docs/api/app">官方文档</a>走的，如果你的Electron版本不是<code>v4.x</code>，那么需要找对应版本的<code>Electron</code>文档。</p><p>当前版本下实现单例应用的官方例子是：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; app &#125; = <span class="built_in">require</span>(<span class="string">&#x27;electron&#x27;</span>)</span><br><span class="line"><span class="keyword">let</span> myWindow = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> gotTheLock = app.<span class="title function_">requestSingleInstanceLock</span>() <span class="comment">// 拿到单例锁</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (!gotTheLock) &#123; <span class="comment">// 如果一个应用二次打开，那么getTheLock为false</span></span><br><span class="line">  app.<span class="title function_">quit</span>() <span class="comment">// 立即退出二次打开的应用</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  app.<span class="title function_">on</span>(<span class="string">&#x27;second-instance&#x27;</span>, <span class="function">(<span class="params">event, commandLine, workingDirectory</span>) =&gt;</span> &#123; <span class="comment">// 一个应用尝试打开第二个实例时触发</span></span><br><span class="line">    <span class="comment">// Someone tried to run a second instance, we should focus our window.</span></span><br><span class="line">    <span class="keyword">if</span> (myWindow) &#123;</span><br><span class="line">      <span class="keyword">if</span> (myWindow.<span class="title function_">isMinimized</span>()) myWindow.<span class="title function_">restore</span>()</span><br><span class="line">      myWindow.<span class="title function_">focus</span>()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Create myWindow, load the rest of the app, etc...</span></span><br><span class="line">  app.<span class="title function_">on</span>(<span class="string">&#x27;ready&#x27;</span>, <span class="function">() =&gt;</span> &#123;</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意有个<code>second-instance</code>事件。当我们试图在打开一个单例应用之后再打开这个应用的时候，就会触发这个事件。并且这个事件的回调函数里，有<code>commandLine</code>和<code>workingDeirectory</code>，实际上它们就是<code>process.argv</code>和对应的<code>cwd</code>（执行路径）。因此我们可以在这个事件里书写当应用试图被二次打开的时候应该做的事的逻辑。以下依然以PicGo举例：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">app.<span class="title function_">on</span>(<span class="string">&#x27;second-instance&#x27;</span>, <span class="function">(<span class="params">event, commandLine, workingDirectory</span>) =&gt;</span> &#123;</span><br><span class="line"> <span class="keyword">let</span> files = <span class="title function_">getUploadFiles</span>(commandLine, workingDirectory)</span><br><span class="line"> <span class="keyword">if</span> (files === <span class="literal">null</span> || files.<span class="property">length</span> &gt; <span class="number">0</span>) &#123; <span class="comment">// 如果有文件列表作为参数，说明是命令行启动</span></span><br><span class="line">   <span class="keyword">if</span> (files === <span class="literal">null</span>) &#123; <span class="comment">// 如果为null说明是让PicGo上传剪贴板的图片</span></span><br><span class="line">     <span class="title function_">uploadClipboardFiles</span>()</span><br><span class="line">   &#125; <span class="keyword">else</span> &#123; <span class="comment">// 否则说明是让PicGo上传具体的图片文件</span></span><br><span class="line">     <span class="comment">// ...</span></span><br><span class="line">     <span class="title function_">uploadChoosedFiles</span>(win.<span class="property">webContents</span>, files)</span><br><span class="line">   &#125;</span><br><span class="line"> &#125; <span class="keyword">else</span> &#123; <span class="comment">// 如果files === [] 说明并不是命令行启动或者并没有带额外参数</span></span><br><span class="line">   <span class="keyword">if</span> (settingWindow) &#123; <span class="comment">// 说明用户是点击了PicGo图标启动，那么这个时候把原有的窗口调出来并focus即可</span></span><br><span class="line">     <span class="keyword">if</span> (settingWindow.<span class="title function_">isMinimized</span>()) &#123;</span><br><span class="line">       settingWindow.<span class="title function_">restore</span>()</span><br><span class="line">     &#125;</span><br><span class="line">     settingWindow.<span class="title function_">focus</span>()</span><br><span class="line">   &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>这里我们通过读取<code>commandLine</code>参数，来判断用户是用命令行来调用<code>PicGo</code>上传图片的，还是仅仅是通过<code>PicGo</code>的图标再次打开<code>PicGo</code>的。关键的逻辑就是判断<code>commandLine</code>里有没有关键的参数，从而得出是否是从命令行调用我们的应用的。如果用户仅仅是通过<code>PicGo</code>图标再次打开<code>PicGo</code>，那么我们应该把之前打开过的窗口复原并激活，告诉用户你之前已经打开过这个应用了。当然具体的业务逻辑不能一概而论，这里只是我对<code>PicGo</code>的一点理解，只需知道核心是监听<code>second-instance</code>事件即可。</p><p>以下是上述实现的截图，注意命令行输出都只在第一个终端进程里，说明我们实现了单例应用的命令行调用：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/commandline-picgo.gif"></p><h4 id="macOS的命令行调用"><a href="#macOS的命令行调用" class="headerlink" title="macOS的命令行调用"></a>macOS的命令行调用</h4><p>其实这个章节到上面基本结束。不过我想起我演示的是在Windows下做的，相对简单。而macOS下的命令行调用<code>Electron</code>应用会有个坑，所以还是要说一下为好。（由于我没有Linux机器，所以Linux部分就不说明了，有兴趣的朋友可以测试一下跟我反馈！）</p><p>大家都知道<code>macOS</code>的应用基本是放在<code>Application</code>下的，所以我们会很自然想到直接命令行调用它们：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">open /Applications/PicGo.app</span><br></pre></td></tr></table></figure><p>但是这样做并不能传递参数进去，因为执行命令的是<code>open</code>。</p><p>所以我们需要到更深层次的路径启动<code>PicGo</code>并传递参数进去：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/Applications/PicGo.app/Contents/MacOS/PicGo upload xxx.jpg</span><br></pre></td></tr></table></figure><p>只有这样才能像Windows那样类似<code>PicGo.exe</code>来实现调用。</p><p>值得注意的是，<code>Electron</code>的macOS应用想要在生产阶段打开<code>debug</code>模式查看<code>console</code>的输出也是到上述应用的对应目录下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/Applications/PicGo.app/Contents/MacOS/PicGo --debug</span><br></pre></td></tr></table></figure><p>而<code>Widnows</code>相对简单，只需要：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.\PicGo.exe --debug</span><br></pre></td></tr></table></figure><p>（Linux请自测）</p><h2 id="系统级别右键菜单"><a href="#系统级别右键菜单" class="headerlink" title="系统级别右键菜单"></a>系统级别右键菜单</h2><p>在实现了命令行调用的功能之后，我就在考虑给PicGo加上原生的系统右键菜单。这样做的好处是用户可以直接在一张图片上右键-&gt;通过PicGo上传。例如：</p><p>Windows下：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/windows-context-menu.png"></p><p>macOS下：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/macos-context-menu.png"></p><p>接下来说说二者在实现上不同的地方。（Linux没有测试，欢迎有兴趣的小伙伴测试一下跟我说说~）</p><h3 id="Windows"><a href="#Windows" class="headerlink" title="Windows"></a>Windows</h3><p>Windows的右键菜单的原理其实很简单，在注册表里写入值就行。篇幅原因不会对Windows注册表的知识做过多的展开。我们只关注往哪里写值，写哪些值才能实现我们要的效果。</p><p>首先我们可以看看VScode是如何实现右键菜单「Open with Code」的。</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/vscode-context-menu.png" alt="VScode的右键菜单"></p><p>在系统里按快捷键<code>WIN+R</code>然后输入<code>regedit</code>打开注册表编辑器，我们来找到<code>VSCode</code>的右键菜单所在地：</p><p><code>HKEY_CLASSES_ROOT</code> → <code>*</code> → <code>shell</code> → <code>VSCode</code>:</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/vscode-reg.png"></p><p>可以看到一个「默认」的属性下的数据为「Open w&amp;ith Code」，这个就是我们看到的菜单名。而一个叫「Icon」的属性下的数据为<code>VSCode</code>的<code>exe</code>安装路径。所以可以认为这个<code>Icon</code>可以获取<code>exe</code>的<code>Icon</code>并显示到菜单上。</p><p>不过这里还没有看到如何将文件路径作为参数传入<code>VScode</code>的。继续看：</p><p><code>HKEY_CLASSES_ROOT</code> → <code>*</code> → <code>shell</code> → <code>VSCode</code> → <code>command</code>:</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/vscode-reg-2.png"></p><p>在<code>command</code>目录下我们看到了如下数据：</p><p><code>&quot;C:\Users\PiEgg\AppData\Local\Programs\Microsoft VS Code\Code.exe&quot; &quot;%1&quot;</code></p><p>可以看出这个<code>%1</code>就是作为参数传给<code>Code.exe</code>的。有了<code>VSCode</code>作为参考，给自己的<code>Electron</code>应用实现一个系统级别的右键菜单也不难了。有人可能会说我可以在应用启动阶段通过某些<code>npm</code>包（比如<a href="https://www.npmjs.com/package/windows-registry">windows-registry</a>）来实现对注册表的写入。</p><p>不过实际上，在<code>Windows</code>平台，如果你是用<code>electron-builder</code>打包的话有一个更简洁的解决方案，那就是编写<code>NSIS</code>脚本来实现，对此<code>electron-builder</code>官方给出的<a href="https://www.electron.build/configuration/nsis#custom-nsis-script">文档</a>可以一看。</p><p>本文不对<code>NSIS</code>脚本做过多的描述，你只需要知道它是用来生成<code>Windows</code>安装界面的一门脚本语言，你可以通过它来控制安装（卸载）界面都有哪些元素。并且它可以接入安装的生命周期，做一些操作，比如写入注册表。我们利用这个特性，来给PicGo做一个安装阶段写入注册表的操作，实现系统级别的右键菜单。</p><p><code>electron-builder</code>给<code>NSIS</code>暴露的钩子主要有<code>customHeader</code>, <code>preInit</code>, <code>customInit</code>, <code>customInstall</code>, <code>customUnInstall</code>，等等。</p><p>我们可以在<code>customInstall</code>阶段通过获取用户安装PicGo的路径<code>$INSTDIR</code>来实现对注册表关键值的写入。自己书写的<code>installer.nsh</code>默认放在项目的<code>build</code>目录下，那么<code>electron-builder</code>在构建<code>Windows</code>应用的时候将会自动读取这个文件以及<code>package.json</code>里的配置来生成安装界面。</p><p>写入注册表的格式大概是这样：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">WriteRegStr &lt;reg-path&gt; &lt;your-reg-path&gt; &lt;attr-name&gt; &lt;value&gt;</span><br></pre></td></tr></table></figure><p>以下是PicGo的<code>installer.nsh</code>，仅供参考：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">!macro customInstall</span><br><span class="line">   WriteRegStr HKCR &quot;*\shell\PicGo&quot; &quot;&quot; &quot;Upload pictures w&amp;ith PicGo&quot;</span><br><span class="line">   WriteRegStr HKCR &quot;*\shell\PicGo&quot; &quot;Icon&quot; &quot;$INSTDIR\PicGo.exe&quot;</span><br><span class="line">   WriteRegStr HKCR &quot;*\shell\PicGo\command&quot; &quot;&quot; &#x27;&quot;$INSTDIR\PicGo.exe&quot; &quot;upload&quot; &quot;%1&quot;&#x27;</span><br><span class="line">!macroend</span><br><span class="line">!macro customUninstall</span><br><span class="line">   DeleteRegKey HKCR &quot;*\shell\PicGo&quot;</span><br><span class="line">!macroend</span><br></pre></td></tr></table></figure><p>注意<code>HKCR</code>即是注册表目录<code>HKEY_CLASSES_ROOT</code>的缩写。在写<code>value</code>的时候如果要写多个参数，可以用单引号包起来。<code>attr-name</code>不写即为默认。相信有了<code>VSCode</code>的右键菜单注册表说明，你也能看得懂上面的PicGo的脚本了。同时注意我们应该在卸载阶段将之前写的注册表删除，以免用户卸载了应用之后菜单还在，上述脚本的后面部分是是在做这个事情。</p><p>因为上一章实现了命令行调用，所以我们的菜单就可以通过<code>&#39;&quot;$INSTDIR\PicGo.exe&quot; &quot;upload&quot; &quot;%1&quot;&#39;</code>来实现菜单调用命令了。</p><h3 id="macOS"><a href="#macOS" class="headerlink" title="macOS"></a>macOS</h3><p>macOS的话可以通过实现自动化脚本来生成右键菜单。打开<code>automator</code>：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/mac-automator.png"></p><p>然后新建一个<code>快速操作</code>:</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/automator-quick.png"></p><p>将快速操作的工作流程限制到<code>图像文件</code>，并且只作用于<code>访达.app</code>里，同时在左侧菜单里找到<code>shell</code>组件，将其拖拽到右侧编辑区：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/automator-quick-1.png"></p><p>将<code>shell</code>选择成<code>/bin/bash</code>，传递输入选成<code>作为自变量</code>。</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/automator.png"></p><p>然后将默认的内容改成如下(实际上就差不多是之前说的<code>macOS</code>下如何命令行调用<code>Electron</code>应用的写法)：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/Applications/PicGo.app/Contents/MacOS/PicGo upload <span class="string">&quot;<span class="variable">$@</span>&quot;</span> &gt; /dev/null 2&gt;&amp;1 &amp;</span><br></pre></td></tr></table></figure><p>其中macOS的快捷操作里，是通过<code>&quot;$@&quot;</code>来作为参数传递的。</p><p>如何作为右键菜单？只要把你生成的这个workflow文件（夹），放到<code>~/Library/Services</code>这个目录下就行了。</p><p>这样你就在你右键菜单里看到它：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/macos-context-menu.png"></p><p>如果你的服务项过多的话，会在服务的二级菜单里看到它：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/macOS-context-menu-2.png"></p><p>其中，菜单名就是你生成的这个workflow的文件（夹）名。</p><p>那么生成了这个workflow之后，我们如何实现不让用户手动创建，而是自动帮他们放到<code>~/Library/Services</code>目录下呢？macOS没有Windows那么方便的安装工具脚本语言，那么我们可以在<code>main</code>进程里手动来实现这个功能。下面是PicGo的<a href="https://github.com/Molunerfinn/PicGo/blob/dev/src/main/utils/beforeOpen.js">beforeOpen.js</a>，其中我们将我们生成的<code>workflow</code>文件（夹）放到项目的<code>static</code>目录下。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="keyword">import</span> os <span class="keyword">from</span> <span class="string">&#x27;os&#x27;</span></span><br><span class="line"><span class="keyword">if</span> (process.<span class="property">env</span>.<span class="property">NODE_ENV</span> !== <span class="string">&#x27;development&#x27;</span>) &#123;</span><br><span class="line">  <span class="variable language_">global</span>.<span class="property">__static</span> = path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;/static&#x27;</span>).<span class="title function_">replace</span>(<span class="regexp">/\\/g</span>, <span class="string">&#x27;\\\\&#x27;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (process.<span class="property">env</span>.<span class="property">DEBUG_ENV</span> === <span class="string">&#x27;debug&#x27;</span>) &#123;</span><br><span class="line">  <span class="variable language_">global</span>.<span class="property">__static</span> = path.<span class="title function_">join</span>(__dirname, <span class="string">&#x27;../../../static&#x27;</span>).<span class="title function_">replace</span>(<span class="regexp">/\\/g</span>, <span class="string">&#x27;\\\\&#x27;</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">beforeOpen</span> (<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> dest = <span class="string">`<span class="subst">$&#123;os.homedir&#125;</span>/Library/Services/Upload pictures with PicGo.workflow`</span></span><br><span class="line">  <span class="keyword">if</span> (fs.<span class="title function_">existsSync</span>(dest)) &#123; <span class="comment">// 判断是否存在</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123; <span class="comment">// 如果不存在就复制过去</span></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      fs.<span class="title function_">copySync</span>(path.<span class="title function_">join</span>(__static, <span class="string">&#x27;Upload pictures with PicGo.workflow&#x27;</span>), dest)</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(e)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> beforeOpen</span><br></pre></td></tr></table></figure><p>然后在主进程里加入这个方法，并判断是否在macOS下运行：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// main/index.js</span></span><br><span class="line"><span class="keyword">import</span> beforeOpen <span class="keyword">from</span> <span class="string">&#x27;./utils/beforeOpen&#x27;</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="keyword">if</span> (process.<span class="property">platform</span> === <span class="string">&#x27;darwin&#x27;</span>) &#123;</span><br><span class="line">  <span class="title function_">beforeOpen</span>()</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>这样用户在安装PicGo之后，打开软件之后，他的右键菜单就多了一个「Upload pictures with PicGo」项了。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>至此，一个<code>Electron</code>应用的命令行调用以及系统级别右键菜单的实现就讲述完了。当然可能还有其他实现的方式，以及更细致的实现（比如还能支持文件夹右键等等）。我在这里也只是一个抛砖引玉，其他的实现或者更好的实现方式需要自己摸索啦。当然本文没有Linux的相关内容，主要是我时间有限并且没有Linux机器，所以也希望有兴趣的朋友自己在Linux下实现了本文的功能后也能跟我说说~</p><p>本文很多都是我在开发<code>PicGo</code>的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的<code>electron-vue</code>开发带来一些启发。文中相关的代码，你都可以在<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>和<a href="https://github.com/PicGo/PicGo-Core">PicGo-Core</a>的项目仓库里找到，欢迎star~如果本文能够给你带来帮助，那么将是我最开心的地方。如果喜欢，欢迎关注我的<a href="https://molunerfinn.com/">博客</a>以及<a href="https://molunerfinn.com/tags/Electron-vue/">本系列文章</a>的后续进展。（PS: 下一篇文章应该会讲述一下如何构建一个Electron应用 <strong>可扩展的快捷键系统</strong> 。）</p><blockquote><p><strong>注：文中的图片除未特地说明之外均属于我个人作品，需要转载请私信</strong></p></blockquote><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p>感谢这些高质量的文章、问题等：</p><ol><li><a href="https://www.xiebruce.top/17.html">一个还不错的图床工具-PicUploader</a></li><li><a href="https://stackoverflow.com/questions/49552703/passing-command-line-arguments-to-electron-executable-after-installing-an-alrea">Passing command line arguments to electron executable (after installing an already packaged app)</a></li><li><a href="https://github.com/SimulatedGREG/electron-vue/issues/581">Command Line Arguments in Dev Mode</a></li><li><a href="https://electronjs.org/docs/api/app#apprequestsingleinstancelock">Electron app Docs</a></li><li>以及没来得及记录的那些好文章，感谢你们！</li></ol>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;前段时间，我用&lt;a href=&quot;https://github.com/SimulatedGREG/electron-vue&quot;&gt;electron-vue&lt;/a&gt;开发了一款跨平台（目前支持主流三大桌面操作系统）的免费开源的图床上传应用——&lt;a href=&quot;https://github.com/Molunerfinn/PicGo&quot;&gt;PicGo&lt;/a&gt;，在开发过程中踩了不少的坑，不仅来自应用的业务逻辑本身，也来自electron本身。在开发这个应用过程中，我学了不少的东西。因为我也是从0开始学习electron，所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历，用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。&lt;/p&gt;
&lt;p&gt;预计将会从几篇&lt;a href=&quot;https://molunerfinn.com/tags/Electron-vue/&quot;&gt;系列文章&lt;/a&gt;或方面来展开：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-1/&quot;&gt;electron-vue入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-2/&quot;&gt;Main进程和Renderer进程的简单开发&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-3/&quot;&gt;引入基于Lodash的JSON database——lowdb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-4/&quot;&gt;跨平台的一些兼容措施&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-5/&quot;&gt;通过CI发布以及更新的方式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-6/&quot;&gt;开发插件系统——CLI部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-7/&quot;&gt;开发插件系统——GUI部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-8/&quot;&gt;命令行调用与系统级别右键菜单的实现&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;想到再写…&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;说明&quot;&gt;&lt;a href=&quot;#说明&quot; class=&quot;headerlink&quot; title=&quot;说明&quot;&gt;&lt;/a&gt;说明&lt;/h2&gt;&lt;p&gt;&lt;code&gt;PicGo&lt;/code&gt;是采用&lt;code&gt;electron-vue&lt;/code&gt;开发的，所以如果你会&lt;code&gt;vue&lt;/code&gt;，那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如&lt;code&gt;react&lt;/code&gt;、&lt;code&gt;angular&lt;/code&gt;，那么纯按照本教程虽然在render端（可以理解为页面）的构建可能学习到的东西不多，不过在main端(&lt;code&gt;Electron&lt;/code&gt;的主进程）应该还是能学习到相应的知识的。&lt;/p&gt;
&lt;p&gt;如果之前的文章没阅读的朋友可以先从&lt;a href=&quot;https://molunerfinn.com/tags/Electron-vue/&quot;&gt;之前的文章&lt;/a&gt;跟着看。本文主要是基于PicGo v2.1.0版本更新的重要内容做的讲述。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="Vue" scheme="https://molunerfinn.com/tags/Vue/"/>
    
    <category term="Electron-vue" scheme="https://molunerfinn.com/tags/Electron-vue/"/>
    
  </entry>
  
  <entry>
    <title>Electron-vue开发实战6——开发插件系统之GUI部分</title>
    <link href="https://molunerfinn.com/electron-vue-7/"/>
    <id>https://molunerfinn.com/electron-vue-7/</id>
    <published>2019-03-17T11:30:00.000Z</published>
    <updated>2026-03-08T01:14:37.983Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>前段时间，我用<a href="https://github.com/SimulatedGREG/electron-vue">electron-vue</a>开发了一款跨平台（目前支持主流三大桌面操作系统）的免费开源的图床上传应用——<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>，在开发过程中踩了不少的坑，不仅来自应用的业务逻辑本身，也来自electron本身。在开发这个应用过程中，我学了不少的东西。因为我也是从0开始学习electron，所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历，用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。</p><p>预计将会从几篇<a href="https://molunerfinn.com/tags/Electron-vue/">系列文章</a>或方面来展开：</p><ol><li><a href="https://molunerfinn.com/electron-vue-1/">electron-vue入门</a></li><li><a href="https://molunerfinn.com/electron-vue-2/">Main进程和Renderer进程的简单开发</a></li><li><a href="https://molunerfinn.com/electron-vue-3/">引入基于Lodash的JSON database——lowdb</a></li><li><a href="https://molunerfinn.com/electron-vue-4/">跨平台的一些兼容措施</a></li><li><a href="https://molunerfinn.com/electron-vue-5/">通过CI发布以及更新的方式</a></li><li><a href="https://molunerfinn.com/electron-vue-6/">开发插件系统——CLI部分</a></li><li><a href="https://molunerfinn.com/electron-vue-7/">开发插件系统——GUI部分</a></li><li>想到再写…</li></ol><h2 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h2><p><code>PicGo</code>是采用<code>electron-vue</code>开发的，所以如果你会<code>vue</code>，那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如<code>react</code>、<code>angular</code>，那么纯按照本教程虽然在render端（可以理解为页面）的构建可能学习到的东西不多，不过在main端(<code>Electron</code>的主进程）应该还是能学习到相应的知识的。</p><p>如果之前的文章没阅读的朋友可以先从<a href="https://molunerfinn.com/tags/Electron-vue/">之前的文章</a>跟着看。并且如果没有看过前一篇CLI插件系统构建的朋友，需要先行阅读，本文涉及到的部分内容来自上一篇文章。</p><span id="more"></span><h2 id="运行时的require"><a href="#运行时的require" class="headerlink" title="运行时的require"></a>运行时的require</h2><p>我们之前构建的插件系统是基于<code>Node.js</code>端的。对于<code>Electron</code>而言，main进程可以认为拥有<code>Node.js</code>环境，所以我们首先要在main进程里将其引入。而对于PicGo而言，由于上传流程已经完全抽离到<code>PicGo-Core</code>这个库里了，所以原本存在于Electron端的上传部分就可以精简整合成调用<code>PicGo-Core</code>的api来实现上传部分的逻辑了。</p><p>而在引入<code>PicGo-Core</code>的时候会遇到一个问题。在<code>Electron</code>端，由于我使用的脚手架是<code>Electron-vue</code>，它会将<code>main</code>进程和<code>renderer</code>进程都通过<code>Webapck</code>进行打包。由于<code>PicGo-Core</code>用于加载插件的部分使用的是<code>require</code>，在Node.js端很正常没问题。但是Webpack并不知道这些<code>require</code>是在运行时才需要调用的，它会认为这是构建时的「常规」<code>require</code>，也就会在打包的时候把你<code>require</code>的插件也打包进来。这样明显是不合理的，我们是运行时才<code>require</code>插件的，所以需要做一些手段来「绕开」<code>Webpack</code>的打包机制：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// eslint-disable-next-line</span></span><br><span class="line"><span class="keyword">const</span> requireFunc = <span class="keyword">typeof</span> __webpack_require__ === <span class="string">&#x27;function&#x27;</span> ? __non_webpack_require__ : <span class="built_in">require</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">PicGo</span> = <span class="title function_">requireFunc</span>(<span class="string">&#x27;picgo&#x27;</span>)</span><br></pre></td></tr></table></figure><blockquote><p>关于<code>__non_webpack_require__</code>的说明，可以查看<a href="https://webpack.docschina.org/api/module-variables/#__non_webpack_require__-webpack-%E7%89%B9%E6%9C%89%E5%8F%98%E9%87%8F-">文档</a>。</p></blockquote><p>打包之后会变成如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> requireFunc = <span class="literal">true</span> ? <span class="built_in">require</span> : <span class="built_in">require</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">PicGo</span> = <span class="title function_">requireFunc</span>(<span class="string">&#x27;picgo&#x27;</span>)</span><br></pre></td></tr></table></figure><p>这样就可以避免PicGo-Core内部的<code>require</code>被<code>Webpack</code>也打包进去了。</p><h2 id="「前后端」分离"><a href="#「前后端」分离" class="headerlink" title="「前后端」分离"></a>「前后端」分离</h2><p><code>Electron</code>的<code>main</code>进程和<code>renderer</code>进程实际上你可以把它们看成我们平时Web开发的后端和前端。二者交流的工具也不再是<code>Ajax</code>，而是<code>ipcMain</code>和<code>ipcRenderer</code>。当然<code>renderer</code>本身能做的事情也不少，只不过这样说一下可能会好理解一点。相应的，我们的插件系统原本实现在<code>Node.js</code>端，是一个没有界面的工具，想要让它拥有「脸面」，其实也不过是在<code>renderer</code>进程里调用来自<code>main</code>进程里的插件系统暴露出来的api而已。这里我们举几个例子来说明。</p><h3 id="简化原有流程"><a href="#简化原有流程" class="headerlink" title="简化原有流程"></a>简化原有流程</h3><p>在以前PicGo上传图片需要经过很多步骤：</p><ol><li>通过<a href="https://github.com/Molunerfinn/PicGo/blob/v1.6.2/src/main/utils/uploader.js">uploader</a>来接收图片，并通过<a href="https://github.com/Molunerfinn/PicGo/blob/v1.6.2/src/datastore/pic-bed-handler.js">pic-bed-handler</a>来指定上传的图床。</li><li>通过<a href="https://github.com/Molunerfinn/PicGo/blob/v1.6.2/src/main/utils/img2base64.js">img2base64</a>来把图片统一转成<code>Base64</code>编码。</li><li>通过指定的<code>imgUploader</code>（比如<code>qiniu</code>比如<code>weibo</code>等）来上传到指定的图床。</li></ol><p>而如今整个底层上传流程系统已经被抽离出来，因此我们可以直接使用PicGo-Core实现的api来上传图片，只需定义一个<a href="https://github.com/Molunerfinn/PicGo/blob/dev/src/main/utils/uploader.js">Uploader</a>类即可（下面的代码是简化版本）：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  app,</span><br><span class="line">  <span class="title class_">Notification</span>,</span><br><span class="line">  <span class="title class_">BrowserWindow</span>,</span><br><span class="line">  ipcMain</span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// eslint-disable-next-line</span></span><br><span class="line"><span class="keyword">const</span> requireFunc = <span class="keyword">typeof</span> __webpack_require__ === <span class="string">&#x27;function&#x27;</span> ? __non_webpack_require__ : <span class="built_in">require</span></span><br><span class="line"><span class="keyword">const</span> <span class="title class_">PicGo</span> = <span class="title function_">requireFunc</span>(<span class="string">&#x27;picgo&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">STORE_PATH</span> = app.<span class="title function_">getPath</span>(<span class="string">&#x27;userData&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">CONFIG_PATH</span> = path.<span class="title function_">join</span>(<span class="variable constant_">STORE_PATH</span>, <span class="string">&#x27;/data.json&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Uploader</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params">img, webContents, picgo = <span class="literal">undefined</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">img</span> = img</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">webContents</span> = webContents</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">picgo</span> = picgo</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">upload</span> () &#123;</span><br><span class="line">    <span class="keyword">const</span> win = <span class="title class_">BrowserWindow</span>.<span class="title function_">fromWebContents</span>(<span class="variable language_">this</span>.<span class="property">webContents</span>) <span class="comment">// 获取上传的窗口</span></span><br><span class="line">    <span class="keyword">const</span> picgo = <span class="variable language_">this</span>.<span class="property">picgo</span> || <span class="keyword">new</span> <span class="title class_">PicGo</span>(<span class="variable constant_">CONFIG_PATH</span>) <span class="comment">// 获取上传的picgo实例</span></span><br><span class="line">    picgo.<span class="property">config</span>.<span class="property">debug</span> = <span class="literal">true</span> <span class="comment">// 方便调试</span></span><br><span class="line">    <span class="comment">// for picgo-core</span></span><br><span class="line">    picgo.<span class="property">config</span>.<span class="property">PICGO_ENV</span> = <span class="string">&#x27;GUI&#x27;</span></span><br><span class="line">    <span class="keyword">let</span> input = <span class="variable language_">this</span>.<span class="property">img</span> <span class="comment">// 传入的this.img是一个数组</span></span><br><span class="line"></span><br><span class="line">    picgo.<span class="title function_">upload</span>(input) <span class="comment">// 上传图片，只用了一句话</span></span><br><span class="line"></span><br><span class="line">    picgo.<span class="title function_">on</span>(<span class="string">&#x27;notification&#x27;</span>, <span class="function"><span class="params">message</span> =&gt;</span> &#123; <span class="comment">// 上传成功或者失败提示信息</span></span><br><span class="line">      <span class="keyword">const</span> notification = <span class="keyword">new</span> <span class="title class_">Notification</span>(message)</span><br><span class="line">      notification.<span class="title function_">show</span>()</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    picgo.<span class="title function_">on</span>(<span class="string">&#x27;uploadProgress&#x27;</span>, <span class="function"><span class="params">progress</span> =&gt;</span> &#123; <span class="comment">// 上传进度</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">webContents</span>.<span class="title function_">send</span>(<span class="string">&#x27;uploadProgress&#x27;</span>, progress)</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">resolve</span>) =&gt;</span> &#123; <span class="comment">// 返回一个Promise方便调用</span></span><br><span class="line">      picgo.<span class="title function_">on</span>(<span class="string">&#x27;finished&#x27;</span>, <span class="function"><span class="params">ctx</span> =&gt;</span> &#123; <span class="comment">// 上传完成的事件</span></span><br><span class="line">        <span class="keyword">if</span> (ctx.<span class="property">output</span>.<span class="title function_">every</span>(<span class="function"><span class="params">item</span> =&gt;</span> item.<span class="property">imgUrl</span>)) &#123;</span><br><span class="line">          <span class="title function_">resolve</span>(ctx.<span class="property">output</span>)</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">          <span class="title function_">resolve</span>(<span class="literal">false</span>)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">      picgo.<span class="title function_">on</span>(<span class="string">&#x27;failed&#x27;</span>, <span class="function"><span class="params">ctx</span> =&gt;</span> &#123; <span class="comment">// 上传失败的事件</span></span><br><span class="line">        <span class="keyword">const</span> notification = <span class="keyword">new</span> <span class="title class_">Notification</span>(&#123;</span><br><span class="line">          <span class="attr">title</span>: <span class="string">&#x27;上传失败&#x27;</span>,</span><br><span class="line">          <span class="attr">body</span>: <span class="string">&#x27;请检查配置和上传的文件是否符合要求&#x27;</span></span><br><span class="line">        &#125;)</span><br><span class="line">        notification.<span class="title function_">show</span>()</span><br><span class="line">        <span class="title function_">resolve</span>(<span class="literal">false</span>)</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">Uploader</span></span><br></pre></td></tr></table></figure><p>可以看出，由于在设计CLI插件系统的时候我们有考虑到设计好插件的生命周期，所以很多功能都可以通过生命周期的钩子、以及相应的一些事件来实现。比如图片上传完成就是通过<code>picgo.on(&#39;finished&#39;， callback)</code>监听<code>finished</code>事件来实现的，而上传的进度与进度条显示就是通过<code>picgo.on(&#39;progress&#39;)</code>来实现的。它们的效果如下：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo-2.0.gif" alt="upload-process"></p><p>而且我们还可以通过接入<code>picgo</code>的生命周期，实现一些以前实现起来比较麻烦的功能，比如上传前重命名：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">picgo.<span class="property">helper</span>.<span class="property">beforeUploadPlugins</span>.<span class="title function_">register</span>(<span class="string">&#x27;renameFn&#x27;</span>, &#123;</span><br><span class="line">  <span class="attr">handle</span>: <span class="keyword">async</span> ctx =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> rename = picgo.<span class="title function_">getConfig</span>(<span class="string">&#x27;settings.rename&#x27;</span>)</span><br><span class="line">    <span class="keyword">const</span> autoRename = picgo.<span class="title function_">getConfig</span>(<span class="string">&#x27;settings.autoRename&#x27;</span>)</span><br><span class="line">    <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>(ctx.<span class="property">output</span>.<span class="title function_">map</span>(<span class="title function_">async</span> (item, index) =&gt; &#123;</span><br><span class="line">      <span class="keyword">let</span> name</span><br><span class="line">      <span class="keyword">let</span> fileName</span><br><span class="line">      <span class="keyword">if</span> (autoRename) &#123;</span><br><span class="line">        fileName = <span class="title function_">dayjs</span>().<span class="title function_">add</span>(index, <span class="string">&#x27;second&#x27;</span>).<span class="title function_">format</span>(<span class="string">&#x27;YYYYMMDDHHmmss&#x27;</span>) + item.<span class="property">extname</span></span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fileName = item.<span class="property">fileName</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> (rename) &#123; <span class="comment">// 如果要重命名</span></span><br><span class="line">        <span class="keyword">const</span> <span class="variable language_">window</span> = <span class="title function_">createRenameWindow</span>(win) <span class="comment">// 创建重命名窗口</span></span><br><span class="line">        <span class="keyword">await</span> <span class="title function_">waitForShow</span>(<span class="variable language_">window</span>.<span class="property">webContents</span>) <span class="comment">// 等待窗口打开</span></span><br><span class="line">        <span class="variable language_">window</span>.<span class="property">webContents</span>.<span class="title function_">send</span>(<span class="string">&#x27;rename&#x27;</span>, fileName, <span class="variable language_">window</span>.<span class="property">webContents</span>.<span class="property">id</span>) <span class="comment">// 给窗口发送相应信息</span></span><br><span class="line">        name = <span class="keyword">await</span> <span class="title function_">waitForRename</span>(<span class="variable language_">window</span>, <span class="variable language_">window</span>.<span class="property">webContents</span>.<span class="property">id</span>) <span class="comment">// 获取重新命名后的文件名</span></span><br><span class="line">      &#125;</span><br><span class="line">      item.<span class="property">fileName</span> = name || fileName</span><br><span class="line">    &#125;))</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>通过注册一个<code>beforeUploadPlugin</code>，在上传前判断是否需要「上传前重命名」，如果是，就创建窗口并等待用户输入重命名的结果，然后将重命名的<code>name</code>赋值给<code>item.fileName</code>供后续的流程使用。</p><p>我们还可以在<code>beforeTransform</code>阶段通知用户当前正在准备上传了：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">picgo.<span class="title function_">on</span>(<span class="string">&#x27;beforeTransform&#x27;</span>, <span class="function"><span class="params">ctx</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (ctx.<span class="title function_">getConfig</span>(<span class="string">&#x27;settings.uploadNotification&#x27;</span>)) &#123;</span><br><span class="line">    <span class="keyword">const</span> notification = <span class="keyword">new</span> <span class="title class_">Notification</span>(&#123;</span><br><span class="line">      <span class="attr">title</span>: <span class="string">&#x27;上传进度&#x27;</span>,</span><br><span class="line">      <span class="attr">body</span>: <span class="string">&#x27;正在上传&#x27;</span></span><br><span class="line">    &#125;)</span><br><span class="line">    notification.<span class="title function_">show</span>()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>等等。所以实际上我们只需要在<code>main</code>进程完成相应的api，那么<code>renderer</code>进程做的事只不过是通过<code>ipcRenderer</code>来通过<code>main</code>进程调用这些api而已了。比如：</p><ul><li>当用户拖动图片到上传区域，通过<code>ipcRenderer</code>通知<code>main</code>进程：</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">this</span>.<span class="property">$electron</span>.<span class="property">ipcRenderer</span>.<span class="title function_">send</span>(<span class="string">&#x27;uploadChoosedFiles&#x27;</span>, sendFiles)</span><br></pre></td></tr></table></figure><ul><li><code>main</code>进程监听事件并调用<code>Uploader</code>的<code>upload</code>方法：</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ipcMain.<span class="title function_">on</span>(<span class="string">&#x27;uploadChoosedFiles&#x27;</span>, <span class="title function_">async</span> (evt, files) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> input = files.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> item.<span class="property">path</span>)</span><br><span class="line">  <span class="keyword">const</span> imgs = <span class="keyword">await</span> <span class="keyword">new</span> <span class="title class_">Uploader</span>(input, evt.<span class="property">sender</span>).<span class="title function_">upload</span>() <span class="comment">// 由于upload返回的是Promise</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>就完成了一次「前后端」交互。其他方式上传（比如剪贴板上传）也同理，就不再赘述。</p><h2 id="实现插件管理界面"><a href="#实现插件管理界面" class="headerlink" title="实现插件管理界面"></a>实现插件管理界面</h2><p>光有插件系统没有插件也不行，所以我们需要实现一个插件管理的界面。而插件管理的功能（比如安装、卸载、更新）已经在CLI版本里实现了，所以这些功能我们只需要通过向上一节里说的调用<code>ipcRenderer</code>和<code>ipcMain</code>来调用相应api即可。</p><h3 id="第三方插件搜索"><a href="#第三方插件搜索" class="headerlink" title="第三方插件搜索"></a>第三方插件搜索</h3><p>在GUI界面我们需要一个很重要的功能就是「插件搜索」的功能。由于PicGo的插件统一是发布到<a href="https://www.npmjs.com/">npm</a>的，所以其实我们可以通过npm的api来打到搜索插件的目的：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">getSearchResult</span> (val) &#123;</span><br><span class="line">  <span class="comment">// this.$http.get(`https://api.npms.io/v2/search?q=$&#123;val&#125;`)</span></span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">$http</span>.<span class="title function_">get</span>(<span class="string">`https://registry.npmjs.com/-/v1/search?text=<span class="subst">$&#123;val&#125;</span>`</span>) <span class="comment">// 调用npm的搜索api</span></span><br><span class="line">    .<span class="title function_">then</span>(<span class="function"><span class="params">res</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">pluginList</span> = res.<span class="property">data</span>.<span class="property">objects</span>.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">handleSearchResult</span>(item) <span class="comment">// 返回格式化的结果</span></span><br><span class="line">      &#125;)</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">loading</span> = <span class="literal">false</span></span><br><span class="line">    &#125;)</span><br><span class="line">    .<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(err)</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">loading</span> = <span class="literal">false</span></span><br><span class="line">    &#125;)</span><br><span class="line">&#125;,</span><br><span class="line"><span class="title function_">handleSearchResult</span> (item) &#123;</span><br><span class="line">  <span class="keyword">const</span> name = item.<span class="property">package</span>.<span class="property">name</span>.<span class="title function_">replace</span>(<span class="regexp">/picgo-plugin-/</span>, <span class="string">&#x27;&#x27;</span>)</span><br><span class="line">  <span class="keyword">let</span> gui = <span class="literal">false</span></span><br><span class="line">  <span class="keyword">if</span> (item.<span class="property">package</span>.<span class="property">keywords</span> &amp;&amp; item.<span class="property">package</span>.<span class="property">keywords</span>.<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (item.<span class="property">package</span>.<span class="property">keywords</span>.<span class="title function_">includes</span>(<span class="string">&#x27;picgo-gui-plugin&#x27;</span>)) &#123;</span><br><span class="line">      gui = <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    <span class="attr">name</span>: name,</span><br><span class="line">    <span class="attr">author</span>: item.<span class="property">package</span>.<span class="property">author</span>.<span class="property">name</span>,</span><br><span class="line">    <span class="attr">description</span>: item.<span class="property">package</span>.<span class="property">description</span>,</span><br><span class="line">    <span class="attr">logo</span>: <span class="string">`https://cdn.jsdelivr.net/npm/<span class="subst">$&#123;item.package.name&#125;</span>/logo.png`</span>,</span><br><span class="line">    <span class="attr">config</span>: &#123;&#125;,</span><br><span class="line">    <span class="attr">homepage</span>: item.<span class="property">package</span>.<span class="property">links</span> ? item.<span class="property">package</span>.<span class="property">links</span>.<span class="property">homepage</span> : <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    <span class="attr">hasInstall</span>: <span class="variable language_">this</span>.<span class="property">pluginNameList</span>.<span class="title function_">some</span>(<span class="function"><span class="params">plugin</span> =&gt;</span> plugin === item.<span class="property">package</span>.<span class="property">name</span>.<span class="title function_">replace</span>(<span class="regexp">/picgo-plugin-/</span>, <span class="string">&#x27;&#x27;</span>)),</span><br><span class="line">    <span class="attr">version</span>: item.<span class="property">package</span>.<span class="property">version</span>,</span><br><span class="line">    gui,</span><br><span class="line">    <span class="attr">ing</span>: <span class="literal">false</span> <span class="comment">// installing or uninstalling</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过搜索然后把结果显示到界面上就是如下：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/test/search-plugin.gif"></p><p>没有安装的插件就会在右下角显示「安装」两个字样。</p><h3 id="本地插件列表"><a href="#本地插件列表" class="headerlink" title="本地插件列表"></a>本地插件列表</h3><p>当我们安装好插件之后，需要从本地获取插件列表。这个部分需要做一些处理。由于插件是安装在Node.js端的，所以我们需要通过<code>ipcRenderer</code>去向<code>main</code>进程发起获取插件列表的「请求」：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">this</span>.<span class="property">$electron</span>.<span class="property">ipcRenderer</span>.<span class="title function_">send</span>(<span class="string">&#x27;getPluginList&#x27;</span>) <span class="comment">// 发起获取插件的「请求」</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">$electron</span>.<span class="property">ipcRenderer</span>.<span class="title function_">on</span>(<span class="string">&#x27;pluginList&#x27;</span>, <span class="function">(<span class="params">evt, list</span>) =&gt;</span> &#123; <span class="comment">// 获取插件列表</span></span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">pluginList</span> = list</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">pluginNameList</span> = list.<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> item.<span class="property">name</span>)</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">loading</span> = <span class="literal">false</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>而获取插件列表以及相应信息我们需要在<code>main</code>端进行，并发送回去：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">ipcMain.<span class="title function_">on</span>(<span class="string">&#x27;getPluginList&#x27;</span>, <span class="function"><span class="params">event</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> picgo = <span class="keyword">new</span> <span class="title class_">PicGo</span>(<span class="variable constant_">CONFIG_PATH</span>)</span><br><span class="line">  <span class="keyword">const</span> pluginList = picgo.<span class="property">pluginLoader</span>.<span class="title function_">getList</span>()</span><br><span class="line">  <span class="keyword">const</span> list = []</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i <span class="keyword">in</span> pluginList) &#123;</span><br><span class="line">   <span class="comment">// 处理插件相关的信息</span></span><br><span class="line">  &#125;</span><br><span class="line">  event.<span class="property">sender</span>.<span class="title function_">send</span>(<span class="string">&#x27;pluginList&#x27;</span>, list) <span class="comment">// 将插件信息列表发送回去</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>注意到由于<code>ipcMain</code>和<code>ipcRenderer</code>里收发数据的时候会自动经过<code>JSON.stringify</code>和<code>JSON.parse</code>，所以对于原来的一些属性是<code>function</code>之类无法被序列化的属性，我们要做一些处理，比如先执行它们得到结果：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">handleConfigWithFunction</span> = config =&gt; &#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i <span class="keyword">in</span> config) &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">typeof</span> config[i].<span class="property">default</span> === <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">      config[i].<span class="property">default</span> = config[i].<span class="title function_">default</span>()</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">typeof</span> config[i].<span class="property">choices</span> === <span class="string">&#x27;function&#x27;</span>) &#123;</span><br><span class="line">      config[i].<span class="property">choices</span> = config[i].<span class="title function_">choices</span>()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> config</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样，在<code>renderer</code>进程里才能拿到完整的数据。</p><h3 id="插件配置相关"><a href="#插件配置相关" class="headerlink" title="插件配置相关"></a>插件配置相关</h3><p>当然光有安装、查看还不够，还需要让插件管理界面拥有其他功能，比如「卸载」、「更新」或者是配置功能，所以在每个安装成功后的插件卡片的右下角有个配置按钮可以弹出相应的菜单：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/20190113160001.png"></p><p>菜单这个部分就是用<code>Electron</code>的<code>Menu</code>模块去实现了（我在之前的文章里已经有涉及，不再赘述），并没有特别复杂的地方。而这里比较关键的地方，就是当我点击<code>配置plugin-xxx</code>的时候，会弹出一个配置的对话框：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/test/20190317161148.png"></p><p>这个配置对话框内的配置内容来自前文《开发CLI插件系统》里我们要求开发者定义好的<code>config</code>方法返回的配置项。由于插件开发者定义的<code>config</code>内容是<a href="https://github.com/SBoudrias/Inquirer.js/">Inquirer.js</a>所要求的格式，便于在CLI环境下使用。但是它和我们平时使用的<code>form</code>表单的一些格式可能有些出入，所以需要「转义」一下，通过原始的<code>config</code>动态生成表单项：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;config-form&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">el-form</span></span></span><br><span class="line"><span class="tag">    <span class="attr">label-position</span>=<span class="string">&quot;right&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">label-width</span>=<span class="string">&quot;120px&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">:model</span>=<span class="string">&quot;ruleForm&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">ref</span>=<span class="string">&quot;form&quot;</span></span></span><br><span class="line"><span class="tag">    <span class="attr">size</span>=<span class="string">&quot;mini&quot;</span></span></span><br><span class="line"><span class="tag">  &gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">el-form-item</span></span></span><br><span class="line"><span class="tag">      <span class="attr">v-for</span>=<span class="string">&quot;(item, index) in configList&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">:label</span>=<span class="string">&quot;item.name&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">:required</span>=<span class="string">&quot;item.required&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">:prop</span>=<span class="string">&quot;item.name&quot;</span></span></span><br><span class="line"><span class="tag">      <span class="attr">:key</span>=<span class="string">&quot;item.name + index&quot;</span></span></span><br><span class="line"><span class="tag">    &gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">el-input</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-if</span>=<span class="string">&quot;item.type === &#x27;input&#x27; || item.type === &#x27;password&#x27;&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:type</span>=<span class="string">&quot;item.type === &#x27;password&#x27; ? &#x27;password&#x27; : &#x27;input&#x27;&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-model</span>=<span class="string">&quot;ruleForm[item.name]&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:placeholder</span>=<span class="string">&quot;item.message || item.name&quot;</span></span></span><br><span class="line"><span class="tag">      &gt;</span><span class="tag">&lt;/<span class="name">el-input</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">el-select</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-else-if</span>=<span class="string">&quot;item.type === &#x27;list&#x27;&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-model</span>=<span class="string">&quot;ruleForm[item.name]&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:placeholder</span>=<span class="string">&quot;item.message || item.name&quot;</span></span></span><br><span class="line"><span class="tag">      &gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">el-option</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-for</span>=<span class="string">&quot;(choice, idx) in item.choices&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:label</span>=<span class="string">&quot;choice.name || choice.value || choice&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:key</span>=<span class="string">&quot;choice.name || choice.value || choice&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:value</span>=<span class="string">&quot;choice.value || choice&quot;</span></span></span><br><span class="line"><span class="tag">        &gt;</span><span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">el-select</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">el-select</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-else-if</span>=<span class="string">&quot;item.type === &#x27;checkbox&#x27;&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-model</span>=<span class="string">&quot;ruleForm[item.name]&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">:placeholder</span>=<span class="string">&quot;item.message || item.name&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">multiple</span></span></span><br><span class="line"><span class="tag">        <span class="attr">collapse-tags</span></span></span><br><span class="line"><span class="tag">      &gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">el-option</span></span></span><br><span class="line"><span class="tag">          <span class="attr">v-for</span>=<span class="string">&quot;(choice, idx) in item.choices&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:label</span>=<span class="string">&quot;choice.name || choice.value || choice&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:key</span>=<span class="string">&quot;choice.value || choice&quot;</span></span></span><br><span class="line"><span class="tag">          <span class="attr">:value</span>=<span class="string">&quot;choice.value || choice&quot;</span></span></span><br><span class="line"><span class="tag">        &gt;</span><span class="tag">&lt;/<span class="name">el-option</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">el-select</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">el-switch</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-else-if</span>=<span class="string">&quot;item.type === &#x27;confirm&#x27;&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">v-model</span>=<span class="string">&quot;ruleForm[item.name]&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">active-text</span>=<span class="string">&quot;yes&quot;</span></span></span><br><span class="line"><span class="tag">        <span class="attr">inactive-text</span>=<span class="string">&quot;no&quot;</span></span></span><br><span class="line"><span class="tag">      &gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">el-switch</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">el-form-item</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">slot</span>&gt;</span><span class="tag">&lt;/<span class="name">slot</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">el-form</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre></td></tr></table></figure><p>上面是针对<code>config</code>里不同的<code>type</code>转换成不同的Web表单控件的代码。下面是初始化的时候处理<code>config</code>的一些工作：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">watch</span>: &#123;</span><br><span class="line">  <span class="attr">config</span>: &#123;</span><br><span class="line">    <span class="attr">deep</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="title function_">handler</span> (val) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">ruleForm</span> = <span class="title class_">Object</span>.<span class="title function_">assign</span>(&#123;&#125;, &#123;&#125;)</span><br><span class="line">      <span class="keyword">const</span> config = <span class="variable language_">this</span>.<span class="property">$db</span>.<span class="title function_">read</span>().<span class="title function_">get</span>(<span class="string">`picBed.<span class="subst">$&#123;<span class="variable language_">this</span>.id&#125;</span>`</span>).<span class="title function_">value</span>()</span><br><span class="line">      <span class="keyword">if</span> (val.<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">configList</span> = <span class="title function_">cloneDeep</span>(val).<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">let</span> defaultValue = item.<span class="property">default</span> !== <span class="literal">undefined</span></span><br><span class="line">            ? item.<span class="property">default</span> : item.<span class="property">type</span> === <span class="string">&#x27;checkbox&#x27;</span></span><br><span class="line">              ? [] : <span class="literal">null</span> <span class="comment">// 处理默认值</span></span><br><span class="line">          <span class="keyword">if</span> (item.<span class="property">type</span> === <span class="string">&#x27;checkbox&#x27;</span>) &#123; <span class="comment">// 处理checkbox选中值</span></span><br><span class="line">            <span class="keyword">const</span> defaults = item.<span class="property">choices</span>.<span class="title function_">filter</span>(<span class="function"><span class="params">i</span> =&gt;</span> &#123;</span><br><span class="line">              <span class="keyword">return</span> i.<span class="property">checked</span></span><br><span class="line">            &#125;).<span class="title function_">map</span>(<span class="function"><span class="params">i</span> =&gt;</span> i.<span class="property">value</span>)</span><br><span class="line">            defaultValue = <span class="title function_">union</span>(defaultValue, defaults)</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">if</span> (config &amp;&amp; config[item.<span class="property">name</span>] !== <span class="literal">undefined</span>) &#123; <span class="comment">// 处理默认值</span></span><br><span class="line">            defaultValue = config[item.<span class="property">name</span>]</span><br><span class="line">          &#125;</span><br><span class="line">          <span class="variable language_">this</span>.$set(<span class="variable language_">this</span>.<span class="property">ruleForm</span>, item.<span class="property">name</span>, defaultValue)</span><br><span class="line">          <span class="keyword">return</span> item</span><br><span class="line">        &#125;)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">immediate</span>: <span class="literal">true</span> <span class="comment">// 立即执行</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>经过上述处理，就可以将原本用于CLI的配置项，近乎「无缝」地迁移到Web（GUI）端了。其实这也是vue-cli3的ui版本实现的思路，大同小异。</p><h2 id="实现特有的guiApi"><a href="#实现特有的guiApi" class="headerlink" title="实现特有的guiApi"></a>实现特有的guiApi</h2><p>不过既然是GUI软件了，只通过调用CLI实现的功能明显是不够丰富的。因此我也为<code>PicGo</code>实现了一些特有的<code>guiApi</code>提供给插件的开发者，让插件的可玩性更强。当然不同的软件给予插件的GUI能力是不一样的，因此不能一概而论。我仅以<code>PicGo</code>为例，讲述我对于<code>PicGo</code>所提供的<code>guiApi</code>的理解和看法。下面我就来说说这部分是如何实现的。</p><p>由于PicGo本质是一个上传系统，所以用户在上传图片的时候，很多插件底层的东西和功能实际上是看不到的。如果要让插件的功能更加丰富，就需要让插件有自己的「可视化」入口让用户去使用。因此对于PicGo而言，我给予插件的「可视化」入口就放在插件配置的界面里——除了给插件默认的配置菜单之外，还给予插件自己的菜单项供用户使用：</p><p><img src="https://i.loli.net/2019/01/12/5c39a2f60a32a.png"></p><p>这个实现也很容易，只要插件在自己的<code>index.js</code>文件里暴露一个<code>guiMenu</code>的选项，就可以生成自己的菜单：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">guiMenu</span> = ctx =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">label</span>: <span class="string">&#x27;打开InputBox&#x27;</span>,</span><br><span class="line">      <span class="keyword">async</span> <span class="title function_">handle</span> (ctx, guiApi) &#123;</span><br><span class="line">        <span class="comment">// do something...</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">label</span>: <span class="string">&#x27;打开FileExplorer&#x27;</span>,</span><br><span class="line">      <span class="keyword">async</span> <span class="title function_">handle</span> (ctx, guiApi) &#123;</span><br><span class="line">        <span class="comment">// do something...</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到菜单项可以自定义，点击之后的操作也可以自定义，因此给予了插件很大的自由度。可以注意到，在点击菜单的时候会触发<code>handle</code>函数，这个函数里会传入一个<code>guiApi</code>，这个就是本节的重点了。就目前而言，<code>guiApi</code>实现了如下功能：</p><ol><li><code>showInputBox([option])</code> 调用之后打开一个输入弹窗，可以用于接受用户输入。</li><li><code>showFileExplorer([option])</code> 调用之后打开一个文件浏览器，可以得到用户选择的文件（夹）路径。</li><li><code>upload([file])</code> 调用之后使用PicGo底层来上传，可以实现自动更新相册图片、上传成功后自动将URL写入剪贴板。</li><li><code>showNotificaiton(option)</code> 调用之后弹出系统通知窗口。</li></ol><p>上面api我们可以通过诸如<code>guiApi.showInputBox()</code>、<code>guiApi.showFileExplorer()</code>等来实现调用。这里面的例子实现思路都差不多，我简单以<code>guiApi.showFileExplorer()</code>来做讲解。</p><p>当我们在<code>renderer</code>界面点击插件实现的某个菜单之后，实际上是通过调用<code>ipcRenderer</code>向<code>main</code>进程传播了一次事件：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (plugin.<span class="property">guiMenu</span>) &#123;</span><br><span class="line">  menu.<span class="title function_">push</span>(&#123;</span><br><span class="line">    <span class="attr">type</span>: <span class="string">&#x27;separator&#x27;</span></span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i <span class="keyword">of</span> plugin.<span class="property">guiMenu</span>) &#123;</span><br><span class="line">    menu.<span class="title function_">push</span>(&#123;</span><br><span class="line">      <span class="attr">label</span>: i.<span class="property">label</span>,</span><br><span class="line">      <span class="title function_">click</span> () &#123; <span class="comment">// 当点击的时候，发送当前的插件名和当前菜单项的名字</span></span><br><span class="line">        _this.<span class="property">$electron</span>.<span class="property">ipcRenderer</span>.<span class="title function_">send</span>(<span class="string">&#x27;pluginActions&#x27;</span>, plugin.<span class="property">name</span>, i.<span class="property">label</span>)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>于是在<code>main</code>进程，我们通过监听这个事件，来调用相应的<code>guiApi</code>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">handlePluginActions</span> = (<span class="params">ipcMain, CONFIG_PATH</span>) =&gt; &#123;</span><br><span class="line">  ipcMain.<span class="title function_">on</span>(<span class="string">&#x27;pluginActions&#x27;</span>, <span class="function">(<span class="params">event, name, label</span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> picgo = <span class="keyword">new</span> <span class="title class_">PicGo</span>(<span class="variable constant_">CONFIG_PATH</span>)</span><br><span class="line">    <span class="keyword">const</span> plugin = picgo.<span class="property">pluginLoader</span>.<span class="title function_">getPlugin</span>(<span class="string">`picgo-plugin-<span class="subst">$&#123;name&#125;</span>`</span>)</span><br><span class="line">    <span class="keyword">const</span> guiApi = <span class="keyword">new</span> <span class="title class_">GuiApi</span>(ipcMain, event.<span class="property">sender</span>, picgo) <span class="comment">// 实例化guiApi</span></span><br><span class="line">    <span class="keyword">if</span> (plugin.<span class="property">guiMenu</span> &amp;&amp; plugin.<span class="title function_">guiMenu</span>(picgo).<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> menu = plugin.<span class="title function_">guiMenu</span>(picgo)</span><br><span class="line">      menu.<span class="title function_">forEach</span>(<span class="function"><span class="params">item</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (item.<span class="property">label</span> === label) &#123; <span class="comment">// 找到相应的label，执行插件的`handle`</span></span><br><span class="line">          item.<span class="title function_">handle</span>(picgo, guiApi)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而<code>guiApi</code>的实现类<a href="https://github.com/Molunerfinn/PicGo/blob/dev/src/main/utils/guiApi.js">GuiApi</a>其实特别简单：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123;</span><br><span class="line">  dialog,</span><br><span class="line">  <span class="title class_">BrowserWindow</span>,</span><br><span class="line">  clipboard,</span><br><span class="line">  <span class="title class_">Notification</span></span><br><span class="line">&#125; <span class="keyword">from</span> <span class="string">&#x27;electron&#x27;</span></span><br><span class="line"><span class="keyword">import</span> db <span class="keyword">from</span> <span class="string">&#x27;../../datastore&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">Uploader</span> <span class="keyword">from</span> <span class="string">&#x27;./uploader&#x27;</span></span><br><span class="line"><span class="keyword">import</span> pasteTemplate <span class="keyword">from</span> <span class="string">&#x27;./pasteTemplate&#x27;</span></span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">WEBCONTENTS</span> = <span class="title class_">Symbol</span>(<span class="string">&#x27;WEBCONTENTS&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">IPCMAIN</span> = <span class="title class_">Symbol</span>(<span class="string">&#x27;IPCMAIN&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="variable constant_">PICGO</span> = <span class="title class_">Symbol</span>(<span class="string">&#x27;PICGO&#x27;</span>)</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">GuiApi</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params">ipcMain, webcontents, picgo</span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>[<span class="variable constant_">WEBCONTENTS</span>] = webcontents</span><br><span class="line">    <span class="variable language_">this</span>[<span class="variable constant_">IPCMAIN</span>] = ipcMain</span><br><span class="line">    <span class="variable language_">this</span>[<span class="variable constant_">PICGO</span>] = picgo</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * for plugin show file explorer</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> &#123;<span class="type">object</span>&#125; <span class="variable">options</span></span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  <span class="title function_">showFileExplorer</span> (options) &#123;</span><br><span class="line">    <span class="keyword">if</span> (options === <span class="literal">undefined</span>) &#123;</span><br><span class="line">      options = &#123;&#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">resolve, reject</span>) =&gt;</span> &#123;</span><br><span class="line">      dialog.<span class="title function_">showOpenDialog</span>(<span class="title class_">BrowserWindow</span>.<span class="title function_">fromWebContents</span>(<span class="variable language_">this</span>[<span class="variable constant_">WEBCONTENTS</span>]), options, <span class="function"><span class="params">filename</span> =&gt;</span> &#123;</span><br><span class="line">        <span class="title function_">resolve</span>(filename)</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实际上就是去调用一些<code>Electron</code>的方法，甚至是你自己封装的一些方法，返回值是一个新的<code>Promise</code>对象。这样插件开发者就可以通过<code>async</code>和<code>await</code>来方便获取这些方法的返回值了：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">guiMenu</span> = ctx =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">label</span>: <span class="string">&#x27;打开文件浏览器&#x27;</span>,</span><br><span class="line">      <span class="keyword">async</span> <span class="title function_">handle</span> (ctx, guiApi) &#123;</span><br><span class="line">        <span class="comment">// 通过await获取用户所选的文件路径</span></span><br><span class="line">        <span class="keyword">const</span> files = <span class="keyword">await</span> guiApi.<span class="title function_">showFileExplorer</span>(&#123;</span><br><span class="line">          <span class="attr">properties</span>: [<span class="string">&#x27;openFile&#x27;</span>, <span class="string">&#x27;multiSelections&#x27;</span>]</span><br><span class="line">        &#125;)</span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(files)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>至此，一个GUI插件系统的关键部分我们就基本实现了。除了整合了CLI插件系统的几乎所有功能之外，我们还提供了独特的<code>guiApi</code>给插件开发者无限的想象空间，也给用户带来更好的插件体验。可以说插件系统的实现，让<code>PicGo</code>有了更多的可玩性。关于<code>PicGo</code>目前的插件，欢迎查看<a href="https://github.com/PicGo/Awesome-PicGo">Awesome-PicGo</a>的列表。以下罗列一些我觉得比较有用或者有意思的插件：</p><ol><li><a href="https://github.com/Spades-S/vs-picgo">vs-picgo</a> 在VSCode里使用PicGo（无需安装GUI！）</li><li><a href="https://github.com/PicGo/picgo-plugin-pic-migrater">picgo-plugin-pic-migrater</a> 可以迁移你的Markdown里的图片地址到你默认指定的图床，哪怕是本地图片也可以迁移到云端！</li><li><a href="https://github.com/zWingz/picgo-plugin-github-plus">picgo-plugin-github-plus</a> 增强版GitHub图床，支持了同步图床以及同步删除操作（删除本地图片也会把GitHub上的图片删除）</li><li><a href="https://github.com/yuki-xin/picgo-plugin-web-uploader">picgo-plugin-web-uploader</a> 支持<a href="https://github.com/xiebruce/PicUploader">PicUploader</a>配置的图床插件</li><li><a href="https://github.com/chengww5217/picgo-plugin-qingstor-uploader">picgo-plugin-qingstor-uploader</a> 支持青云云存储的图床插件</li><li><a href="https://github.com/chengww5217/picgo-plugin-blog-uploader">picgo-plugin-blog-uploader</a> 支持掘金、简书和CSDN来做图床的图床插件</li></ol><p>如果你也想为PicGo开发插件，欢迎阅读<a href="https://picgo.github.io/PicGo-Core-Doc/zh/">开发文档</a>，PicGo有你更精彩哈哈！</p><p>本文很多都是我在开发<code>PicGo</code>的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的<code>electron-vue</code>开发带来一些启发。文中相关的代码，你都可以在<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>和<a href="https://github.com/PicGo/PicGo-Core">PicGo-Core</a>的项目仓库里找到，欢迎star~如果本文能够给你带来帮助，那么将是我最开心的地方。如果喜欢，欢迎关注我的<a href="https://molunerfinn.com/">博客</a>以及<a href="https://molunerfinn.com/tags/Electron-vue/">本系列文章</a>的后续进展。</p><blockquote><p><strong>注：文中的图片除未特地说明之外均属于我个人作品，需要转载请私信</strong></p></blockquote><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p>感谢这些高质量的文章：</p><ol><li><a href="https://zhuanlan.zhihu.com/p/38730825">用Node.js开发一个Command Line Interface (CLI)</a></li><li><a href="https://zhuanlan.zhihu.com/p/26895282">Node.js编写CLI的实践</a></li><li><a href="http://www.infoq.com/cn/articles/nodejs-module-mechanism">Node.js模块机制</a></li><li><a href="https://onetwo.ren/%E5%89%8D%E7%AB%AF%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/">前端插件系统设计与实现</a></li><li><a href="https://blog.csdn.net/kyfxbl/article/details/47787827">Hexo插件机制分析</a></li><li><a href="http://blog.yunplus.io/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E6%8F%92%E4%BB%B6%E6%89%A9%E5%B1%95/">如何实现一个简单的插件扩展</a></li><li><a href="https://fenying.net/2017/12/02/publish-to-npm/">使用NPM发布与维护TypeScript模块</a></li><li><a href="https://github.com/basarat/ts-npm-module">typescript npm 包例子</a></li><li><a href="https://docs.travis-ci.com/user/deployment/npm/">通过travis-ci发布npm包</a></li><li><a href="https://discuss.atom.io/t/dynamically-load-module-in-plugin-from-local-project-node-modules-folder/42930/2">Dynamic load module in plugin from local project node_modules folder</a></li><li><a href="https://aotu.io/notes/2016/08/09/command-line-development/index.html">跟着老司机玩转Node命令行</a></li><li>以及没来得及记录的那些好文章，感谢你们！</li></ol>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;前段时间，我用&lt;a href=&quot;https://github.com/SimulatedGREG/electron-vue&quot;&gt;electron-vue&lt;/a&gt;开发了一款跨平台（目前支持主流三大桌面操作系统）的免费开源的图床上传应用——&lt;a href=&quot;https://github.com/Molunerfinn/PicGo&quot;&gt;PicGo&lt;/a&gt;，在开发过程中踩了不少的坑，不仅来自应用的业务逻辑本身，也来自electron本身。在开发这个应用过程中，我学了不少的东西。因为我也是从0开始学习electron，所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历，用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。&lt;/p&gt;
&lt;p&gt;预计将会从几篇&lt;a href=&quot;https://molunerfinn.com/tags/Electron-vue/&quot;&gt;系列文章&lt;/a&gt;或方面来展开：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-1/&quot;&gt;electron-vue入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-2/&quot;&gt;Main进程和Renderer进程的简单开发&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-3/&quot;&gt;引入基于Lodash的JSON database——lowdb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-4/&quot;&gt;跨平台的一些兼容措施&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-5/&quot;&gt;通过CI发布以及更新的方式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-6/&quot;&gt;开发插件系统——CLI部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-7/&quot;&gt;开发插件系统——GUI部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;想到再写…&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;说明&quot;&gt;&lt;a href=&quot;#说明&quot; class=&quot;headerlink&quot; title=&quot;说明&quot;&gt;&lt;/a&gt;说明&lt;/h2&gt;&lt;p&gt;&lt;code&gt;PicGo&lt;/code&gt;是采用&lt;code&gt;electron-vue&lt;/code&gt;开发的，所以如果你会&lt;code&gt;vue&lt;/code&gt;，那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如&lt;code&gt;react&lt;/code&gt;、&lt;code&gt;angular&lt;/code&gt;，那么纯按照本教程虽然在render端（可以理解为页面）的构建可能学习到的东西不多，不过在main端(&lt;code&gt;Electron&lt;/code&gt;的主进程）应该还是能学习到相应的知识的。&lt;/p&gt;
&lt;p&gt;如果之前的文章没阅读的朋友可以先从&lt;a href=&quot;https://molunerfinn.com/tags/Electron-vue/&quot;&gt;之前的文章&lt;/a&gt;跟着看。并且如果没有看过前一篇CLI插件系统构建的朋友，需要先行阅读，本文涉及到的部分内容来自上一篇文章。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="Vue" scheme="https://molunerfinn.com/tags/Vue/"/>
    
    <category term="Electron-vue" scheme="https://molunerfinn.com/tags/Electron-vue/"/>
    
  </entry>
  
  <entry>
    <title>Electron-vue开发实战5——开发插件系统之CLI部分</title>
    <link href="https://molunerfinn.com/electron-vue-6/"/>
    <id>https://molunerfinn.com/electron-vue-6/</id>
    <published>2019-02-04T11:30:00.000Z</published>
    <updated>2026-03-08T01:14:37.983Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p><strong>祝大家2019年猪年新年快乐！本文较长，需要一定耐心看完哦~</strong></p><p>前段时间，我用<a href="https://github.com/SimulatedGREG/electron-vue">electron-vue</a>开发了一款跨平台（目前支持主流三大桌面操作系统）的免费开源的图床上传应用——<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>，在开发过程中踩了不少的坑，不仅来自应用的业务逻辑本身，也来自electron本身。在开发这个应用过程中，我学了不少的东西。因为我也是从0开始学习electron，所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历，用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。</p><p>预计将会从几篇<a href="https://molunerfinn.com/tags/Electron-vue/">系列文章</a>或方面来展开：</p><ol><li><a href="https://molunerfinn.com/electron-vue-1/">electron-vue入门</a></li><li><a href="https://molunerfinn.com/electron-vue-2/">Main进程和Renderer进程的简单开发</a></li><li><a href="https://molunerfinn.com/electron-vue-3/">引入基于Lodash的JSON database——lowdb</a></li><li><a href="https://molunerfinn.com/electron-vue-4/">跨平台的一些兼容措施</a></li><li><a href="https://molunerfinn.com/electron-vue-5/">通过CI发布以及更新的方式</a></li><li><a href="https://molunerfinn.com/electron-vue-6/">开发插件系统——CLI部分</a></li><li><a href="https://molunerfinn.com/electron-vue-7/">开发插件系统——GUI部分</a></li><li>想到再写…</li></ol><h2 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h2><p><code>PicGo</code>是采用<code>electron-vue</code>开发的，所以如果你会<code>vue</code>，那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如<code>react</code>、<code>angular</code>，那么纯按照本教程虽然在render端（可以理解为页面）的构建可能学习到的东西不多，不过在main端（electron的主进程）应该还是能学习到相应的知识的。</p><p>如果之前的文章没阅读的朋友可以先从<a href="https://molunerfinn.com/tags/Electron-vue/">之前的文章</a>跟着看。</p><span id="more"></span><p><strong>说在前面，其实这篇文章写起来真的很难。如何构建一个插件系统，我花了半年的时间。要在一篇或者两篇文章里把这个东西说好是真的不容易。所以可能行文上会有一些瑕疵，后续会不断打磨。</strong></p><h2 id="插件系统——容器"><a href="#插件系统——容器" class="headerlink" title="插件系统——容器"></a>插件系统——容器</h2><p>相信很多人平时更多的是给其他框架诸如<code>Vue</code>、<code>React</code>或者<code>Webpack</code>等写插件。我们可以把提供插件系统的框架称为「容器」，通过容器暴露出来的API，插件可以挂载到容器上，或者接入容器的生命周期来实现一些更定制化的功能。</p><p>比如<code>Webpack</code>本质上是一个流程系统，它通过<a href="https://github.com/webpack/tapable">Tapable</a>暴露了很多生命周期的钩子，插件可以通过接入这些生命周期钩子实现流水线作业——比如<code>babel</code>系列的插件把<code>ES6</code>代码转义成<code>ES5</code>；<code>SASS</code>、<code>LESS</code>、<code>Stylus</code>系列的插件把预处理的<code>CSS</code>代码编译成浏览器可识别的正常<code>CSS</code>代码等等。</p><p>我们要实现一个插件系统，本质上也是实现这么一个容器。这个容器以及对应的插件需要具备如下基本特征：</p><ul><li>容器在没有 <strong>第三方插件</strong> 接入的情况下也能 <strong>实现基本功能</strong></li><li>插件具有独立性</li><li>插件可配置可管理</li></ul><p>第一点应该很容易理解。如果一个插件系统因为没有第三方插件的存在就无法运行，那么这个插件系统有什么用呢？不过有别于第三方插件，很多插件系统有自己内置的插件，比如<code>vue-cli</code>、<code>Webpack</code>的一系列内置插件。这个时候插件系统本身的一些功能就会由内置的插件去实现。</p><p>第二点，插件的独立性是指插件本身运行时不会 <strong>主动</strong> 影响其他插件的运作。当然某个插件可以依赖于其他插件的运行结果。</p><p>第三点，插件如果不能配置不能管理，那么从安装插件阶段就会遇到问题。所以容器需要有设计良好的入口给予插件注册。</p><p>接下来的部分，我将结合<a href="https://github.com/PicGo/PicGo-Core">PicGo-Core</a>与<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>来详细说明CLI插件系统与GUI插件系统如何构建与实现。</p><h2 id="CLI插件系统"><a href="#CLI插件系统" class="headerlink" title="CLI插件系统"></a>CLI插件系统</h2><h3 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h3><p>其实CLI插件系统可以认为是无GUI的插件系统，也就是运行在命令行或者不带有可视化界面的插件系统。为什么我们开发Electron的插件系统，需要扯到CLI插件系统呢？这里需要简单回顾一下Electron的结构：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/8700af19ly1fncq342rk8j20cs0d63zd" alt="Electron结构"></p><p>可以看到除了<code>Renderer</code>的界面渲染，大部分的功能是由<code>Main</code>进程提供的。对于PicGo而言，它的底层应该是一个上传流程系统，如下：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/picgo-core-fix.jpg" alt="PicGo-Core"></p><ol><li>Input（输入）：接受来自外部的输入，默认是通过路径或者完整的图片base64信息</li><li>Transformer（转换器）：把输入转换成可以被上传器上传的对象（包含图片尺寸、base64、图片名等信息）</li><li>Uploader（上传器）：将来自转换器的输出上传到指定的地方，默认的上传器将会是SM.MS</li><li>Output（输出）：输出上传的结果，通常可以在输出的imgUrl里拿到结果</li></ol><p>所以理论上它的底层应该在Node.js端就能实现。而Electron的<code>Renderer</code>进程只是实现了GUI界面，去调用底层Node.js端实现的流程系统提供的API而已。类似于我们平时在开发网页时候的前后端分离，只不过现在这个后端是基于Node.js实现的插件系统。基于这个思路，我开始着手<a href="https://github.com/PicGo/PicGo-Core">PicGo-Core</a>的实现。</p><h3 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h3><p>通常来说一个插件系统都有自己的一个生命周期，比如<code>Vue</code>有<code>beforeCreate</code>、<code>created</code>、<code>mounted</code>等等，<code>Webpack</code>有<code>beforeRun</code>、<code>run</code>、<code>afterCompile</code>等等。这个也是一个插件系统的灵魂所在，通过接入系统的生命周期，赋予了插件更多的自由度。</p><p>因此我们可以先来实现一个生命周期类。代码可以参考<a href="https://github.com/PicGo/PicGo-Core/blob/dev/src/core/Lifecycle.ts">Lifecycle.ts</a>。</p><p>生命周期流程可以参考上面的流程图。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Lifecycle</span> &#123;</span><br><span class="line">  <span class="comment">// 整个生命周期的入口</span></span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">start</span> (<span class="attr">input</span>: <span class="built_in">any</span>[]): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">beforeTransform</span>(input)</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">doTransform</span>(input)</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">beforeUpload</span>(input)</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">doUpload</span>(input)</span><br><span class="line">      <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">afterUpload</span>(input)</span><br><span class="line">    &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(e)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 获取原始输入，转换前</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">beforeTransform</span> (input) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 将输入转换成Uploader可上传的格式</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">doTransform</span> (input) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// Uploader上传前</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">beforeUpload</span> (input) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// Uploader上传</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">doUpload</span> (input) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// Uploader上传完成后</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">afterUpload</span> (input) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在实际使用中，我们可以通过：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> lifeCycle = <span class="keyword">new</span> <span class="title class_">LifeCycle</span>()</span><br><span class="line">lifeCycle.<span class="title function_">start</span>([...])</span><br></pre></td></tr></table></figure><p>来运行整个上传流程的生命周期。不过到这里我们还没有看到任何跟插件相关的东西。这是为了实现我们说的第一个条件： 容器在没有 <strong>第三方插件</strong> 接入的情况下也能 <strong>实现基本功能</strong>。</p><h3 id="广播事件"><a href="#广播事件" class="headerlink" title="广播事件"></a>广播事件</h3><p>很多时候我们需要将一些事件以某种方式传递出去。就像发布订阅模型一样，由容器发布，由插件订阅。这个时候我们可以直接让<code>Lifecycle</code>这个类继承Node.js自带的<code>EventEmmit</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Lifecycle</span> <span class="keyword">extends</span> <span class="title class_ inherited__">EventEmitter</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>()</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么<code>Lifecycle</code>也就拥有了<code>EventEmitter</code>的<code>emit</code>和<code>on</code>方法了。对于容器来说，我们只需要<code>emit</code>事件出去即可。</p><p>比如在<code>PicGo-Core</code>里，上传的整个流程都会往外广播事件，通知插件当前进行到什么阶段，并且将当前的输入或者输出在广播的时候发送出去。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">beforeTransform</span> (input) &#123;</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">&#x27;beforeTransform&#x27;</span>, input) <span class="comment">// 广播事件</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>插件可以自由选择监听想要监听的事件。比如插件想要知道上传结束后的结果（伪代码）：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">plugin.<span class="title function_">on</span>(<span class="string">&#x27;finished&#x27;</span>, <span class="function">(<span class="params">output</span>) =&gt;</span> &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(output) <span class="comment">// 获取output</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>在开发PicGo-Core的时候，有一些很有用的事件。在这里我也想分享出来，虽然不是所有插件系统都会有这样的事件，但是结合自己和项目的实际需要，他们有的时候很有用。</p><h4 id="进度事件"><a href="#进度事件" class="headerlink" title="进度事件"></a>进度事件</h4><p>平时我们上传或者下载文件的时候，都会注意一个东西：进度条。同样，在PicGo-Core里也暴露了一个事件，叫做<code>uploadProgress</code>，用于告诉用户当前的上传进度。不过在PicGo-Core，上传进度是从<code>beforeTransform</code>就开始算了，为了方便计算，划分了5个固定的值。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">beforeTransform</span> (input) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">&#x27;uploadProgress&#x27;</span>, <span class="number">0</span>) <span class="comment">// 转换前，进度0</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">doTransform</span> (input) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">&#x27;uploadProgress&#x27;</span>, <span class="number">30</span>) <span class="comment">// 开始转换，进度30</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">beforeUpload</span> (input) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">&#x27;uploadProgress&#x27;</span>, <span class="number">60</span>) <span class="comment">// 开始上传，进度60</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">afterUpload</span> (input) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">&#x27;uploadProgress&#x27;</span>, <span class="number">100</span>) <span class="comment">// 上传完毕，进度100</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果上传失败的话就返回<code>-1</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="title function_">start</span> (<span class="attr">input</span>: <span class="built_in">any</span>[]): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line"> <span class="keyword">try</span> &#123;</span><br><span class="line">   <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">beforeTransform</span>(input)</span><br><span class="line">   <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">doTransform</span>(input)</span><br><span class="line">   <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">beforeUpload</span>(input)</span><br><span class="line">   <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">doUpload</span>(input)</span><br><span class="line">   <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">afterUpload</span>(input)</span><br><span class="line"> &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">   <span class="variable language_">console</span>.<span class="title function_">log</span>(e)</span><br><span class="line">   <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">&#x27;uploadProgress&#x27;</span>, -<span class="number">1</span>)</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过监听这个事件，PicGo就能做出如下的上传进度条：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/picgo-2.0.gif" alt="progress-bar"></p><h4 id="系统通知"><a href="#系统通知" class="headerlink" title="系统通知"></a>系统通知</h4><p>如果上传出了问题，或者有些信息需要通过系统级别的通知告诉用户的话，可以发布<code>notification</code>事件。通过监听这个事件可以调用系统通知来发布。插件也可以发布这个事件，让PicGo监听。如上图上传成功后右上角的通知。</p><h3 id="接入生命周期"><a href="#接入生命周期" class="headerlink" title="接入生命周期"></a>接入生命周期</h3><p>上部分讲到了生命周期中的事件广播，可以发现事件广播是只管发不管结果的。也就是PicGo-Core只管发布这个事件，至于有没有插件监听，监听后做了什么都不用关心。（怎么有点像UDP一样）。但是实际上很多时候我们需要接入生命周期做一些事情的。</p><p>就拿上传流程来说，我要是想要上传前压缩图片，那么监听<code>beforeUpload</code>事件是做不到的。因为在<code>beforeUpload</code>事件里就算你把图片已经压缩了，恐怕上传的流程早就走完了，<code>emit</code>事件出去后生命周期照旧运行。</p><p>因此我们需要在容器的生命周期里实现一个功能，能够让插件接入它的生命周期，在执行完当前生命周期的插件的动作后，才把结果送往下一个生命周期。可以发现，这里有一个「等待」插件执行的动作。因此PicGo-Core使用最简易而直观的<code>async</code>函数配合<code>await</code>来实现「等待」。</p><p>我们先不用考虑插件是如何注册的，后文会说到。我们先来实现怎么让插件接入生命周期。</p><p>下面以生命周期<code>beforeUpload</code>为例：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">beforeUpload</span> (input) &#123;</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="title function_">emit</span>(<span class="string">&#x27;uploadProgress&#x27;</span>, <span class="number">60</span>)</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="title function_">emit</span>(<span class="string">&#x27;beforeUpload&#x27;</span>, input)</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">handlePlugins</span>(beforeUploadPlugins.<span class="title function_">getList</span>(), input) <span class="comment">// 执行并「等待」插件执行结束</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到我们通过<code>await</code>等待生命周期方法<code>handlePlugins</code>（下文会说明如何实现）的执行结束。而我们运行的插件列表是通过<code>beforeUploadPlugins.getList()</code>（下文会说明如何实现）获取的，说明这些是只针对<code>beforeUpload</code>这个生命周期的插件。然后将输入<code>input</code>传入<code>handlePlugins</code>让插件们调用即可。</p><p>现在我们实现一下<code>handlePlugins</code>：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">async</span> <span class="title function_">handlePlugins</span> (<span class="attr">plugins</span>: <span class="title class_">Plugin</span>[], <span class="attr">input</span>: <span class="built_in">any</span>[]) &#123;</span><br><span class="line">  <span class="keyword">await</span> <span class="title class_">Promise</span>.<span class="title function_">all</span>(plugins.<span class="title function_">map</span>(<span class="title function_">async</span> (<span class="attr">plugin</span>: <span class="title class_">Plugin</span>) =&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> plugin.<span class="title function_">handle</span>(input)</span><br><span class="line">  &#125;))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们通过<code>Promise.all</code>以及<code>await</code>来「等待」所有插件执行。这里需要注意的是，每个PicGo插件需要实现一个<code>handle</code>方法来供<code>PicGo-Core</code>调用。可以看到，这里实现我们说的第二个特征： <strong>插件具有独立性</strong>。</p><p>从这里也能看到我们通过<code>async</code>和<code>await</code>构建了一个能够「等待」插件执行结束的环境。这样就解决了光是通过广播事件无法接入插件系统的生命周期的问题。</p><p>不，等等，这里还有一个问题。<code>beforeUploadPlugins.getList()</code>是哪来的？上面只是一个示例代码。实际上PicGo-Core根据上传流程里的不同生命周期预留了五种不同的插件：</p><ul><li>beforeTransformPlugins</li><li>transformer</li><li>beforeUploadPlugins</li><li>uploader</li><li>afterUploadPlugins</li></ul><p>分别在上传的5个周期里调用。虽然这5种插件调用的时机不一样，但是它们的实现是同样的：有同样的注册机制、同样的方法用于获取插件列表、获取插件信息等等。所以我们接下去来实现一个生命周期的插件类。</p><h3 id="生命周期插件类"><a href="#生命周期插件类" class="headerlink" title="生命周期插件类"></a>生命周期插件类</h3><p>这个是插件系统里很关键的一环，这个类的实现了插件应该以什么方式注册到我们的插件系统里，以及插件系统如何获取他们。这块的代码可以参考 <a href="https://github.com/PicGo/PicGo-Core/blob/dev/src/lib/LifecyclePlugins.ts">LifecyclePlugins.ts</a>。</p><p>以下是实现：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LifecyclePlugins</span> &#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// list就是插件列表。以对象形式呈现。</span></span><br><span class="line">  <span class="attr">list</span>: &#123;</span><br><span class="line">    [<span class="attr">propName</span>: <span class="built_in">string</span>]: <span class="title class_">Plugin</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">list</span> = &#123;&#125; <span class="comment">// 初始化插件列表为&#123;&#125;</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 插件注册的入口</span></span><br><span class="line">  <span class="title function_">register</span> (<span class="attr">id</span>: <span class="built_in">string</span>, <span class="attr">plugin</span>: <span class="title class_">Plugin</span>): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="comment">// 如果插件没有提供id，则不予注册</span></span><br><span class="line">    <span class="keyword">if</span> (!id) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">&#x27;id is required!&#x27;</span>)</span><br><span class="line">    <span class="comment">// 如果插件没有handle的方法，则不予注册</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="keyword">typeof</span> plugin.<span class="property">handle</span> !== <span class="string">&#x27;function&#x27;</span>) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">&#x27;plugin.handle must be a function!&#x27;</span>)</span><br><span class="line">    <span class="comment">// 如果插件的id重复了，则不予注册</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">list</span>[id]) <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">`<span class="subst">$&#123;<span class="variable language_">this</span>.name&#125;</span> duplicate id: <span class="subst">$&#123;id&#125;</span>!`</span>)</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">list</span>[id] = plugin</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 通过插件ID获取插件</span></span><br><span class="line">  <span class="title function_">get</span> (<span class="attr">id</span>: <span class="built_in">string</span>): <span class="title class_">Plugin</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">list</span>[id]</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 获取插件列表</span></span><br><span class="line">  <span class="title function_">getList</span> (): <span class="title class_">Plugin</span>[] &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">keys</span>(<span class="variable language_">this</span>.<span class="property">list</span>).<span class="title function_">map</span>(<span class="function">(<span class="params"><span class="attr">item</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="variable language_">this</span>.<span class="property">list</span>[item])</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 获取插件ID列表</span></span><br><span class="line">  <span class="title function_">getIdList</span> (): <span class="built_in">string</span>[] &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">keys</span>(<span class="variable language_">this</span>.<span class="property">list</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">LifecyclePlugins</span></span><br></pre></td></tr></table></figure><p>对于插件而言最重要的是<code>register</code>方法，它是插件注册的入口。通过<code>register</code>注册后，会在<code>Lifecycle</code>内部的<code>list</code>以<code>id:plugin</code>形式里写入这个插件。注意到，PicGo-Core要求每个插件需要实现一个<code>handle</code>的方法，用于之后在生命周期里调用。</p><p>这里用伪代码说明一下插件要如何注册：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">beforeTransformPlugins.<span class="title function_">register</span>(<span class="string">&#x27;test&#x27;</span>, &#123;</span><br><span class="line">  <span class="title function_">handle</span> (ctx) &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(ctx)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>这里我们就注册了一个<code>id</code>叫做<code>test</code>的插件，它是一个<code>beforeTransform</code>阶段的插件，它的作用就是打印传入的信息。</p><p>然后在不同的生命周期里，调用<code>LifeCyclePlugins.getList()</code>的方法就能获取这个生命周期对应的插件的列表了。</p><h3 id="抽离出核心类"><a href="#抽离出核心类" class="headerlink" title="抽离出核心类"></a>抽离出核心类</h3><p>如果仅仅是实现一个能够在Node.js项目里运行的插件系统，上面两个部分基本就够了：</p><ol><li>Lifecyle类负责整个生命周期</li><li>LifecylePlugins类负责插件的注册与调用</li></ol><p>不过一个良好的CLI插件系统还需要至少如下的部分（至少我觉得）：</p><ol><li>可以通过命令行调用</li><li>能够读取配置文件进行额外配置</li><li>命令行一键安装插件</li><li>命令行完成插件配置</li><li>友好的log信息提示</li></ol><blockquote><p>此处可以参考vue-cli3这个工具。</p></blockquote><p>因此我们至少还需要如下的部分：</p><ol><li>命令行操作相关的类</li><li>配置文件操作相关</li><li>插件安装、卸载、更新等相关操作的类</li><li>插件加载相关的类</li><li>日志信息输出相关的类</li></ol><p>这上面的几个部分都跟生命周期类本身没有特别强的耦合关系，所以可以不必将它们都放到生命周期类里实现。</p><p>相对的，我们抽离出一个<code>Core</code>作为核心，将上述这些类包含到这个核心类中，核心类负责命令行命令的注册、插件的加载、优化日志信息以及调用生命周期等等。</p><p>最后再将这个核心类暴露出去，供使用者或者开发者使用。这个就是PicGo-Core的核心 <a href="https://github.com/PicGo/PicGo-Core/blob/dev/src/core/PicGo.ts">PicGo.ts</a> 的实现。</p><p>PicGo本身的实现并不复杂，基本上只是调用上述几个类实例的方法。</p><p>不过注意到这里有一个之前一直没有提到的东西。PicGo-Core除了核心PicGo之外的几个子类里，基本上在<code>constructor</code>构建函数阶段都会传入一个叫做<code>ctx</code>的参数。这个参数是什么？这个参数是PicGo这个类自身的<code>this</code>。通过传入<code>this</code>，PicGo-Core的子类也能使用PicGo核心类暴露出来的方法了。</p><p>比如<code>Logger</code>类实现了美观的命令行日志输出：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/20180912153940.png" alt="logger"></p><p>那么在其他子类里想要调用<code>Logger</code>的方法也很容易：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ctx.<span class="property">log</span>.<span class="title function_">success</span>(<span class="string">&#x27;Hello world!&#x27;</span>)</span><br></pre></td></tr></table></figure><p>其中<code>ctx</code>就是我们上面说的，PicGo自身的<code>this</code>指针。</p><p>我们接下去介绍的每个类具体的实现。</p><h3 id="日志输出相关类"><a href="#日志输出相关类" class="headerlink" title="日志输出相关类"></a>日志输出相关类</h3><p>先从这个类开始说起是因为这个类是最简单而且侵入性最小的一个类。有它没它都行，但是有它自然是锦上添花。</p><p>PicGo实现美化日志输出的库是<a href="https://github.com/chalk/chalk">chalk</a>，它的作用就是用来输出花花绿绿的命令行文字：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f6368616c6b2f616e73692d7374796c657340383236313639376339356266333462366337373637653263626539393431613835316435393338352f73637265656e73686f742e737667"></p><p>用起来也很简单：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> log = chalk.<span class="title function_">green</span>(<span class="string">&#x27;Success&#x27;</span>)</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(log) <span class="comment">// 绿色字体的Success</span></span><br></pre></td></tr></table></figure><p>我们打算实现4种输出类型，success、warn、info和error：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/20180912153940.png" alt="logger"></p><p>于是创建如下的类：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> chalk <span class="keyword">from</span> <span class="string">&#x27;chalk&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">PicGo</span> <span class="keyword">from</span> <span class="string">&#x27;../core/PicGo&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Logger</span> &#123;</span><br><span class="line">  <span class="attr">level</span>: &#123;</span><br><span class="line">    [<span class="attr">propName</span>: <span class="built_in">string</span>]: <span class="built_in">string</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="attr">ctx</span>: <span class="title class_">PicGo</span></span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"><span class="attr">ctx</span>: <span class="title class_">PicGo</span></span>) &#123; <span class="comment">// 将PicGo的this传入构造函数，使得Logger也能使用PicGo核心类暴露的方法</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">level</span> = &#123;</span><br><span class="line">      <span class="attr">success</span>: <span class="string">&#x27;green&#x27;</span>,</span><br><span class="line">      <span class="attr">info</span>: <span class="string">&#x27;blue&#x27;</span>,</span><br><span class="line">      <span class="attr">warn</span>: <span class="string">&#x27;yellow&#x27;</span>,</span><br><span class="line">      <span class="attr">error</span>: <span class="string">&#x27;red&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">ctx</span> = ctx</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 实际输出函数</span></span><br><span class="line">  <span class="keyword">protected</span> <span class="title function_">handleLog</span> (<span class="attr">type</span>: <span class="built_in">string</span>, <span class="attr">msg</span>: <span class="built_in">string</span> | <span class="title class_">Error</span>): <span class="built_in">string</span> | <span class="title class_">Error</span> | <span class="literal">undefined</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">config</span>.<span class="property">silent</span>) &#123; <span class="comment">// 如果不是静默模式，静默模式不输出log</span></span><br><span class="line">      <span class="keyword">let</span> log = chalk[<span class="variable language_">this</span>.<span class="property">level</span>[<span class="keyword">type</span>]](<span class="string">`[PicGo <span class="subst">$&#123;<span class="keyword">type</span>.toUpperCase()&#125;</span>]: `</span>)</span><br><span class="line">      log += msg</span><br><span class="line">      <span class="variable language_">console</span>.<span class="title function_">log</span>(log)</span><br><span class="line">      <span class="keyword">return</span> msg</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 对应四种不同类型</span></span><br><span class="line">  <span class="title function_">success</span> (<span class="attr">msg</span>: <span class="built_in">string</span> | <span class="title class_">Error</span>): <span class="built_in">string</span> | <span class="title class_">Error</span> | <span class="literal">undefined</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">handleLog</span>(<span class="string">&#x27;success&#x27;</span>, msg)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">info</span> (<span class="attr">msg</span>: <span class="built_in">string</span> | <span class="title class_">Error</span>): <span class="built_in">string</span> | <span class="title class_">Error</span> | <span class="literal">undefined</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">handleLog</span>(<span class="string">&#x27;info&#x27;</span>, msg)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">error</span> (<span class="attr">msg</span>: <span class="built_in">string</span> | <span class="title class_">Error</span>): <span class="built_in">string</span> | <span class="title class_">Error</span> | <span class="literal">undefined</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">handleLog</span>(<span class="string">&#x27;error&#x27;</span>, msg)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">warn</span> (<span class="attr">msg</span>: <span class="built_in">string</span> | <span class="title class_">Error</span>): <span class="built_in">string</span> | <span class="title class_">Error</span> | <span class="literal">undefined</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">handleLog</span>(<span class="string">&#x27;warn&#x27;</span>, msg)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">Logger</span></span><br></pre></td></tr></table></figure><p>之后再将<code>Logger</code>这个类挂载到PicGo核心类上：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Logger</span> <span class="keyword">from</span> <span class="string">&#x27;../lib/Logger&#x27;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PicGo</span> &#123;</span><br><span class="line">  <span class="attr">log</span>: <span class="title class_">Logger</span></span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"></span>) &#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">log</span> = <span class="keyword">new</span> <span class="title class_">Logger</span>(<span class="variable language_">this</span>) <span class="comment">// 把this传入Logger，也就是Logger里的ctx</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样其他挂载到PicGo核心类上的类就能使用<code>ctx.log</code>来调用log里的方法了。</p><h3 id="配置文件相关"><a href="#配置文件相关" class="headerlink" title="配置文件相关"></a>配置文件相关</h3><p>很多时候我们的所写的系统也好、插件也好，或多或少需要一些配置之后才能更好地使用。比如<code>vue-cli3</code>的<code>vue.config.js</code>，比如<code>hexo</code>的<code>_config.yml</code>等等。而PicGo也不例外。默认情况下它可以直接使用，但是如果想要做些其他操作，自然就需要配置了。所以配置文件是插件系统很重要的一个组成部分。</p><p>之前我在Electron版的PicGo上使用了<a href="https://github.com/typicode/lowdb">lowdb</a>作为JSON配置文件的读写库，体验不错。为了向前兼容PicGo的配置，写PicGo-Core的时候我依然采用了这个库。关于lowdb的一些具体用法，我在之前的一篇文章里有提及，有兴趣的可以看看——<a href="https://molunerfinn.com/electron-vue-3/">传送门</a>。</p><p>由于lowdb做的是类似MySQL一样的持久化配置，它需要磁盘上一个具体的JSON文件作为载体，所以无法通过创建一个配置对象去初始化配置。因此一切都从这个配置文件展开：</p><p>PicGo-Core采用一个默认的配置文件：<code>homedir()/.picgo/config.json</code>，如果在实例化PicGo没提供配置文件路径那么就会使用这个文件。如果使用者提供了具体的配置文件，那么就会使用所提供的配置文件。</p><p>下面来实现一下PicGo初始化的过程：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PicGo</span> <span class="keyword">extends</span> <span class="title class_ inherited__">EventEmitter</span> &#123;</span><br><span class="line">  <span class="attr">configPath</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="keyword">private</span> <span class="attr">lifecycle</span>: <span class="title class_">Lifecycle</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"><span class="attr">configPath</span>: <span class="built_in">string</span> = <span class="string">&#x27;&#x27;</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>()</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">configPath</span> = configPath <span class="comment">// 传入configPath</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">init</span>()</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">init</span> () &#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">configPath</span> === <span class="string">&#x27;&#x27;</span>) &#123; <span class="comment">// 如果不提供配置文件路径，就使用默认配置</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">configPath</span> = <span class="title function_">homedir</span>() + <span class="string">&#x27;/.picgo/config.json&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (path.<span class="title function_">extname</span>(<span class="variable language_">this</span>.<span class="property">configPath</span>).<span class="title function_">toUpperCase</span>() !== <span class="string">&#x27;.JSON&#x27;</span>) &#123; <span class="comment">// 如果配置文件的格式不是JSON就返回错误日志</span></span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">configPath</span> = <span class="string">&#x27;&#x27;</span></span><br><span class="line">      <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">log</span>.<span class="title function_">error</span>(<span class="string">&#x27;The configuration file only supports JSON format.&#x27;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">const</span> exist = fs.<span class="title function_">pathExistsSync</span>(<span class="variable language_">this</span>.<span class="property">configPath</span>)</span><br><span class="line">    <span class="keyword">if</span> (!exist) &#123; <span class="comment">// 如果不存在就创建</span></span><br><span class="line">      fs.<span class="title function_">ensureFileSync</span>(<span class="string">`<span class="subst">$&#123;<span class="variable language_">this</span>.configPath&#125;</span>`</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么在实例化PicGo的时候就是如下这样：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">PicGo</span> = <span class="built_in">require</span>(<span class="string">&#x27;picgo&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> picgo = <span class="keyword">new</span> <span class="title class_">PicGo</span>() <span class="comment">// 不提供配置文件就用默认配置文件</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 或者</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> picgo = <span class="keyword">new</span> <span class="title class_">PicGo</span>(<span class="string">&#x27;./xxx.json&#x27;</span>) <span class="comment">// 提供配置文件就用所提供的配置文件</span></span><br></pre></td></tr></table></figure><p>有了配置文件之后，我们只需要实现三个基本操作：</p><ol><li>初始化配置</li><li>读取配置</li><li>写入配置（写入配置包括创建、更新、删除等）</li></ol><h4 id="初始化配置"><a href="#初始化配置" class="headerlink" title="初始化配置"></a>初始化配置</h4><p>一般来说我们的系统都会有一些默认的配置，PicGo也不例外。我们可以选择把默认配置写到代码里，也可以选择把默认配置写到代码里。因为PicGo的配置文件有持久化的需求，所以把一些关键的默认配置写入配置文件是合理的。</p><p>初始化配置的时候会用到<a href="https://github.com/typicode/lowdb">lowdb</a>的一些知识，这里就不展开了：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> lowdb <span class="keyword">from</span> <span class="string">&#x27;lowdb&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">FileSync</span> <span class="keyword">from</span> <span class="string">&#x27;lowdb/adapters/FileSync&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> initConfig = (<span class="attr">configPath</span>: <span class="built_in">string</span>): lowdb.<span class="property">LowdbSync</span>&lt;<span class="built_in">any</span>&gt; =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> adapter = <span class="keyword">new</span> <span class="title class_">FileSync</span>(configPath, &#123; <span class="comment">// lowdb的adapter，用于读取配置文件</span></span><br><span class="line">    <span class="attr">deserialize</span>: (<span class="attr">data</span>: <span class="built_in">string</span>): <span class="function"><span class="params">Function</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> (<span class="keyword">new</span> <span class="title class_">Function</span>(<span class="string">`return <span class="subst">$&#123;data&#125;</span>`</span>))()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">const</span> db = <span class="title function_">lowdb</span>(adapter) <span class="comment">// 暴露出来的db对象</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!db.<span class="title function_">has</span>(<span class="string">&#x27;picBed&#x27;</span>).<span class="title function_">value</span>()) &#123; <span class="comment">// 如果没有picBed配置</span></span><br><span class="line">    db.<span class="title function_">set</span>(<span class="string">&#x27;picBed&#x27;</span>, &#123; <span class="comment">// 就生成一个默认图床为SM.MS的配置</span></span><br><span class="line">      <span class="attr">current</span>: <span class="string">&#x27;smms&#x27;</span></span><br><span class="line">    &#125;).<span class="title function_">write</span>()</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> (!db.<span class="title function_">has</span>(<span class="string">&#x27;picgoPlugins&#x27;</span>).<span class="title function_">value</span>()) &#123; <span class="comment">// 同理</span></span><br><span class="line">    db.<span class="title function_">set</span>(<span class="string">&#x27;picgoPlugins&#x27;</span>, &#123;&#125;).<span class="title function_">write</span>()</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> db <span class="comment">// 将db暴露出去让外部使用</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么在PicGo初始化阶段就可以将<code>configPath</code>传入，来实现配置的初始化，以及获取配置。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">init</span> () &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">let</span> db = <span class="title function_">initConfig</span>(<span class="variable language_">this</span>.<span class="property">configPath</span>)</span><br><span class="line">  <span class="variable language_">this</span>.<span class="property">config</span> = db.<span class="title function_">read</span>().<span class="title function_">value</span>() <span class="comment">// 将配置文件内容存入this.config</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="读取配置"><a href="#读取配置" class="headerlink" title="读取配置"></a>读取配置</h4><p>一旦初始化配置之后，要获取配置就很容易了：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; get &#125; <span class="keyword">from</span> <span class="string">&#x27;lodash&#x27;</span></span><br><span class="line"><span class="title function_">getConfig</span> (<span class="attr">name</span>: <span class="built_in">string</span> = <span class="string">&#x27;&#x27;</span>): <span class="built_in">any</span> &#123;</span><br><span class="line">  <span class="keyword">if</span> (name) &#123; <span class="comment">// 如果提供了配置项的名字</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">get</span>(<span class="variable language_">this</span>.<span class="property">config</span>, name) <span class="comment">// 返回具体配置项结果</span></span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">config</span> <span class="comment">// 否则就返回完整配置</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里用到了<code>lodash</code>的<code>get</code>方法，主要是为了方便获取如下情况：</p><p>比如配置内容长这样：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;a&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;b&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>往常我们要获取<code>a.b</code>需要：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> b = <span class="variable language_">this</span>.<span class="property">config</span>.<span class="property">a</span>.<span class="property">b</span></span><br></pre></td></tr></table></figure><p>万一遇到<code>a</code>不存在的时候，那么上面那句话就会报错了。因为<code>a</code>不存在，那么<code>a.b</code>就是<code>undefined.b</code>自然会报错了。而用<code>lodash</code>的<code>get</code>方法则可以避免这个问题，并且可以很方便的获取：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> b = <span class="title function_">get</span>(<span class="variable language_">this</span>.<span class="property">config</span>, <span class="string">&#x27;a.b&#x27;</span>)</span><br></pre></td></tr></table></figure><p>如果<code>a</code>不存在，那么获取到的结果<code>b</code>也不会报错，而是<code>undefined</code>。</p><h4 id="写入配置"><a href="#写入配置" class="headerlink" title="写入配置"></a>写入配置</h4><p>有了上面的铺垫，写入内容也很简单。通过<code>lowdb</code>提供的接口，写入配置如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> saveConfig = (<span class="attr">configPath</span>: <span class="built_in">string</span>, <span class="attr">config</span>: <span class="built_in">any</span>): <span class="function"><span class="params">void</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> db = <span class="title function_">initConfig</span>(configPath)</span><br><span class="line">  <span class="title class_">Object</span>.<span class="title function_">keys</span>(config).<span class="title function_">forEach</span>(<span class="function">(<span class="params"><span class="attr">name</span>: <span class="built_in">string</span></span>) =&gt;</span> &#123;</span><br><span class="line">    db.<span class="title function_">read</span>().<span class="title function_">set</span>(name, config[name]).<span class="title function_">write</span>()</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们可以用：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">saveConfig</span>(<span class="variable language_">this</span>.<span class="property">configPath</span>, &#123; <span class="attr">a</span>: &#123; <span class="attr">b</span>: <span class="literal">true</span> &#125; &#125;)</span><br></pre></td></tr></table></figure><p>或者：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">saveConfig</span>(<span class="variable language_">this</span>.<span class="property">configPath</span>, &#123; <span class="string">&#x27;a.b&#x27;</span>: <span class="literal">true</span> &#125;)</span><br></pre></td></tr></table></figure><p>上面两种写法都会生成如下配置：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;a&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;b&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>可以看到明显后者更简洁点。这多亏了lowdb里由lodash提供的<code>set</code>方法。</p><p>至此我们已经将配置文件相关的操作实现完了。其实可以把这堆操作封装成一个类的，PicGo-Core在一开始实现的时候觉得东西不多不复杂，所以只是抽成了一个小工具来调用的。当然这个不是关键，关键在于实现了配置文件的相关操作后，你的系统和这个系统的插件都能因此受益。系统可以把跟配置文件相关的操作的API暴露给插件使用。接下去我们一步步来完善这个插件系统。</p><h3 id="插件操作类"><a href="#插件操作类" class="headerlink" title="插件操作类"></a>插件操作类</h3><p>暂时没想好这个类要取的名字是啥，代码里我写的是<code>pluginHandler</code>，那么就叫它插件操作类吧。这个类主要目的就三个：</p><ol><li>通过<code>npm</code>安装插件 —— install</li><li>通过<code>npm</code>卸载插件 —— uninstall</li><li>通过<code>npm</code>更新插件 —— update</li></ol><p>用<code>npm</code>来分发插件，这是大多数Node.js插件系统会选择的解决方案。毕竟在没有自己的插件商店（比如VSCode）的基础上，<code>npm</code>就是一个天然的「插件商店」。当然发布到<code>npm</code>之上好处还有很多，比如可以十分方便地来对插件进行安装、更新和卸载，比如对Node.js用户来说是0成本的上手。这也是<code>pluginHandler</code>这个类要做的事。</p><blockquote><p><code>pluginHandler</code>相关的实现思路来自<a href="https://github.com/feflow/feflow">feflow</a>，特此感谢。</p></blockquote><p>平时我们安装一个npm模块的时候，很简单：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install xxxx --save</span><br></pre></td></tr></table></figure><p>不过我们是在当前项目目录的上来安装的。PicGo由于引入了配置文件，所以我们可以直接在配置文件所在的目录里进行插件的安装，这样如果你要卸载PicGo，只要把。但是每次都让用户打开PicGo的配置文件所在的路径去安装插件未免太累了。这样也不优雅。</p><p>相对的，如果我们全局安装了<code>picgo</code>之后，在文件系统任何一个角落里只需要通过<code>picgo install xxx</code>就能安装一个<code>picgo</code>的插件，而不需要定位到PicGo的配置文件所在的文件夹，这样用户体验会好不少。这里大家可以类比<code>vue-cli3</code>安装插件的步骤。</p><p>为了实现这个效果，我们需要通过代码的方式去调用<code>npm</code>这个命令。那么Node.js要如何通过代码去实现命令行调用呢？</p><p>这里我们可以使用<a href="https://github.com/moxystudio/node-cross-spawn">cross-spawn</a>来实现跨平台的、通过代码来调用命令行的目的。</p><p><code>spawn</code>这个方法Node.js原生也有（在child_process里），不过<code>cross-spawn</code>解决了一些跨平台的问题。使用上是一样的。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> spawn = <span class="built_in">require</span>(<span class="string">&#x27;cross-spawn&#x27;</span>)</span><br><span class="line"><span class="title function_">spawn</span>(<span class="string">&#x27;npm&#x27;</span>, [<span class="string">&#x27;install&#x27;</span>, <span class="string">&#x27;@vue/cli&#x27;</span>, <span class="string">&#x27;-g&#x27;</span>])</span><br></pre></td></tr></table></figure><p>可以看到，它的参数是以数组的形式传入的。</p><p>而我们要实现的插件操作，除了主要命令<code>install</code>、<code>update</code>、<code>uninstall</code>不一样之外，其他的参数都是一样的。所以我们抽离出一个<code>execCommand</code>的方法来实现它们背后的公共逻辑：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">execCommand</span> (<span class="attr">cmd</span>: <span class="built_in">string</span>, <span class="attr">modules</span>: <span class="built_in">string</span>[], <span class="attr">where</span>: <span class="built_in">string</span>, <span class="attr">proxy</span>: <span class="built_in">string</span> = <span class="string">&#x27;&#x27;</span>): <span class="title class_">Promise</span>&lt;<span class="title class_">Result</span>&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>((<span class="attr">resolve</span>: <span class="built_in">any</span>, <span class="attr">reject</span>: <span class="built_in">any</span>): <span class="function"><span class="params">void</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// spawn的命令行参数是以数组形式传入</span></span><br><span class="line">    <span class="comment">// 此处将命令和要安装的插件以数组的形式拼接起来</span></span><br><span class="line">    <span class="comment">// 此处的cmd指的是执行的命令，比如install\uninstall\update</span></span><br><span class="line">    <span class="keyword">let</span> args = [cmd].<span class="title function_">concat</span>(modules).<span class="title function_">concat</span>(<span class="string">&#x27;--color=always&#x27;</span>).<span class="title function_">concat</span>(<span class="string">&#x27;--save&#x27;</span>)</span><br><span class="line">    <span class="keyword">const</span> npm = <span class="title function_">spawn</span>(<span class="string">&#x27;npm&#x27;</span>, args, &#123; <span class="attr">cwd</span>: where &#125;) <span class="comment">// 执行npm，并通过 cwd指定执行的路径——配置文件所在文件夹</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> output = <span class="string">&#x27;&#x27;</span></span><br><span class="line">    npm.<span class="property">stdout</span>.<span class="title function_">on</span>(<span class="string">&#x27;data&#x27;</span>, <span class="function">(<span class="params"><span class="attr">data</span>: <span class="built_in">string</span></span>) =&gt;</span> &#123;</span><br><span class="line">      output += data <span class="comment">// 获取输出日志</span></span><br><span class="line">    &#125;).<span class="title function_">pipe</span>(process.<span class="property">stdout</span>)</span><br><span class="line"></span><br><span class="line">    npm.<span class="property">stderr</span>.<span class="title function_">on</span>(<span class="string">&#x27;data&#x27;</span>, <span class="function">(<span class="params"><span class="attr">data</span>: <span class="built_in">string</span></span>) =&gt;</span> &#123;</span><br><span class="line">      output += data <span class="comment">// 获取报错日志</span></span><br><span class="line">    &#125;).<span class="title function_">pipe</span>(process.<span class="property">stderr</span>)</span><br><span class="line"></span><br><span class="line">    npm.<span class="title function_">on</span>(<span class="string">&#x27;close&#x27;</span>, <span class="function">(<span class="params"><span class="attr">code</span>: <span class="built_in">number</span></span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (!code) &#123;</span><br><span class="line">        <span class="title function_">resolve</span>(&#123; <span class="attr">code</span>: <span class="number">0</span>, <span class="attr">data</span>: output &#125;) <span class="comment">// 如果没有报错就输出正常日志</span></span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="title function_">reject</span>(&#123; <span class="attr">code</span>: code, <span class="attr">data</span>: output &#125;) <span class="comment">// 如果报错就输出报错日志</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>关键的部分基本都已经在代码里给出了注释。当然这里还是有一些需要注意的地方。注意这句话：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> npm = <span class="title function_">spawn</span>(<span class="string">&#x27;npm&#x27;</span>, args, &#123; <span class="attr">cwd</span>: where &#125;) <span class="comment">// 执行npm，并通过 cwd指定执行的路径——配置文件所在文件夹</span></span><br></pre></td></tr></table></figure><p>里面的<code>{cwd: where}</code>，这个<code>where</code>是会从外部传进来的值，表示这个<code>npm</code>命令会在哪个目录下执行。这个也是我们要做这个插件操作类最关键的地方——不用让用户主动打开配置文件所在目录去安装插件，在系统任何地方都可以轻松安装PicGo的插件。</p><p>接下去我们实现一下<code>install</code>方法，这样另外两个就可以类推了。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="title function_">install</span> (<span class="attr">plugins</span>: <span class="built_in">string</span>[], <span class="attr">proxy</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">  plugins = plugins.<span class="title function_">map</span>(<span class="function">(<span class="params"><span class="attr">item</span>: <span class="built_in">string</span></span>) =&gt;</span> <span class="string">&#x27;picgo-plugin-&#x27;</span> + item)</span><br><span class="line">   <span class="keyword">const</span> result = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="title function_">execCommand</span>(<span class="string">&#x27;install&#x27;</span>, plugins, <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">baseDir</span>, proxy)</span><br><span class="line">   <span class="keyword">if</span> (!result.<span class="property">code</span>) &#123;</span><br><span class="line">     <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">log</span>.<span class="title function_">success</span>(<span class="string">&#x27;插件安装成功&#x27;</span>)</span><br><span class="line">     <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="title function_">emit</span>(<span class="string">&#x27;installSuccess&#x27;</span>, &#123;</span><br><span class="line">       <span class="attr">title</span>: <span class="string">&#x27;插件安装成功&#x27;</span>,</span><br><span class="line">       <span class="attr">body</span>: plugins</span><br><span class="line">     &#125;)</span><br><span class="line">   &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">     <span class="keyword">const</span> err = <span class="string">`插件安装失败，失败码为<span class="subst">$&#123;result.code&#125;</span>，错误日志为<span class="subst">$&#123;result.data&#125;</span>`</span></span><br><span class="line">     <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">log</span>.<span class="title function_">error</span>(err)</span><br><span class="line">     <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="title function_">emit</span>(<span class="string">&#x27;failed&#x27;</span>, &#123;</span><br><span class="line">       <span class="attr">title</span>: <span class="string">&#x27;插件安装失败&#x27;</span>,</span><br><span class="line">       <span class="attr">body</span>: err</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>别看代码很多，关键就一句<code>const result = await this.execCommand(&#39;install&#39;, plugins, this.ctx.baseDir, proxy)</code>，剩下的都是日志输出而已。好了，插件也安装完了，如何加载呢？</p><h3 id="插件加载类"><a href="#插件加载类" class="headerlink" title="插件加载类"></a>插件加载类</h3><p>上面说了，我们会将插件安装在配置文件所在目录里。值得注意的是，由于<code>npm</code>的特点，如果目录里有个叫做<code>package.json</code>的文件，那么安装插件、更新插件等操作会同时修改<code>package.json</code>文件。因此我们可以通过读取<code>package.json</code>文件来得知当前目录下有什么PicGo的插件。这也是Hexo的插件加载机制里的很重要的一环。</p><blockquote><p><code>pluginLoader</code>相关的实现思路来自<a href="https://github.com/hexojs/hexo">hexo</a>，特此感谢。</p></blockquote><p>关于插件的命名，PicGo这里有个约束（这也是很多插件系统选择的方式），必须以<code>picgo-plugin-</code>开头。这样才能方便插件加载类识别它们。</p><p>这里有一个小坑。如果我们配置文件所在的目录里没有<code>package.json</code>的话，那么执行安装插件的命令会有报错信息。但是我们不想让用户看到这个报错，于是在初始化<code>插件加载类</code>的时候，需要判断一下这个文件存不存在，如果不存在那么我们就要创建一个：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">PluginLoader</span> &#123;</span><br><span class="line">  <span class="attr">ctx</span>: <span class="title class_">PicGo</span></span><br><span class="line">  <span class="attr">list</span>: <span class="built_in">string</span>[]</span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"><span class="attr">ctx</span>: <span class="title class_">PicGo</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">ctx</span> = ctx</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">list</span> = [] <span class="comment">// 插件列表</span></span><br><span class="line">    <span class="variable language_">this</span>.<span class="title function_">init</span>()</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title function_">init</span> (): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> packagePath = path.<span class="title function_">join</span>(<span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">baseDir</span>, <span class="string">&#x27;package.json&#x27;</span>)</span><br><span class="line">    <span class="keyword">if</span> (!fs.<span class="title function_">existsSync</span>(packagePath)) &#123; <span class="comment">// 如果不存在</span></span><br><span class="line">      <span class="keyword">const</span> pkg = &#123;</span><br><span class="line">        <span class="attr">name</span>: <span class="string">&#x27;picgo-plugins&#x27;</span>,</span><br><span class="line">        <span class="attr">description</span>: <span class="string">&#x27;picgo-plugins&#x27;</span>,</span><br><span class="line">        <span class="attr">repository</span>: <span class="string">&#x27;https://github.com/Molunerfinn/PicGo-Core&#x27;</span>,</span><br><span class="line">        <span class="attr">license</span>: <span class="string">&#x27;MIT&#x27;</span></span><br><span class="line">      &#125;</span><br><span class="line">      fs.<span class="title function_">writeFileSync</span>(packagePath, <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(pkg), <span class="string">&#x27;utf8&#x27;</span>) <span class="comment">// 创建这个文件</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来我们要实现最关键的<code>load</code>方法了。我们需要如下步骤：</p><ol><li>先通过<code>package.json</code>来找到所有合法的插件</li><li>通过<code>require</code>来加载插件</li><li>通过维护<code>picgoPlugins</code>配置来判断插件是否被禁用</li><li>通过执行未被禁用的插件暴露的<code>register</code>方法来实现插件注册</li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">PicGo</span> <span class="keyword">from</span> <span class="string">&#x27;../core/PicGo&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="keyword">import</span> resolve <span class="keyword">from</span> <span class="string">&#x27;resolve&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="title function_">load</span> (): <span class="built_in">void</span> | <span class="built_in">boolean</span> &#123;</span><br><span class="line">  <span class="keyword">const</span> packagePath = path.<span class="title function_">join</span>(<span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">baseDir</span>, <span class="string">&#x27;package.json&#x27;</span>)</span><br><span class="line">  <span class="keyword">const</span> pluginDir = path.<span class="title function_">join</span>(<span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">baseDir</span>, <span class="string">&#x27;node_modules/&#x27;</span>)</span><br><span class="line">    <span class="comment">// Thanks to hexo -&gt; https://github.com/hexojs/hexo/blob/master/lib/hexo/load_plugins.js</span></span><br><span class="line">  <span class="keyword">if</span> (!fs.<span class="title function_">existsSync</span>(pluginDir)) &#123; <span class="comment">// 如果插件文件夹不存在，返回false</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> json = fs.<span class="title function_">readJSONSync</span>(packagePath) <span class="comment">// 读取package.json</span></span><br><span class="line">  <span class="keyword">const</span> deps = <span class="title class_">Object</span>.<span class="title function_">keys</span>(json.<span class="property">dependencies</span> || &#123;&#125;)</span><br><span class="line">  <span class="keyword">const</span> devDeps = <span class="title class_">Object</span>.<span class="title function_">keys</span>(json.<span class="property">devDependencies</span> || &#123;&#125;)</span><br><span class="line">  <span class="comment">// 1.获取插件列表</span></span><br><span class="line">  <span class="keyword">const</span> modules = deps.<span class="title function_">concat</span>(devDeps).<span class="title function_">filter</span>(<span class="function">(<span class="params"><span class="attr">name</span>: <span class="built_in">string</span></span>) =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="regexp">/^picgo-plugin-|^@[^/]+\/picgo-plugin-/</span>.<span class="title function_">test</span>(name)) <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    <span class="keyword">const</span> path = <span class="variable language_">this</span>.<span class="title function_">resolvePlugin</span>(<span class="variable language_">this</span>.<span class="property">ctx</span>, name) <span class="comment">// 获取插件路径</span></span><br><span class="line">    <span class="keyword">return</span> fs.<span class="title function_">existsSync</span>(path)</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">for</span> (<span class="keyword">let</span> i <span class="keyword">in</span> modules) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">list</span>.<span class="title function_">push</span>(modules[i]) <span class="comment">// 把插件push进插件列表</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">config</span>.<span class="property">picgoPlugins</span>[modules[i]] || <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">config</span>.<span class="property">picgoPlugins</span>[modules[i]] === <span class="literal">undefined</span>) &#123; <span class="comment">// 3.判断插件是否被禁用，如果是undefined则为新安装的插件，默认不禁用</span></span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="title function_">getPlugin</span>(modules[i]).<span class="title function_">register</span>() <span class="comment">// 4.调用插件的`register`方法进行注册</span></span><br><span class="line">        <span class="keyword">const</span> plugin = <span class="string">`picgoPlugins[<span class="subst">$&#123;modules[i]&#125;</span>]`</span></span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="title function_">saveConfig</span>( <span class="comment">// 将插件设为启用--&gt;让新安装的插件的值从undefined变成true</span></span><br><span class="line">          &#123;</span><br><span class="line">            [plugin]: <span class="literal">true</span></span><br><span class="line">          &#125;</span><br><span class="line">        )</span><br><span class="line">      &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">log</span>.<span class="title function_">error</span>(e)</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="title function_">emit</span>(<span class="string">&#x27;notification&#x27;</span>, &#123;</span><br><span class="line">          <span class="attr">title</span>: <span class="string">`Plugin <span class="subst">$&#123;modules[i]&#125;</span> Load Error`</span>,</span><br><span class="line">          <span class="attr">body</span>: e</span><br><span class="line">        &#125;)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="title function_">resolvePlugin</span> (<span class="attr">ctx</span>: <span class="title class_">PicGo</span>, <span class="attr">name</span>: <span class="built_in">string</span>): <span class="built_in">string</span> &#123; <span class="comment">// 获取插件路径</span></span><br><span class="line">  <span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> resolve.<span class="title function_">sync</span>(name, &#123; <span class="attr">basedir</span>: ctx.<span class="property">baseDir</span> &#125;)</span><br><span class="line">  &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">    <span class="keyword">return</span> path.<span class="title function_">join</span>(ctx.<span class="property">baseDir</span>, <span class="string">&#x27;node_modules&#x27;</span>, name)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="title function_">getPlugin</span> (<span class="attr">name</span>: <span class="built_in">string</span>): <span class="built_in">any</span> &#123; <span class="comment">// 通过插件名获取插件</span></span><br><span class="line">  <span class="keyword">const</span> pluginDir = path.<span class="title function_">join</span>(<span class="variable language_">this</span>.<span class="property">ctx</span>.<span class="property">baseDir</span>, <span class="string">&#x27;node_modules/&#x27;</span>)</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">require</span>(pluginDir + name)(<span class="variable language_">this</span>.<span class="property">ctx</span>) <span class="comment">// 2.通过require获取插件并传入ctx</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>load</code>这个方法是整个插件系统加载的最关键的部分。光看上面的步骤和代码可能没办法很好理解。我们下面用一个具体的插件例子来说明。</p><p>假设我写了一个<code>picgo-plugin-xxx</code>的插件。我的代码如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 插件系统会传入picgo的ctx，方便插件调用picgo暴露出来的api</span></span><br><span class="line"><span class="comment">// 所以我们需要有一个ctx的参数用于接收来自picgo的api</span></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function"><span class="params">ctx</span> =&gt;</span> &#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 插件系统会调用这个方法来进行插件的注册</span></span><br><span class="line">  <span class="keyword">const</span> <span class="title function_">register</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">    ctx.<span class="property">helper</span>.<span class="property">beforeTransformPlugins</span>.<span class="title function_">register</span>(<span class="string">&#x27;xxx&#x27;</span>, &#123;</span><br><span class="line">      <span class="title function_">handle</span> (ctx) &#123; <span class="comment">// 调用插件的 handle 方法时也会传入 ctx 方便调用api</span></span><br><span class="line">        <span class="variable language_">console</span>.<span class="title function_">log</span>(ctx.<span class="property">output</span>)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    register</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们从前文已经大概知道插件运行流程：</p><ol><li>首先运行生命周期</li><li>当运行到某个生命周期，比如这里的<code>beforeTransform</code>，那么这个阶段就去获取<code>beforeTransformPlugins</code>这些插件</li><li><code>beforeTransformPlugins</code>这些插件由<code>ctx.helper.beforeTransformPlugins.register</code>方法注册，并可以通过<code>ctx.helper.beforeTransformPlugins.getList()</code>获取</li><li>拿到插件之后将调用每个<code>beforeTransformPlugins</code>的<code>handle</code>方法，并传入<code>ctx</code>供插件使用</li></ol><p>注意上面的第三步，<code>ctx.helper.beforeTransformPlugins.register</code>这个方法是在什么时候被调用的？答案就是在本小节介绍的插件的加载阶段，<code>pluginLoader</code>调用了每个插件的<code>register</code>方法，那么在插件的<code>register</code>方法里，我们写了：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ctx.<span class="property">helper</span>.<span class="property">beforeTransformPlugins</span>.<span class="title function_">register</span>(<span class="string">&#x27;xxx&#x27;</span>, &#123;</span><br><span class="line">  <span class="title function_">handle</span> (ctx) &#123; <span class="comment">// 调用插件的 handle 方法时也会传入 ctx 方便调用api</span></span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(ctx.<span class="property">output</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>也就是在这个时候，<code>ctx.helper.beforeTransformPlugins.register</code>这个方法被调用。</p><p>于是乎，在生命周期开始之前，整个插件以及每个生命周期的插件已经预先被注册了。所以在生命周期开始运作的时候，只需要通过<code>getList()</code>就可以获取注册过的插件，从而执行整个流程了。</p><p>也因此，我以前在跑<code>Hexo</code>生成博客的时候曾经遇到的问题就得到解释了。我以前安装过一些<code>Hexo</code>的插件，但是不知道为什么总是无法生效。后来发现是安装的时候没有使用<code>--save</code>，导致它们没被写入<code>package.json</code>的依赖字段。而<code>Hexo</code>加载插件的第一步就是从<code>package.json</code>里获取合法的插件列表，如果插件不在<code>package.json</code>里，哪怕在<code>node_modules</code>里有，也不会生效了。</p><p>有了插件，接下去我们讲讲如何在命令行调用和配置了。</p><h3 id="命令行操作类"><a href="#命令行操作类" class="headerlink" title="命令行操作类"></a>命令行操作类</h3><p>PicGo的命令行操作类主要依赖于两个库：<a href="https://github.com/tj/commander.js/">commander.js</a>和<a href="https://github.com/SBoudrias/Inquirer.js/">Inquirer.js</a>。这两个也是做Node.js命令行应用很常用的库了。前者负责命令行解析、执行相关命令。后者负责提供与用户交互的命令行界面。</p><p>比如你可以输入：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">picgo use uploader</span><br></pre></td></tr></table></figure><p>这个时候由<code>commander.js</code>去解析这句命令，告诉我们这个时候调用的是<code>use</code>这个命令，参数是<code>uploader</code>，那么就进入<code>Inquirer.js</code>提供的交互式界面了：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/5c529491e27e4.png" alt="Inquirer.js"></p><p>如果你用过诸如<code>vue-cli3</code>或者<code>create-react-app</code>等类似的命令行工具一定类似的情况很熟悉。</p><p>首先我们写一个命令行操作类，用于暴露api给其他部分注册命令，此处源码可以参考<a href="https://github.com/PicGo/PicGo-Core/blob/dev/src/lib/Commander.ts">Commander.ts</a>。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">PicGo</span> <span class="keyword">from</span> <span class="string">&#x27;../core/PicGo&#x27;</span></span><br><span class="line"><span class="keyword">import</span> program <span class="keyword">from</span> <span class="string">&#x27;commander&#x27;</span></span><br><span class="line"><span class="keyword">import</span> inquirer <span class="keyword">from</span> <span class="string">&#x27;inquirer&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Plugin</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../utils/interfaces&#x27;</span></span><br><span class="line"><span class="keyword">const</span> pkg = <span class="built_in">require</span>(<span class="string">&#x27;../../package.json&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Commander</span> &#123;</span><br><span class="line">  <span class="attr">list</span>: &#123;</span><br><span class="line">    [<span class="attr">propName</span>: <span class="built_in">string</span>]: <span class="title class_">Plugin</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="attr">program</span>: <span class="keyword">typeof</span> program</span><br><span class="line">  <span class="attr">inquirer</span>: <span class="keyword">typeof</span> inquirer</span><br><span class="line">  <span class="keyword">private</span> <span class="attr">ctx</span>: <span class="title class_">PicGo</span></span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"><span class="attr">ctx</span>: <span class="title class_">PicGo</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">list</span> = &#123;&#125;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">program</span> = program</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">inquirer</span> = inquirer</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">ctx</span> = ctx</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="title class_">Commander</span></span><br></pre></td></tr></table></figure><p>然后我们在PicGo-Core的核心类里将其实例化：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">Commander</span> <span class="keyword">from</span> <span class="string">&#x27;../lib/Commander&#x27;</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PicGo</span> <span class="keyword">extends</span> <span class="title class_ inherited__">EventEmitter</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="attr">cmd</span>: <span class="title class_">Commander</span></span><br><span class="line"></span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"><span class="attr">configPath</span>: <span class="built_in">string</span> = <span class="string">&#x27;&#x27;</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>()</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">cmd</span> = <span class="keyword">new</span> <span class="title class_">Commander</span>(<span class="variable language_">this</span>)</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>这样其他部分就可以使用<code>ctx.cmd.program</code>来调用<code>commander.js</code>以及使用<code>ctx.cmd.inquirer</code>来调用<code>Inquirer.js</code>了。</p><p>这两个库的使用，网络上有很多教程了。此处简单举个例子，我们从PicGo最基本的功能——命令行上传图片开始说起。</p><h4 id="命令的注册"><a href="#命令的注册" class="headerlink" title="命令的注册"></a>命令的注册</h4><p>为了与之前的插件结构统一，我们把命令注册也写到<code>handle</code>函数里。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">PicGo</span> <span class="keyword">from</span> <span class="string">&#x27;../../core/PicGo&#x27;</span></span><br><span class="line"><span class="keyword">import</span> path <span class="keyword">from</span> <span class="string">&#x27;path&#x27;</span></span><br><span class="line"><span class="keyword">import</span> fs <span class="keyword">from</span> <span class="string">&#x27;fs-extra&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">handle</span>: (<span class="attr">ctx</span>: <span class="title class_">PicGo</span>): <span class="function"><span class="params">void</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> cmd = ctx.<span class="property">cmd</span></span><br><span class="line">    cmd.<span class="property">program</span> <span class="comment">// 此处是一个commander.js实例</span></span><br><span class="line">      .<span class="title function_">command</span>(<span class="string">&#x27;upload&#x27;</span>) <span class="comment">// 注册命令 upload</span></span><br><span class="line">      .<span class="title function_">description</span>(<span class="string">&#x27;upload, go go go&#x27;</span>) <span class="comment">// 命令的描述</span></span><br><span class="line">      .<span class="title function_">arguments</span>(<span class="string">&#x27;[input...]&#x27;</span>) <span class="comment">// 命令的参数</span></span><br><span class="line">      .<span class="title function_">alias</span>(<span class="string">&#x27;u&#x27;</span>) <span class="comment">// 命令的别名 u</span></span><br><span class="line">      .<span class="title function_">action</span>(<span class="title function_">async</span> (<span class="attr">input</span>: <span class="built_in">string</span>[]) =&gt; &#123; <span class="comment">// 命令执行的函数</span></span><br><span class="line">        <span class="keyword">const</span> inputList = input <span class="comment">// 获取输入的input</span></span><br><span class="line">            .<span class="title function_">map</span>(<span class="function">(<span class="params"><span class="attr">item</span>: <span class="built_in">string</span></span>) =&gt;</span> path.<span class="title function_">resolve</span>(item))</span><br><span class="line">            .<span class="title function_">filter</span>(<span class="function">(<span class="params"><span class="attr">item</span>: <span class="built_in">string</span></span>) =&gt;</span> &#123;</span><br><span class="line">              <span class="keyword">const</span> exist = fs.<span class="title function_">existsSync</span>(item) <span class="comment">// 判断输入的地址存不存在</span></span><br><span class="line">              <span class="keyword">if</span> (!exist) &#123;</span><br><span class="line">                ctx.<span class="property">log</span>.<span class="title function_">warn</span>(<span class="string">`<span class="subst">$&#123;item&#125;</span> is not existed.`</span>) <span class="comment">// 如果不存在就返回警告信息</span></span><br><span class="line">              &#125;</span><br><span class="line">              <span class="keyword">return</span> exist</span><br><span class="line">            &#125;)</span><br><span class="line">        <span class="keyword">await</span> ctx.<span class="title function_">upload</span>(inputList) <span class="comment">// 上传图片（调用生命周期的start函数）</span></span><br><span class="line">      &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样我们如果通过某种方式把命令注册进去：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">PicGo</span> <span class="keyword">from</span> <span class="string">&#x27;../../core/PicGo&#x27;</span></span><br><span class="line"><span class="keyword">import</span> upload <span class="keyword">from</span> <span class="string">&#x27;./upload&#x27;</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="title function_">default</span> (<span class="attr">ctx</span>: <span class="title class_">PicGo</span>): <span class="function"><span class="params">void</span> =&gt;</span> &#123;</span><br><span class="line">  ctx.<span class="property">cmd</span>.<span class="title function_">register</span>(<span class="string">&#x27;upload&#x27;</span>, upload) <span class="comment">// 此处的注册逻辑跟lifecyclePlugins一致。</span></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当代码写到这里，可能大家觉得已经大功告成了。实际上还差了最后一步，我们缺少一个入口来接纳我们输入的命令。就比如现在我们写完了命令，也写完了命令的注册，然后我们要怎么在命令行里使用呢？</p><h4 id="命令行的使用"><a href="#命令行的使用" class="headerlink" title="命令行的使用"></a>命令行的使用</h4><p>这个时候要简单说下<code>package.json</code>里的两个字段<code>bin</code>和<code>main</code>。其中<code>main</code>字段指向的文件，是你<code>const xxx = require(&#39;xxx&#39;)</code>的时候拿到的东西。而<code>bin</code>字段指向的文件，就是你在全局安装了之后，可以在命令行里直接输入的命令。</p><p>举个例子，PicGo-Core的<code>bin</code>字段如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="attr">&quot;bin&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;picgo&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./bin/picgo&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><p>那么用户如果全局安装了picgo，就可以通过<code>picgo</code>这个命令来使用picgo了。类似安装<code>@vue/cli</code>之后，可以使用<code>vue</code>这个命令一样。</p><p>那么我们来看看<code>./bin/picgo</code>做了啥。源码在<a href="https://github.com/PicGo/PicGo-Core/blob/dev/bin/picgo">这里</a>。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env node</span></span><br><span class="line"><span class="keyword">const</span> path = <span class="built_in">require</span>(<span class="string">&#x27;path&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> minimist = <span class="built_in">require</span>(<span class="string">&#x27;minimist&#x27;</span>)</span><br><span class="line"><span class="keyword">let</span> argv = <span class="title function_">minimist</span>(process.<span class="property">argv</span>.<span class="title function_">slice</span>(<span class="number">2</span>)) <span class="comment">// 解析命令行</span></span><br><span class="line"><span class="keyword">let</span> configPath = argv.<span class="property">c</span> || argv.<span class="property">config</span> || <span class="string">&#x27;&#x27;</span> <span class="comment">// 查看是否提供了configPath</span></span><br><span class="line"><span class="keyword">if</span> (configPath !== <span class="literal">true</span> &amp;&amp; configPath !== <span class="string">&#x27;&#x27;</span>) &#123;</span><br><span class="line">  configPath = path.<span class="title function_">resolve</span>(configPath)</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">  configPath = <span class="string">&#x27;&#x27;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">PicGo</span> = <span class="built_in">require</span>(<span class="string">&#x27;../dist/index&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> picgo = <span class="keyword">new</span> <span class="title class_">PicGo</span>(configPath) <span class="comment">// 实例化picgo</span></span><br><span class="line">picgo.<span class="title function_">registerCommands</span>() <span class="comment">// 注册命令</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">  picgo.<span class="property">cmd</span>.<span class="property">program</span>.<span class="title function_">parse</span>(process.<span class="property">argv</span>) <span class="comment">// 调用commander.js解析命令</span></span><br><span class="line">&#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">  picgo.<span class="property">log</span>.<span class="title function_">error</span>(e)</span><br><span class="line">  <span class="keyword">if</span> (process.<span class="property">argv</span>.<span class="title function_">includes</span>(<span class="string">&#x27;--debug&#x27;</span>)) &#123;</span><br><span class="line">    <span class="title class_">Promise</span>.<span class="title function_">reject</span>(e)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>关键部分就在<code>picgo.cmd.program.parse(process.argv)</code>这句话，这句话调用了<code>commander.js</code>来解析<code>process.argv</code>，也就是命令行里命令以及参数。</p><p>那么我们在开发阶段就可以用<code>./bin/picgo upload</code>这样来调用命令，而在生产环境下，也就是用户全局安装后，就可以通过<code>picgo upload</code>这样来调用命令了。</p><h4 id="配置项的处理"><a href="#配置项的处理" class="headerlink" title="配置项的处理"></a>配置项的处理</h4><p>前文提到了，配置项是插件系统里很重要的一个组成部分。不同插件系统的配置项处理不太一样。比如<code>Hexo</code>提供了<code>_config.yml</code>供用户配置，<code>vue-cli3</code>提供了<code>vue.config.js</code>供用户配置。PicGo也提供了<code>config.json</code>供用户配置，不过在此基础上，我想提供一个更方便的方式来让用户直接在命令行里完成配置，而不需要专门打开这个配置文件。</p><p>比如我们可以通过命令行来选择当前上传的图床是什么：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$ picgo use</span><br><span class="line">? Use an uploader (Use arrow keys)</span><br><span class="line">  smms</span><br><span class="line">❯ tcyun</span><br><span class="line">  weibo</span><br><span class="line">  github</span><br><span class="line">  qiniu</span><br><span class="line">  imgur</span><br><span class="line">  aliyun</span><br><span class="line">(Move up and down to reveal more choices)</span><br></pre></td></tr></table></figure><p>这种在命令行里的交互，需要之前提到的<code>Inquirer.js</code>来辅助我们达到这个效果。</p><p>它的用法也很简单，传入一个<code>prompts</code>（可以理解为一个问题数组），然后它会将问题的结果再以对象的形式返回出来，我们通常将这个结果记为<code>answer</code>。</p><p>而PicGo为了简化这个过程，只需要插件提供一个<code>config</code>方法，这个方法只需返回一个合法的<code>prompts</code>问题数组，然后PicGo会自动调用<code>Inquirer.js</code>去执行它，并自动将结果写入配置文件里。</p><p>举个例子，PicGo内置的<code>Imgur</code>图床的<code>config</code>代码如下：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> config = (<span class="attr">ctx</span>: <span class="title class_">PicGo</span>): <span class="title class_">PluginConfig</span>[] =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> userConfig = ctx.<span class="title function_">getConfig</span>(<span class="string">&#x27;picBed.imgur&#x27;</span>)</span><br><span class="line">  <span class="keyword">if</span> (!userConfig) &#123;</span><br><span class="line">    userConfig = &#123;&#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">const</span> config = [</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;clientId&#x27;</span>,</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;input&#x27;</span>,</span><br><span class="line">      <span class="attr">default</span>: userConfig.<span class="property">clientId</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">      <span class="attr">required</span>: <span class="literal">true</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;proxy&#x27;</span>,</span><br><span class="line">      <span class="attr">type</span>: <span class="string">&#x27;input&#x27;</span>,</span><br><span class="line">      <span class="attr">default</span>: userConfig.<span class="property">proxy</span> || <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">      <span class="attr">required</span>: <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">  <span class="keyword">return</span> config <span class="comment">// 这个config就是一个合法的prompts数组</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  config</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后我们用代码实现能够在命令行里调用它，源码<a href="https://github.com/PicGo/PicGo-Core/blob/dev/src/plugins/commander/setting.ts">传送门</a>：</p><blockquote><p>以下代码有所精简</p></blockquote><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">PicGo</span> <span class="keyword">from</span> <span class="string">&#x27;../../core/PicGo&#x27;</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">PluginConfig</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;../../utils/interfaces&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 处理uploader的config数组，然后写入配置文件</span></span><br><span class="line"><span class="keyword">const</span> handleConfig = <span class="title function_">async</span> (<span class="attr">ctx</span>: <span class="title class_">PicGo</span>, <span class="attr">prompts</span>: <span class="title class_">PluginConfig</span>, <span class="attr">name</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> answer = <span class="keyword">await</span> ctx.<span class="property">cmd</span>.<span class="property">inquirer</span>.<span class="title function_">prompt</span>(prompts)</span><br><span class="line">  <span class="keyword">let</span> configName = <span class="string">`picBed.<span class="subst">$&#123;name&#125;</span>`</span></span><br><span class="line">  ctx.<span class="title function_">saveConfig</span>(&#123;</span><br><span class="line">    [configName]: answer</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">handle</span>: (<span class="attr">ctx</span>: <span class="title class_">PicGo</span>): <span class="function"><span class="params">void</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="attr">cmd</span>: <span class="keyword">typeof</span> ctx.<span class="property">cmd</span> = ctx.<span class="property">cmd</span></span><br><span class="line">    cmd.<span class="property">program</span></span><br><span class="line">      .<span class="title function_">command</span>(<span class="string">&#x27;set&#x27;</span>) <span class="comment">// 注册一个set命令</span></span><br><span class="line">      .<span class="title function_">alias</span>(<span class="string">&#x27;config&#x27;</span>) <span class="comment">// 别名 config</span></span><br><span class="line">      .<span class="title function_">description</span>(<span class="string">&#x27;configure config of picgo&#x27;</span>)</span><br><span class="line">      .<span class="title function_">action</span>(<span class="title function_">async</span> () =&gt; &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">          <span class="keyword">let</span> prompts = [ <span class="comment">// prompts问题数组</span></span><br><span class="line">            &#123;</span><br><span class="line">              <span class="attr">type</span>: <span class="string">&#x27;list&#x27;</span>,</span><br><span class="line">              <span class="attr">name</span>: <span class="string">&#x27;uploader&#x27;</span>,</span><br><span class="line">              <span class="attr">choices</span>: ctx.<span class="property">helper</span>.<span class="property">uploader</span>.<span class="title function_">getIdList</span>(), <span class="comment">// 获取Uploader列表</span></span><br><span class="line">              <span class="attr">message</span>: <span class="string">`Choose a(n) uploader`</span>,</span><br><span class="line">              <span class="attr">default</span>: ctx.<span class="property">config</span>.<span class="property">picBed</span>.<span class="property">uploader</span> || ctx.<span class="property">config</span>.<span class="property">picBed</span>.<span class="property">current</span></span><br><span class="line">            &#125;</span><br><span class="line">          ]</span><br><span class="line">          <span class="keyword">let</span> answer = <span class="keyword">await</span> ctx.<span class="property">cmd</span>.<span class="property">inquirer</span>.<span class="title function_">prompt</span>(prompts) <span class="comment">// 等待inquirer处理用户的输入</span></span><br><span class="line">          <span class="keyword">const</span> item = ctx.<span class="property">helper</span>.<span class="property">uploader</span>.<span class="title function_">get</span>(answer.<span class="property">uploader</span>) <span class="comment">// 获取用户选择的uploader</span></span><br><span class="line">          <span class="keyword">if</span> (item.<span class="property">config</span>) &#123; <span class="comment">// 如果uploader提供了config方法</span></span><br><span class="line">            <span class="keyword">await</span> <span class="title function_">handleConfig</span>(ctx, item.<span class="title function_">config</span>(ctx), answer.<span class="property">uploader</span>) <span class="comment">//处理该config方法暴露出的prompts数组</span></span><br><span class="line">          &#125;</span><br><span class="line">          ctx.<span class="property">log</span>.<span class="title function_">success</span>(<span class="string">&#x27;Configure config successfully!&#x27;</span>)</span><br><span class="line">        &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">          ctx.<span class="property">log</span>.<span class="title function_">error</span>(e)</span><br><span class="line">          <span class="keyword">if</span> (process.<span class="property">argv</span>.<span class="title function_">includes</span>(<span class="string">&#x27;--debug&#x27;</span>)) &#123;</span><br><span class="line">            <span class="title class_">Promise</span>.<span class="title function_">reject</span>(e)</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面是针对Uploader的config方法进行的配置处理，对于其他插件也是同理的，就不再赘述。这样我们就实现了能够通过命令行快速对配置文件进行配置，用户体验又是++。</p><h2 id="插件系统发布"><a href="#插件系统发布" class="headerlink" title="插件系统发布"></a>插件系统发布</h2><p>讲了那么多，我们都是在本地书写的插件系统，如何发布让别人能够安装使用呢？关于往npm发布模块有很多相关文章，比如参考这篇<a href="https://fenying.net/2017/12/02/publish-to-npm/">文章</a>。我在这里想讲的是如何发布一个既能在命令行使用，又可以通过比如<code>const picgo = require(&#39;picgo&#39;)</code>在Node.js项目里使用API调用的库。</p><h3 id="CLI与API调用并存"><a href="#CLI与API调用并存" class="headerlink" title="CLI与API调用并存"></a>CLI与API调用并存</h3><p>其实这个上面的部分里也提到了。我们在发布一个npm库的时候通常是在<code>package.json</code>里的<code>main</code>字段指定这个库的入口文件。那么这样使用者就可以通过比如<code>const picgo = require(&#39;picgo&#39;)</code>在Node.js项目里使用。</p><p>如果我们想要让这个库安装之后能够注册一个命令，那么我们可以在<code>bin</code>字段里指定这个命令已经对应的入口文件。比如：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="attr">&quot;bin&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;picgo&quot;</span><span class="punctuation">:</span> <span class="string">&quot;./bin/picgo&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure><p>这样我们在全局安装之后就会在系统里注册一个叫做<code>picgo</code>的命令了。</p><p>当然这个时候<code>bin</code>和<code>main</code>的入口文件通常是不一样的。<code>bin</code>的入口文件需要做好解析命令行的功能。所以通常我们会使用一些命令行解析的库例如<code>minimist</code>或者<code>commander.js</code>等等来解析命令行里的参数。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>至此，一个CLI插件系统的关键部分我们就基本实现了。那么我们在Electron项目里，可以在<code>main</code>进程里使用我们所写的插件系统，并通过这个插件暴露的API来打造应用的插件系统了。下一篇文章会详细讲述如何把CLI插件系统整合进Electron，实现GUI插件系统，并加入一些额外的机制，使得在GUI上的插件系统更加灵活而强大。</p><p>本文很多都是我在开发<code>PicGo</code>的时候碰到的问题、踩的坑。也许文中简单的几句话背后就是我无数次的查阅和调试。希望这篇文章能够给你的<code>electron-vue</code>开发带来一些启发。文中相关的代码，你都可以在<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>和<a href="https://github.com/PicGo/PicGo-Core">PicGo-Core</a>的项目仓库里找到，欢迎star~如果本文能够给你带来帮助，那么将是我最开心的地方。如果喜欢，欢迎关注我的<a href="https://molunerfinn.com/">博客</a>以及<a href="https://molunerfinn.com/tags/Electron-vue/">本系列文章</a>的后续进展。</p><blockquote><p><strong>注：文中的图片除未特地说明之外均属于我个人作品，需要转载请私信</strong></p></blockquote><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><p>感谢这些高质量的文章：</p><ol><li><a href="https://zhuanlan.zhihu.com/p/38730825">用Node.js开发一个Command Line Interface (CLI)</a></li><li><a href="https://zhuanlan.zhihu.com/p/26895282">Node.js编写CLI的实践</a></li><li><a href="http://www.infoq.com/cn/articles/nodejs-module-mechanism">Node.js模块机制</a></li><li><a href="https://onetwo.ren/%E5%89%8D%E7%AB%AF%E6%8F%92%E4%BB%B6%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/">前端插件系统设计与实现</a></li><li><a href="https://blog.csdn.net/kyfxbl/article/details/47787827">Hexo插件机制分析</a></li><li><a href="http://blog.yunplus.io/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E6%8F%92%E4%BB%B6%E6%89%A9%E5%B1%95/">如何实现一个简单的插件扩展</a></li><li><a href="https://fenying.net/2017/12/02/publish-to-npm/">使用NPM发布与维护TypeScript模块</a></li><li><a href="https://github.com/basarat/ts-npm-module">typescript npm 包例子</a></li><li><a href="https://docs.travis-ci.com/user/deployment/npm/">通过travis-ci发布npm包</a></li><li><a href="https://discuss.atom.io/t/dynamically-load-module-in-plugin-from-local-project-node-modules-folder/42930/2">Dynamic load module in plugin from local project node_modules folder</a></li><li><a href="https://aotu.io/notes/2016/08/09/command-line-development/index.html">跟着老司机玩转Node命令行</a></li><li>以及没来得及记录的那些好文章，感谢你们！</li></ol>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;祝大家2019年猪年新年快乐！本文较长，需要一定耐心看完哦~&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前段时间，我用&lt;a href=&quot;https://github.com/SimulatedGREG/electron-vue&quot;&gt;electron-vue&lt;/a&gt;开发了一款跨平台（目前支持主流三大桌面操作系统）的免费开源的图床上传应用——&lt;a href=&quot;https://github.com/Molunerfinn/PicGo&quot;&gt;PicGo&lt;/a&gt;，在开发过程中踩了不少的坑，不仅来自应用的业务逻辑本身，也来自electron本身。在开发这个应用过程中，我学了不少的东西。因为我也是从0开始学习electron，所以很多经历应该也能给初学、想学electron开发的同学们一些启发和指示。故而写一份Electron的开发实战经历，用最贴近实际工程项目开发的角度来阐述。希望能帮助到大家。&lt;/p&gt;
&lt;p&gt;预计将会从几篇&lt;a href=&quot;https://molunerfinn.com/tags/Electron-vue/&quot;&gt;系列文章&lt;/a&gt;或方面来展开：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-1/&quot;&gt;electron-vue入门&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-2/&quot;&gt;Main进程和Renderer进程的简单开发&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-3/&quot;&gt;引入基于Lodash的JSON database——lowdb&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-4/&quot;&gt;跨平台的一些兼容措施&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-5/&quot;&gt;通过CI发布以及更新的方式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-6/&quot;&gt;开发插件系统——CLI部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://molunerfinn.com/electron-vue-7/&quot;&gt;开发插件系统——GUI部分&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;想到再写…&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;说明&quot;&gt;&lt;a href=&quot;#说明&quot; class=&quot;headerlink&quot; title=&quot;说明&quot;&gt;&lt;/a&gt;说明&lt;/h2&gt;&lt;p&gt;&lt;code&gt;PicGo&lt;/code&gt;是采用&lt;code&gt;electron-vue&lt;/code&gt;开发的，所以如果你会&lt;code&gt;vue&lt;/code&gt;，那么跟着一起来学习将会比较快。如果你的技术栈是其他的诸如&lt;code&gt;react&lt;/code&gt;、&lt;code&gt;angular&lt;/code&gt;，那么纯按照本教程虽然在render端（可以理解为页面）的构建可能学习到的东西不多，不过在main端（electron的主进程）应该还是能学习到相应的知识的。&lt;/p&gt;
&lt;p&gt;如果之前的文章没阅读的朋友可以先从&lt;a href=&quot;https://molunerfinn.com/tags/Electron-vue/&quot;&gt;之前的文章&lt;/a&gt;跟着看。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="Vue" scheme="https://molunerfinn.com/tags/Vue/"/>
    
    <category term="Electron-vue" scheme="https://molunerfinn.com/tags/Electron-vue/"/>
    
  </entry>
  
  <entry>
    <title>2018小结</title>
    <link href="https://molunerfinn.com/2018-summary/"/>
    <id>https://molunerfinn.com/2018-summary/</id>
    <published>2019-01-18T09:22:00.000Z</published>
    <updated>2026-03-08T01:14:37.981Z</updated>
    
    <content type="html"><![CDATA[<p>终于把研究生开题的事情弄得差不多了，可以抽空写一下2018年的小结了。</p><span id="more"></span><p>今年和去年一样，也是格外忙。不仅实验室活多，还要兼顾研究生的开题等。跟去年一样，列一个今年学习成果清单：</p><h1 id="过去的一年"><a href="#过去的一年" class="headerlink" title="过去的一年"></a>过去的一年</h1><h2 id="技术成果"><a href="#技术成果" class="headerlink" title="技术成果"></a>技术成果</h2><ul><li><p><strong>2019.01.13</strong>（插播） <a href="https://github.com/Molunerfinn/PicGo">PicGo</a> 发布v2.0版本，正式支持插件系统。star数破3200，下载量破26k。【Electron】</p></li><li><p><strong>2018.08.28</strong> <a href="https://github.com/Molunerfinn/PicGo">PicGo</a> star数破2000，下载量破12k。【Electron】</p></li><li><p><strong>2018.07.19</strong> <a href="https://github.com/Molunerfinn/PicGo-Core">PicGo-Core</a> 开坑PicGo底层流程系统，将支持插件系统【Node+TypeScript】</p></li><li><p><strong>2018.07.11</strong> <a href="https://github.com/Molunerfinn/PicGo">PicGo</a> 更新v1.6版本，支持阿里云OSS，imgur，mini窗口，批量删除等功能。【Electron】</p></li><li><p><strong>2018.05.23</strong> 为VSCode的<a href="https://github.com/aioutecism/amVim-for-VSCode">amVim-for-VSCode</a>插件提交的支持<code>:</code>呼出<code>Command Palette</code>并实现部分Vim命令的<a href="https://github.com/aioutecism/amVim-for-VSCode/pull/199">PR</a>被合并。【TypeScript】</p></li><li><p><strong>2018.05.17</strong> <a href="https://github.com/Molunerfinn/PicGo">PicGo</a> star数破800，下载数破5k。【Electron】</p></li><li><p><strong>2018.05.15</strong> 开发推来推趣3期后台时遇到微信二维码支付相关功能的开发，总结了一篇<a href="https://molunerfinn.com/koa2-wechatpay/">《基于Koa2开发微信二维码扫码支付相关流程》</a>的经验文。【Koa】</p></li><li><p><strong>2018.05.09</strong> <a href="https://github.com/Molunerfinn/PicGo">PicGo</a> 更新v1.5版本，支持腾讯云COSv5、GitHub图床、重命名等新功能。【Electron】</p></li><li><p><strong>2018.03.28</strong> <a href="https://github.com/Molunerfinn/node-github-profile-summary">node-github-profile-summary</a>和<a href="https://github.com/Molunerfinn/vue-koa-demo">vue-koa-demo</a>的Docker话。【Docker】</p></li><li><p><strong>2018.03.10~2018.05.31</strong> 推来推趣3期后台（全栈）迭代。【Vue+Koa+Graphql】</p></li><li><p><strong>2018.03.06</strong> <a href="https://github.com/Molunerfinn/hexo-theme-melody">hexo-theme-melody</a> 更新v1.5版本，支持iframe、支持slides等特性。【hexo+hexo-theme】</p></li><li><p><strong>2018.01.17~2018.03.28</strong> 开坑<a href="https://github.com/Molunerfinn/node-github-profile-summary">node-github-profile-summary</a>，可以生成漂亮的GitHub总结报告。【Vue+Koa+Chart.js+Graphql】</p></li><li><p><strong>2018.01.11~2018.05.08</strong> 写了<a href="https://molunerfinn.com/tags/Electron-vue/">Electron-vue开发实战系列教程</a>，用于记录自己开发PicGo的坑以及帮助新人入门Electron开发。【Electron】</p></li></ul><p>对比去年给自己立的目标：</p><ul><li>算法、数据结构 【一部分】</li><li>Parcel 【没有】</li><li>TypeScript 【用上了】</li><li>Puppeteer自动化测试 【没有】</li><li>PWA 【有新的体验】</li><li>给开源库提PR 【完成】</li><li>github robot 【没有】</li><li>如果可以，学习一下react 【碰了皮毛】</li></ul><p>感觉完成度不够高，不及去年同期对2016年的目标的实现。主要是没有预料到下半年研究生的开题的战线耗时这么久。从2018年8月开始我就没有发过笔记或者技术文章了，真的非常惭愧。</p><h1 id="期望、目标"><a href="#期望、目标" class="headerlink" title="期望、目标"></a>期望、目标</h1><p>依然要写下2019年需要学习的东西：</p><ul><li>算法、数据结构</li><li>Flutter入门</li><li>PWA</li><li>学习react</li><li>Puppeteer使用</li></ul><p>感觉把目标缩小点应该完成度会更高。毕竟19年要开始找实习和正式工作+写研究生毕设了。</p><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>这一年来的前端的学习之路，收获还是不少的。比起2017年来说，我感觉最大的收获就是阅读源码的能力提高了。虽然不是什么高深的源码，不过相比之前对阅读源码有恐惧心理的自己，还是好了不少。</p><p>5月份的时候，那段时间我的Mac上的VSCode的Vim插件变得异常卡，可以参考这个<a href="https://github.com/VSCodeVim/Vim/issues/2021">issue</a>。无奈之下只能把官方的Vim插件替换掉，换成了<a href="https://github.com/aioutecism/amVim-for-VSCode">amVim-for-VSCode</a>，当初刚换上的时候，操作如丝般顺滑！不过当时发现它不支持<code>:</code>带来的一系列操作，比如<code>:w</code>保存，<code>:q</code>退出等。于是我萌生了一个想法，能不能把VSCodeVim的操作移植到amVim上？在阅读了VSCodeVim的源码之后，我也模仿了它的实现，把一部分常用的命令移植到了amVim上，并最终成功被作者<a href="https://github.com/aioutecism/amVim-for-VSCode/pull/199">合并</a>。</p><p>这次提交PR的过程，我也发了一篇<a href="https://molunerfinn.com/vscode-extension-develop-1/">文章</a>作为记录。应该说这次经历过后我对阅读源码的恐惧感减轻了不少，这也为之后的<a href="https://github.com/PicGo/PicGo-Core">PicGo-Core</a>的开发带来很大的帮助。</p><p>8月份之后很长的一段时间里，除了在做研究生开题相关的东西，我基本就是在开发<a href="https://github.com/PicGo/PicGo-Core">PicGo-Core</a>了。如果你有用过<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>，那么你应该知道它的1.x版本是不支持插件系统的。而且内置的只有有限的8个图床。（如果你不知道<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>，欢迎使用，对你的文章写作有很大帮助~）。<code>PicGo</code>里我收到最多的issue，应该就是<code>能否支持XXX图床</code>。如果是一开始写PicGo的时候，我一般会在下一个版本里更新新的图床支持。但是支持到第8个的时候我发现这样无限地支持下去不是一个办法。正巧有个用户提出一个<a href="https://github.com/Molunerfinn/PicGo/issues/26#issuecomment-370105520">想法</a>：能否将对各种图床的支持，做成插件化的管理，类似 Core + Plugins 这样的模式。</p><p>我为此思考了好久，发现这样是可行而且非常合理的。于是我开始找相关的资料——我一开始的想法只是在Electron内部实现一个插件系统。为此我去找了不少例子，比如VSCode、Kap、Atom、Hyper等用Electron写的工具，想看看他们的插件系统是如何实现的。发现他们的实现相对比较复杂。对我来说我是想要实现一个底层的上传流程系统。</p><p>后来我想到了Hexo也是有插件系统的，于是就去阅读了Hexo的插件系统如何实现。在看Hexo插件系统实现的同时，我还发现了另外一个工具<a href="https://segmentfault.com/a/1190000013362598">feflow的插件系统实现</a>。不过我后来发现，feflow的插件体系其实底层大部分是「抄」的hexo的源码的，尤其一个很经典的例子…</p><p><img src="https://i.loli.net/2019/01/18/5c4135bf942d9.png" alt="20190118101110.png"></p><p>于是我就把feflow的文章当做hexo插件系统实现的解析文章了哈哈。</p><p>在充分理解了hexo插件如何实现了之后，我也开始着手我自己的<a href="https://github.com/PicGo/PicGo-Core">PicGo-Core</a>了。当然我并没有完全照搬hexo的实现，因为我发现那样的话不利于插件开发者开发插件（主要是语法提示），hexo的插件机制是暴露全局的<code>hexo</code>变量去实现的。</p><p><code>PicGo-Core</code>的流程大概如下：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo-core-fix.jpg" alt="flow"></p><p>输入路径或者变量等-&gt;经过转换器转换-&gt;上传器上传-&gt;输出结果。中间包含着三个生命周期钩子。这样的话用户开发插件可以只实现其中的某个部分，也可以实现其中的某几个部分，来实现<code>PicGo</code>原先不能实现的一些功能：</p><ol><li>比如上传非图片文件</li><li>比如上传图片前压缩、加水印</li><li>比如通过已知URL上传图片</li></ol><p>等等。</p><p>我也正式使用了<code>TypeScript</code>作为<code>PicGo-Core</code>的开发语言，使用起来一开始确实很不习惯，但是后来越用越顺手，学习新东西的过程大概都是这样吧！</p><p>在开发<code>PicGo-Core</code>的过程中，我也做了很多除了上面流程系统之外的工作。比如：</p><ol><li>要让用户在命令行和Node里都能使用，我为此基于<a href="https://github.com/tj/commander.js/">commander.js</a>和<a href="https://github.com/SBoudrias/Inquirer.js/">Inquirer.js</a>给<code>PicGo-Core</code>加上了命令行支持，同样插件也能支持注册命令等操作。</li><li>为了方便其他开发者开发插件，首先我得写好一个插件模板<a href="https://github.com/PicGo/picgo-template-plugin">picgo-template-plugin</a>，并学习了<code>vue-cli2</code>和<code>vue-cli3</code>对于模板生成的实现，写了一个下载模板、生成模板的命令<a href="https://github.com/PicGo/PicGo-Core/blob/dev/src/plugins/commander/init.ts">init</a>，好让插件开发者能够快速创建插件模板进行插件开发。</li><li>为了让使用者方便下载使用插件，我写了一个<a href="https://github.com/PicGo/PicGo-Core/blob/dev/src/lib/PluginHandler.ts">PluginHandler</a>用于调用<code>npm</code>命令来下载插件。</li><li>除了写代码，还得写文档，没有文档怎么能有其他开发者为你开发插件呢？所以还花了很大的精力写了<code>PicGo-Core</code>的<a href="https://picgo.github.io/PicGo-Core-Doc/zh/">文档</a>，配图、示例一应俱全。</li></ol><p>开发完Node版本的<code>PicGo-Core</code>之后，我还要将它和Electron版本的<code>PicGo</code>整合起来，使得Electron版本的<code>PicGo</code>也能拥有插件系统。并且还得通过<code>ipcMain</code>等方式，将主进程的信息通知给渲染进程，从而渲染出插件页面里的插件列表：</p><p><img src="https://user-images.githubusercontent.com/12621342/50515434-bc9e8180-0adf-11e9-8c71-0e39973c06b1.png"></p><p>为了让插件开发者能够更好地利用GUI版本的优势，我还为GUI版本的PicGo插件加了GUI插件特有的<code>guiApi</code>、<code>guiMenu</code>等功能：</p><p><img src="https://i.loli.net/2019/01/12/5c39a2f60a32a.png"></p><p>这样插件拥有自己的菜单，可以执行自己的操作，那么能做的事就更多了，比如：</p><ol><li>结合GitHub刚刚开放的免费私人仓库，可以通过插件实现PicGo的相册以及配置文件同步。</li><li>结合TinyPng等工具实现上传前给图片瘦身。（不过可能挺影响上传速度的。）</li><li>结合一些Canvas工具，可以在上传图片前给图片加水印。</li><li>通过指定文件夹，将文件夹内部的markdown里的图片地址进行图床迁移。</li></ol><p>等等。。</p><p>终于，在2019年1月13号，PicGo迎来了2.0版本的<a href="https://github.com/Molunerfinn/PicGo/releases/">更新</a>。</p><p>回顾这些工作，都是我一个人在半年的时间里通过课余的时间做出来的，其实还是很自豪的。更关键的是，通过开放了插件系统，可以让更多的人参与到PicGo软件的完善中来，通过插件可以实现很多本体不提供或者不足的功能，也是让PicGo更加强大的一个条件。我也希望它日后也能形成自己的一个小生态。</p><p>实际上，PicGo-Core以及PicGo2.0发布之后，就已经有第三方开发者开发插件了，速度之快让我始料未及。为此我也迅速加上了<a href="https://github.com/PicGo/Awesome-PicGo">Awesome-PicGo</a>的仓库，这样能让更多的开发者的作品让用户看到：</p><p><img src="https://i.loli.net/2019/01/18/5c413c6300681.png" alt="20190118103930.png"></p><p>你已经可以在VSCode里搜索PicGo，就能发现VSCode版的PicGo扩展了，实现了三种在Markdown里快速上传图片的方式：</p><ul><li>通过截图上传</li></ul><p><img src="https://camo.githubusercontent.com/e7898449cadc72bb7045319e4195a5210fef60cf/68747470733a2f2f692e6c6f6c692e6e65742f323031392f30312f31362f356333656430333335373761302e676966"></p><ul><li>通过文件浏览器上传</li></ul><p><img src="https://camo.githubusercontent.com/955c32665b55b1ac85ec9696cc51fddcb740076d/68747470733a2f2f692e6c6f6c692e6e65742f323031392f30312f31362f356333656433366430643964332e676966"></p><ul><li>通过输入文件路径上传</li></ul><p><img src="https://camo.githubusercontent.com/f2cb528b4fcca4e64f6e6bf80d1e25ea47b85483/68747470733a2f2f692e6c6f6c692e6e65742f323031392f30312f31362f356333656432333836623761632e676966"></p><p>2019年，我会更新几篇文章，主要讲讲如何实现一个插件系统，如何将Node端实现的插件系统整合到Electron端，如何实现一个模板下载、生成功能，如何实现良好的命令行交互等等。</p><p>2019年也是我找实习、找正式工作的一年，希望今年一切都顺利吧！</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;终于把研究生开题的事情弄得差不多了，可以抽空写一下2018年的小结了。&lt;/p&gt;</summary>
    
    
    
    <category term="日志" scheme="https://molunerfinn.com/categories/%E6%97%A5%E5%BF%97/"/>
    
    
    <category term="随笔" scheme="https://molunerfinn.com/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>图床「神器」PicGo v2.0更新，插件系统终于来了</title>
    <link href="https://molunerfinn.com/picgo-v2.0-update/"/>
    <id>https://molunerfinn.com/picgo-v2.0-update/</id>
    <published>2019-01-13T11:30:00.000Z</published>
    <updated>2026-03-08T01:14:37.986Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>距离上次更新(v1.6.2)已经过去了5个月，很抱歉2.0版本来得这么晚。本来想着在18年12月（PicGo一周年的时候）发布2.0版本，但是无奈正值研究生开题期间，需要花费不少时间（不然毕不了业了T T），所以这个大版本姗姗来迟。不过从这个版本开始，正式支持插件系统，发挥你们的无限想象，PicGo也能成为一个极致的效率工具。</p><p>除了发布PicGo 2.0<a href="https://github.com/Molunerfinn/PicGo/releases/">本体</a>，一同发布的还有<a href="https://picgo.github.io/PicGo-Core-Doc/">PicGo-Core</a>（PicGo 2.0的底层，支持CLI和API调用），以及VSCode的PicGo插件<a href="https://github.com/Spades-S/vs-picgo">vs-picgo</a>等。</p><span id="more"></span><h3 id="插件系统"><a href="#插件系统" class="headerlink" title="插件系统"></a>插件系统</h3><p>PicGo的底层核心其实是<code>PicGo-Core</code>。这个核心主要就是一个流程系统。(它支持在Node.js环境下全局安装，可以通过命令行上传图片文件、也可以接入Node.js项目中调用api实现上传。)</p><p><code>PicGo-Core</code>的上传流程如下：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo-core-fix.jpg"></p><p><code>Input</code>一般是文件路径，经过<code>Transformer</code>读取信息，传入<code>Uploader</code>进行上传，最后通过 <code>Output</code> 输出结果。而插件可以接入三个生命周期（<code>beforeTransform</code>、<code>beforeUpload</code>、<code>afterUpload</code>）以及两种部件（<code>Transformer</code>和<code>Uploader</code>）。</p><p>换句话说，如果你书写了合适的<code>Uploader</code>，那么可以上传到不同的图床。如果你书写了合适的<code>Transformer</code>，你可以通过URL先行下载文件再通过<code>Uploader</code>上传等等。</p><p>另外，如果你不想下载PicGo的electron版本，也可以通过npm安装picgo来实现命令行一键上传图片的快速体验。</p><p>PicGo除了<code>PicGo-Core</code>提供的核心功能之外，额外给GUI插件给予一些自主控制权。</p><p>比如插件可以拥有自己的菜单项：</p><p><img src="https://i.loli.net/2019/01/12/5c39a2f60a32a.png"></p><p>因此GUI插件除了能够接管<code>PicGo-Core</code>给予的上传流程，还可以通过PicGo提供的guiApi等接口，在插件页面实现一些以前单纯通过<code>上传区</code>实现不了的功能：</p><p>比如可以通过打开一个<code>InputBox</code>获取用户的输入：</p><p><img src="https://i.loli.net/2019/01/12/5c39aa4dab0b4.png"></p><p>可以通过打开一个路径来执行其他功能（而非只是上传文件）：</p><p><img src="https://i.loli.net/2019/01/12/5c39aea61e80d.gif"></p><p>甚至还可以直接在插件面板通过调用api实现上传。</p><p>另外插件可以监听相册里图片删除的事件：</p><p><img src="https://i.loli.net/2019/01/12/5c39b3c8746cf.png"></p><p>这个功能就可以写一个插件来实现相册图片和远端存储里的同步删除了。</p><p>通过如上介绍，我现在甚至就已经能想到插件系统能做出哪些有意思的插件了。</p><p>比如：</p><ol><li>结合GitHub刚刚开放的免费私人仓库，可以通过插件实现PicGo的相册以及配置文件同步。</li><li>结合TinyPng等工具实现上传前给图片瘦身。（不过可能挺影响上传速度的。）</li><li>结合一些Canvas工具，可以在上传图片前给图片加水印。</li><li>通过指定文件夹，将文件夹内部的markdown里的图片地址进行图床迁移。</li><li>等等。。</li></ol><p>希望这个插件系统能够给PicGo带来更强大的威力，也希望它能够成为你的极致的效率工具。</p><p><strong>需要注意的是，想要使用PicGo 2.0的插件系统，需要先行安装<a href="https://nodejs.org/en/">Node.js</a>环境，因为PicGo的插件安装依赖<code>npm</code>。</strong></p><h2 id="2-0其他更新内容"><a href="#2-0其他更新内容" class="headerlink" title="2.0其他更新内容"></a>2.0其他更新内容</h2><p>除了上面说的插件系统，PicGo 2.0还更新了如下内容：</p><ul><li>底层重构了之后，某些图床上传不通过<code>base64</code>值的将会提升不少速度。比如<code>SM.MS</code>图床等。而原本就通过<code>base64</code>上传的图床速度不变。</li><li>增加一些配置项，比如打开配置文件（包括了上传的图片列表）、mini窗口置顶、代理设置等。<br><img src="https://user-images.githubusercontent.com/12621342/50515474-ea83c600-0adf-11e9-8022-52f4ab9e0ea5.png" alt="image"></li><li>在相册页可以选择复制的链接格式，不用再跑去上传页改了。<br><img src="https://user-images.githubusercontent.com/12621342/50515502-17d07400-0ae0-11e9-80b9-c38f25b64922.png" alt="image"></li><li>增加不同页面切换的淡入淡出动画。</li><li>macOS版本配色小幅更新。（Windows版本配色更新Fluent Design效果预计在2.1版本上线）</li><li>更新electron版本从1.8-&gt;4.0，启动速度更快了，性能也更好了。</li></ul><h2 id="Bug-Fixed"><a href="#Bug-Fixed" class="headerlink" title="Bug Fixed"></a>Bug Fixed</h2><ul><li>修复：macOS多屏下打开详细窗口时位置错误的<a href="https://github.com/Molunerfinn/PicGo/issues/128">问题</a>。</li><li>修复：多图片上传重命名一致的<a href="https://github.com/Molunerfinn/PicGo/issues/136">问题</a>。</li><li>修复：拖拽图片到软件会自动在软件内部打开这张图片的<a href="https://github.com/Molunerfinn/PicGo/issues/140">bug</a>。</li><li>修复：重命名窗口只出现在屏幕中央而不是跟随主窗口的<a href="https://github.com/Molunerfinn/PicGo/issues/145">bug</a>。</li></ul><h2 id="VSCode的PicGo插件vs-picgo"><a href="#VSCode的PicGo插件vs-picgo" class="headerlink" title="VSCode的PicGo插件vs-picgo"></a>VSCode的PicGo插件vs-picgo</h2><p>在PicGo-Core发布不久，就有人根据PicGo-Core的API编写了VSCode版的PicGo插件。使用起来也非常方便：</p><ul><li>截图上传</li></ul><p><img src="https://user-gold-cdn.xitu.io/2019/1/13/1684764986e5edd7?w=891&h=498&f=gif&s=297594"></p><ul><li>文件浏览器选择文件上传</li></ul><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/vs-picgo-explorer.gif"></p><ul><li>输入文件路径上传</li></ul><p><img src="https://user-gold-cdn.xitu.io/2019/1/13/1684765698ad41fe?w=891&h=498&f=gif&s=155843"></p><p>配置项与PicGo的图床的配置项基本保持一致。在VSCode插件栏搜索PicGo即可下载安装与体验！</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>PicGo第一个稳定版本是在少数派上发布的，详见<a href="https://sspai.com/post/42310">PicGo：基于 Electron 的图片上传工具</a>。支持macOS、Windows、Linux三平台，开源免费，界面美观，也得到了很多朋友的认可。如果你对它有什么意见或者建议，也欢迎在<a href="https://github.com/Molunerfinn/PicGo/issues">issues</a>里指出。如果你喜欢它，不妨给它点个star。如果对你真的很有帮助，不妨请我喝杯咖啡（PicGo的GitHub<a href="https://github.com/Molunerfinn/PicGo">首页</a>有赞助的二维码）？</p><blockquote><p>下载地址：<a href="https://github.com/Molunerfinn/PicGo/releases">https://github.com/Molunerfinn/PicGo/releases</a></p></blockquote><blockquote><p>Windows用户请下载<code>.exe</code>文件，macOS用户请下载<code>.dmg</code>文件，Linux用户请下载<code>.AppImage</code>文件。</p></blockquote><p>Happy uploading！</p>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;距离上次更新(v1.6.2)已经过去了5个月，很抱歉2.0版本来得这么晚。本来想着在18年12月（PicGo一周年的时候）发布2.0版本，但是无奈正值研究生开题期间，需要花费不少时间（不然毕不了业了T T），所以这个大版本姗姗来迟。不过从这个版本开始，正式支持插件系统，发挥你们的无限想象，PicGo也能成为一个极致的效率工具。&lt;/p&gt;
&lt;p&gt;除了发布PicGo 2.0&lt;a href=&quot;https://github.com/Molunerfinn/PicGo/releases/&quot;&gt;本体&lt;/a&gt;，一同发布的还有&lt;a href=&quot;https://picgo.github.io/PicGo-Core-Doc/&quot;&gt;PicGo-Core&lt;/a&gt;（PicGo 2.0的底层，支持CLI和API调用），以及VSCode的PicGo插件&lt;a href=&quot;https://github.com/Spades-S/vs-picgo&quot;&gt;vs-picgo&lt;/a&gt;等。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="Vue" scheme="https://molunerfinn.com/tags/Vue/"/>
    
    <category term="Electron-vue" scheme="https://molunerfinn.com/tags/Electron-vue/"/>
    
  </entry>
  
  <entry>
    <title>一周一部好电影V【WEEK210 网络迷踪】</title>
    <link href="https://molunerfinn.com/PerfectMoviePerWeek5/"/>
    <id>https://molunerfinn.com/PerfectMoviePerWeek5/</id>
    <published>2018-11-19T22:29:00.000Z</published>
    <updated>2026-03-08T01:14:37.981Z</updated>
    
    <content type="html"><![CDATA[<h3 id="2018-11-11-WEEK210-网络迷踪"><a href="#2018-11-11-WEEK210-网络迷踪" class="headerlink" title="2018-11-11 WEEK210 网络迷踪"></a>2018-11-11 WEEK210 网络迷踪</h3><p>网络迷踪——————————————Searching<br><img src="https://img.piegg.cn/week210.jpg?imgslim" alt="网络迷踪"></p><ul><li>导演：阿尼什·查甘蒂</li><li>主演：约翰·赵&#x2F;米切尔·拉&#x2F;黛博拉·梅辛&#x2F;约瑟夫·李&#x2F;萨拉·米博·孙&#x2F;亚历克丝·杰恩·高&#x2F;梅金·刘&#x2F;刘卡雅&#x2F;多米尼克·霍夫曼&#x2F;西尔维亚·米纳西安&#x2F;梅丽莎·迪斯尼&#x2F;康纳·麦克雷斯&#x2F;科林·伍德尔&#x2F;约瑟夫·约翰·谢尔勒&#x2F;阿什丽·艾德纳&#x2F;托马斯·巴布萨卡&#x2F;朱莉·内桑森&#x2F;罗伊·阿布拉姆森&#x2F;盖奇·<br>比尔托福&#x2F;肖恩·奥布赖恩&#x2F;瑞克·萨拉比亚&#x2F;布拉德·阿布瑞尔&#x2F;加布里埃尔D·安吉尔</li><li>片长：102分钟</li><li>影  片类型：剧情&#x2F;悬疑&#x2F;惊悚</li><li>豆  瓣评分：8.7&#x2F;10(from85,981users)</li><li>IMDB评分：7.8&#x2F;10(from38,178users)</li></ul><span id="more"></span><p>Hi，各位好久不见！（最近在忙毕设开题的事，所以一直没办法按期完成推送。等忙过这一段就能大致恢复正常。）这部电影可以说是小成本制作的典范之作了。全片很有意思，大部分用的镜头来自手机、电脑的前置摄像头，然后配合电脑、手机屏幕的聊天记录、网页记录等来描述故事、展现角色心理状态。很多时候刚敲完的文字，然后想了想又删掉的光标；在屏幕前停留的视线等等都会让你身临其境——因为这些场景在我们当今的生活中，真的司空见惯。</p><p>可以说手机和电脑加上互联网已经占据了很多人一天的大部分。本片也聚焦在当前的网络环境下的人与人之间，父母和孩子之间的关系。我们经常会对父母隐藏自己的某一面，而在互联网上却又是另一副的面孔。所以很多时候本该最了解我们的人，却成了最熟悉的陌生人。当然，意外的惊喜是本片还加入了很不错的悬疑元素，真相大白的那刻，总算把你觉得不对劲的地方说了出来，但是却让你依然感觉很过瘾。好电影，值得一看！</p><hr><h3 id="2018-10-07-WEEK209-奇迹男孩"><a href="#2018-10-07-WEEK209-奇迹男孩" class="headerlink" title="2018-10-07 WEEK209 奇迹男孩"></a>2018-10-07 WEEK209 奇迹男孩</h3><p>奇迹男孩——————————————Wonder<br><img src="https://img.piegg.cn/week209.jpg?imgslim" alt="奇迹男孩"></p><ul><li>导演：斯蒂芬·卓博斯基</li><li>主演：雅各布·特伦布莱&#x2F;朱莉娅·罗伯茨&#x2F;伊扎贝拉·维多维奇&#x2F;欧文·威尔逊&#x2F;诺亚·尤佩&#x2F;丹妮尔·罗丝·拉塞尔&#x2F;纳吉·杰特&#x2F;戴维<br>德·迪格斯&#x2F;曼迪·帕廷金&#x2F;布莱斯·吉扎尔&#x2F;艾尔·麦金农&#x2F;泰·孔西利奥&#x2F;詹姆斯·休斯&#x2F;凯尔·布瑞特科夫&#x2F;米莉·戴维斯&#x2F;莉娅·朱厄<br>特&#x2F;凯琳·布瑞特科夫&#x2F;利亚姆·迪金森&#x2F;艾玛·特伦布莱&#x2F;马克·多兹劳&#x2F;鲁奇娅·伯纳德&#x2F;J·道格拉斯·斯图瓦特&#x2F;阿里·利伯特&#x2F;埃丽卡<br>·麦基特里克&#x2F;本杰明·拉特纳&#x2F;杰森·麦金农&#x2F;索尼娅·布拉加&#x2F;吉洁特</li><li>片长：113分钟</li><li>影  片类型：剧情&#x2F;家庭&#x2F;儿童</li><li>豆  瓣评分：8.6&#x2F;10(from211,803users)</li><li>IMDB评分：8.0&#x2F;10(from89,828users)</li></ul><!--more--><p>Hi，各位好久不见！本周给大家推荐的是一部来自美国的《奇迹男孩》。从片名就可以看出是讲述一个小男孩的故事。温馨的故事很多，不过各有各打动人的地方。本片讲述的故事可能并没有什么出奇的地方，甚至你也有可能遇到类似的例子。影片中的的主要角色都有自己的一段独白戏。而从独白戏中，你才可以看到那些角色真实的自己。</p><p>就像行星绕着恒星转一样，我们的生活中也或多或少会围着某个人转。在关心他人的同时不得不遮盖自己的伤疤。但其实很多时候跟对方坦诚相待能获得更好的效果。要成为一个善良的人，要做善良的事。温馨的电影，值得一看~</p><hr><h3 id="2018-09-23-WEEK208-谍影重重"><a href="#2018-09-23-WEEK208-谍影重重" class="headerlink" title="2018-09-23 WEEK208 谍影重重"></a>2018-09-23 WEEK208 谍影重重</h3><p>谍影重重——————————————The Bourne Identity<br><img src="https://img.piegg.cn/week208.jpg?imgslim" alt="谍影重重"></p><ul><li>导演：道格·里曼</li><li>主演：马特·达蒙&#x2F;弗朗卡·波滕特&#x2F;克里斯·库珀&#x2F;克里夫·欧文&#x2F;朱丽娅·斯蒂尔斯&#x2F;布莱恩·考克斯&#x2F;阿德沃尔·阿吉纽依-艾格拜吉&#x2F;加布里埃尔·曼&#x2F;沃尔顿·戈金斯&#x2F;约什·汉密尔顿&#x2F;Orso Maria Guerrini</li><li>片长：119分钟</li><li>影  片类型：动作&#x2F;悬疑&#x2F;惊悚</li><li>豆  瓣评分：8.5&#x2F;10(from215,888users)</li><li>IMDB评分：7.9&#x2F;10(from461,682users)</li></ul><!--more--><p>Hi，各位好久不见！继上次看完《碟中谍6》之后，在经过舍友的推荐后我找来了另外一部讲述特工的电影《谍影重重》。跟《碟中谍》系列不同的是，《谍影重重》系列的男主角马特达蒙并没有阿汤哥那样帅到让你印象深刻。相反他一开始并不吸引人。</p><p>如果说《007》的看点是特工+美女，《碟中谍》的看点是阿汤哥的颜和拼命，那么《谍影重重》的看点就真的是一个特工的自我救赎了。我想推荐的并不是这一部电影，而是这整个系列（1、2、3、5部）。并且这里面每一部的水平、评分都很高。可以说是荧幕上「最为真实」的讲述间谍、特工的电影了。在这里面你是能真的学习到一些常人并不会特意关注到的细节。伯恩的招式可能没有那么华丽，但是是招招制敌，干净利落不拖泥带水，剧情的发展也是一波三折，紧凑而牵动人心。作为一部动作、悬疑电影我觉得虽然动作戏不如《碟中谍》那么华丽但是也已经足够帮。</p><p>其实第一部已经做得很出色，没想到第二部、第三部也同样出彩。好电影，值得一看。</p><hr><h3 id="2018-09-16-WEEK207-走到尽头"><a href="#2018-09-16-WEEK207-走到尽头" class="headerlink" title="2018-09-16 WEEK207 走到尽头"></a>2018-09-16 WEEK207 走到尽头</h3><p>走到尽头——————————————끝까지 간다<br><img src="https://img.piegg.cn/week207.jpg?imgslim" alt="走到尽头"></p><ul><li>导演：金成勋</li><li>主演：李善均&#x2F;赵震雄&#x2F;郑满植&#x2F;申东美&#x2F;申正根&#x2F;朴宝剑</li><li>片长：111分钟</li><li>影  片类型：动作&#x2F;惊悚&#x2F;犯罪</li><li>豆  瓣评分：7.8&#x2F;10(from41,868users)</li><li>IMDB评分：7.2&#x2F;10(from7,509users)</li></ul><!--more--><p>Hi，各位好久不见！本周给大家推荐的是一部来自韩国的《走到尽头》。这部电影我在3年前曾看过一次，不过最近重新又看了一遍依然感觉十分不错。</p><p>从影片一开始就开始就把观众带入非常紧张、刺激的情节，让人不由自主地为主角捏一把汗。而后的矛盾冲突又依然保持着高度的紧张和不突兀的幽默镜头。而随着剧情的推进，不断地反转也是让人看得很是过瘾。可以说是不停地用新的错误掩盖旧的错误。我想虽然电影有所夸张，但是现实中的我们却总会有类似的时刻。环环相扣的剧情在影片的最后达到高潮。开放式的结局也能让你思考良多。而比起我们的电影结局大多是阳光美好而言，这部电影的结局可以说带着一些黑色气息了。好电影，值得一看！</p><hr><h3 id="2018-09-09-WEEK206-碟中谍6：全面瓦解"><a href="#2018-09-09-WEEK206-碟中谍6：全面瓦解" class="headerlink" title="2018-09-09 WEEK206 碟中谍6：全面瓦解"></a>2018-09-09 WEEK206 碟中谍6：全面瓦解</h3><p>碟中谍6：全面瓦解——————————————Mission: Impossible - Fallout<br><img src="https://img.piegg.cn/week206.jpg?imgslim" alt="碟中谍6：全面瓦解"></p><ul><li>导演：克里斯托弗·麦奎里</li><li>主演：汤姆·克鲁斯&#x2F;亨利·卡维尔&#x2F;文·瑞姆斯&#x2F;西蒙·佩吉&#x2F;丽贝卡·弗格森&#x2F;西恩·哈里斯&#x2F;安吉拉·贝塞特&#x2F;凡妮莎·柯比&#x2F;米歇尔·莫纳汉&#x2F;韦斯·本特利&#x2F;费雷德里克·施密特&#x2F;亚历克·鲍德温&#x2F;杨亮&#x2F;克里斯托弗·琼勒&#x2F;沃尔夫·布利策&#x2F;拉斐尔·琼勒&#x2F;安德鲁·卡扎纳夫·平&#x2F;克里斯多夫·德·舒瓦西&#x2F;拉裴尔·德普雷&#x2F;让·巴普蒂斯特·菲永&#x2F;马克斯·盖勒&#x2F;奥利维尔·体班德&#x2F;亚历山大·普尔&#x2F;阿利克斯·贝纳泽什&#x2F;乔伊·安沙&#x2F;维利贝·托皮奇&#x2F;格雷厄姆·福克斯&#x2F;卡斯珀·菲利普森&#x2F;菲恩·乔利&#x2F;鲁斯·贝恩&#x2F;奈杰尔·艾伦</li><li>片长：147分钟</li><li>影  片类型：动作&#x2F;惊悚&#x2F;冒险</li><li>豆  瓣评分：8.3&#x2F;10(from164,538users)</li><li>IMDB评分：8.1&#x2F;10(from118,713users)</li></ul><!--more--><p>Hi，各位好久不见！本周给大家推荐的是一部最近正在热映的电影《碟中谍6：全面瓦解》。动作片系列，我觉得如今只有《速度与激情》系列能与《碟中谍》系列比拼了。</p><p>阿汤哥依然是拼命三郎。本片全程无尿点。虽然剧情依然是跟核弹有关（哈哈）。不过不管是跳伞、飙车、开飞机甚至是「屋顶跑酷」都让人看得热血沸腾。22年了，阿汤哥依然是那个阿汤哥，不过当年看他电影的人已经长大了。熟悉的片头曲，琳琅满目的「黑科技」，剧情也是不停地反转反转。整部电影几乎一直处于神经紧绷的状态，让人看了大呼过瘾！</p><p>不知道还能再看到阿汤哥的碟中谍多少次，这部好电影，我想你一定要去看看。</p><hr><h3 id="2018-08-26-WEEK205-游戏之夜"><a href="#2018-08-26-WEEK205-游戏之夜" class="headerlink" title="2018-08-26 WEEK205 游戏之夜"></a>2018-08-26 WEEK205 游戏之夜</h3><p>游戏之夜——————————————Game Night<br><img src="https://img.piegg.cn/week205.jpg?imgslim" alt="游戏之夜"></p><!--more--><ul><li>导演：约翰·弗朗西斯·戴利&#x2F;乔纳森·M·戈尔茨坦</li><li>主演：杰森·贝特曼&#x2F;瑞秋·麦克亚当斯&#x2F;凯尔·钱德勒&#x2F; 莎朗·豪根&#x2F;比利·马格努森&#x2F;拉蒙尼·莫里斯&#x2F;凯莉·班伯里&#x2F;杰西·普莱蒙&#x2F; 迈克尔·C·豪尔&#x2F;丹尼·赫斯顿&#x2F;切尔西·帕瑞蒂&#x2F;卡米利·陈&#x2F;泽瑞克·威廉姆斯&#x2F;约书亚·米克尔&#x2F;R·F·戴利</li><li>片长：100分钟</li><li>影  片类型：喜剧&#x2F;悬疑&#x2F;犯罪</li><li>豆  瓣评分：7.1&#x2F;10(from22,632users)</li><li>IMDB评分：7.0&#x2F;10(from109,979users)</li></ul><p>Hi，各位好久不见~本周给大家推荐的是来自英国的喜剧电影《游戏之夜》。听名字可能并不知道是什么意思，甚至有点「游戏人生」的感觉。但是看完之后却能把你笑得人仰马翻。这部结合了悬疑、犯罪的喜剧电影从分类上来说就让人忍俊不禁。其实同样类型的国内电影还有比如《唐人街探案》系列。不过我更加推荐这一部电影。</p><p>原因？原因在于这部电影的笑点和反转总是让你措手不及，反转会给你会心一击，笑点会让你笑掉下巴。我觉得一部喜剧电影的成功在于它能不用老梗把观众欢笑地送出电影院。而每部喜剧电影或多或少都会有些荤段子。有些电影处理地不好反而让人反感。而这部电影处理起来就让人看完很舒服。它不是什么很高内涵的电影，但是确是一部老少咸宜，适合一群人一起观看一起欢笑的好电影。值得一看~</p><hr><h3 id="2018-08-05-WEEK204-华盛顿邮报"><a href="#2018-08-05-WEEK204-华盛顿邮报" class="headerlink" title="2018-08-05 WEEK204 华盛顿邮报"></a>2018-08-05 WEEK204 华盛顿邮报</h3><p>华盛顿邮报——————————————The Post<br><img src="https://img.piegg.cn/week204.jpg?imgslim" alt="华盛顿邮报"></p><!--more--><ul><li>导演：史蒂文·斯皮尔伯格</li><li>主演：梅丽尔·斯特里普&#x2F;汤姆·汉克斯&#x2F;莎拉·保罗森&#x2F;鲍勃·奥登科克&#x2F;崔西·莱茨&#x2F;布莱德利·惠特福德&#x2F;布鲁斯·格林伍德&#x2F;马修·瑞斯&#x2F;爱丽森·布里&#x2F;凯莉·库恩&#x2F;杰西·普莱蒙&#x2F;大卫·克罗斯&#x2F;扎克·伍兹&#x2F;帕特·希利&#x2F;约翰·鲁&#x2F;里克·霍姆斯&#x2F;菲利普·卡斯诺夫&#x2F;杰茜·缪勒&#x2F;斯塔克·桑德斯&#x2F;迈克尔·西里尔·克赖顿&#x2F;威尔·丹顿&#x2F;迪尔德丽·罗夫乔&#x2F;迈克尔·斯图巴</li><li>片长：116分钟</li><li>影  片类型：剧情&#x2F;惊悚&#x2F;传记&#x2F;历史</li><li>豆  瓣评分：8.2&#x2F;10(from45,185users)</li><li>IMDB评分：7.2&#x2F;10(from82,885users)</li></ul><p>Hi，各位好久不见。本周给大家推荐的，是来自美国的电影《华盛顿邮报》。我记得此前给大家推荐过《聚焦》，那部电影讲的是波士顿环球报的故事。而本片从片名上你就能看出来，讲述的是华盛顿邮报的故事。同样都是讲报社的电影，两部电影讲出了各自不同的风格，不过同样都很精彩。</p><p>本片基于真实事件改编，剧情总体并不复杂，讲述的是华盛顿邮报揭露美国当时的越战黑幕，与尼克松政府「对着干」的故事。如果说《聚焦》的风格是尽力的克制，那么《华盛顿邮报》的风格就是与之相反的锋芒毕露。美国当时深陷越战泥潭，而政府却把战局的节节败退告知公众于步步胜利。在明知打不赢这场战争的情况下还依然偷偷往越南派兵。如果没有有良知的记着冒死将机密文件从五角大厦里偷出，华盛顿邮报将其公之于众，恐怕越战还将继续持续很久。</p><p>不得不提的是报社与政府之间的较量。他们捍卫着新闻自由，捍卫着美国宪法赋予新闻工作者的权利。「报纸不应为统治者服务，而是应该为被统治者服务」。这部电影虽然讲述的故事发生在40多年前，但是对于当世而言依然有很强烈的警示作用。「如果纽约时报和我们（华盛顿邮报）输了，那么自由新闻才是真的输了。」可以很自然的想到，如果当初华盛顿邮报在于政府的禁令面前败下阵来，那么1年后的水门事件也将同样的被压下来。而曝光水门事件促使尼克松下台的，正是华盛顿邮报。</p><p>看完电影真的非常感动，感动于那些新闻工作者为了国家，为了社会，为了人民在努力追求真相，拼死把真相曝光。可恨在于我们的当下，没有质量、没有深入调查、没有核实来源的假新闻、谣言却铺天盖地。不管是前段时间的「汤兰兰事件」，还是「慈溪被害女生事件」等等新闻，都是为了增加曝光量，却没有考虑到当事人、当事方的感受的新闻。一味追求标题党，撒手把事情甩锅给其他人，让当事人当事方得花十倍百倍的力气去辟谣。这种新闻是可恨的。而那些追求真理，曝光真相的新闻，比如曝光疫苗案、比如毒奶粉案等等的调查记者们，却因为触动了某些人的利益，触动了它们脆弱的神经，遭到掩盖、封杀、甚至人身威胁。演变成现在，我们很多的新闻、很多的细节不得不通过微信公众号、微博截图等等才能看到第一手的材料。因为一旦晚了，就是「该内容已被发布者删除」「该内容因违规无法查看」。这里面也是鱼龙混杂，有的人为了坚持正义，发出的文章无奈被封，而自己的账号也被封禁；有的人为了蹭热点不惜一切代价做出煽情的文章，而变相输出一些谣言。然而人们在这里面获取到信息后通常容易出现广泛传播。当局的做法通常是不论真假一并封杀。</p><p>《中华人民共和国宪法》第二章第三十五条规定：中华人民共和国公民有言论、出版、集会、结社、游行、示威的自由。</p><p>然而今天的真相是我们的「言论自由」是有代价的，通常只要触犯到某些人的利益就会遭到全面的封杀。我不知道它们看过《华盛顿邮报》之后会怎么想，恐怕会很害怕吧。「宜疏不宜堵」「水能载舟亦能覆舟」的道理，小学生都知道。不知道那些口口声声说着「为人民服务」的人，为什么不懂呢。好电影，值得一看。</p><hr><h3 id="2018-07-22-WEEK203-第十二人"><a href="#2018-07-22-WEEK203-第十二人" class="headerlink" title="2018-07-22 WEEK203 第十二人"></a>2018-07-22 WEEK203 第十二人</h3><p>第十二人——————————————Den 12. mann<br><img src="https://img.piegg.cn/week203.jpg?imgslim" alt="第十二人"></p><!--more--><ul><li>导演：哈罗德·兹瓦特</li><li>主演：乔纳森·莱斯·梅耶斯&#x2F;托马斯·古勒斯塔德&#x2F;玛丽·布洛克胡斯&#x2F;维加·霍尔&#x2F;马丁·基弗</li><li>片长：135分钟</li><li>影  片类型：剧情&#x2F;历史&#x2F;战争</li><li>豆  瓣评分：7.8&#x2F;10(from2,665users)</li><li>IMDB评分：7.5&#x2F;10(from4,773users)</li></ul><p>Hi，各位好久不见！好久没给大家推荐战争类型的电影了。本次给大家推荐的是一部来自挪威的电影，讲述了一个12人的小队，最终只有第12个人生还的故事。这部电影最震撼的就是片头说的「这个故事里，最令人难以置信的是，确有其事。」。</p><p>跟敦刻尔克一样，这部电影讲述的不是消灭了多少德军，而是讲述了如何生还（或者「逃跑」）的故事。这个故事讲述的虽然是「一个人」，但是实际上讲的是一群人的故事。「我不是英雄，那些帮助了我的人才是英雄」。这部电影伟大之处在于给予一路上帮助主角的人足够多的镜头和描写。为了帮助主角逃生，很多人甚至会为此付出生命的代价。</p><p>这部取景很「冷」的电影，在冲破国境线的那一刻却格外地热血沸腾。好电影，值得一看。</p><hr><h3 id="2018-07-08-WEEK202-我不是药神"><a href="#2018-07-08-WEEK202-我不是药神" class="headerlink" title="2018-07-08 WEEK202 我不是药神"></a>2018-07-08 WEEK202 我不是药神</h3><p>我不是药神——————————————我不是药神<br><img src="https://img.piegg.cn/week202.jpg?imgslim" alt="我不是药神"></p><!--more--><ul><li>导演：文牧野</li><li>主演：徐峥&#x2F;王传君&#x2F;周一围&#x2F;谭卓&#x2F;章宇&#x2F;杨新鸣&#x2F;王佳佳&#x2F;王砚辉&#x2F;贾晨飞&#x2F;龚蓓苾&#x2F;宁浩&#x2F;李乃文</li><li>片长：117分钟</li><li>影  片类型：剧情&#x2F;喜剧</li><li>豆  瓣评分：9.0&#x2F;10(from350,132users)</li><li>IMDB评分：8.3&#x2F;10(from372users)</li></ul><p>Hi，各位好久不见！本周给大家推荐的是刚上映的大热门《我不是药神》。其实光看名字和海报的时候，我以为只是徐峥的一部常规喜剧电影。然而自从点映以来就有不少朋友给我推荐。于是今天也去电影院看了，发现确实值得上豆瓣9.0的分数。「我们也拍出了韩国那样的电影」。这是我看完感慨最深的一点。</p><p>在审查严苛、国情如此的情况下我们还能拿出一部直击社会问题，反映社会现实和矛盾，并让不少人由衷落泪的电影，真的非常不容易。其实从前年的《湄公河行动》、去年的《战狼2》、今年的《红海行动》之后，我很害怕我们国家以后的「好」电影都只能是这类主旋律的动作片了。我们有《心迷宫》、《暴裂无声》等质量上乘的悬疑电影，也有逗男女老少开心一笑的《泰囧》、《夏洛特烦恼》等优质喜剧片等等。但是我们缺的是直击社会问题，挖掘人性的剧情电影。我们少了多少《辩护人》、少了多少未见的《熔炉》、少了多少难得的《Taxi Driver》。今天一部《我不是药神》让我看到了中国电影的未来还是有希望的。这部基于真实事件改编的电影，从影片一开始就会让你有种深深代入感——因为你也是千千万万中国人之一，这就会是发生在你身边的事。</p><p>不管是配乐、剪辑还是情节的把控，导演和主演们都让我们感到了深深的负责和认真。该给的镜头一个不少，该有的细节一个不缺，该哭的泪点一个不落。你知道这将是中国电影的一个里程碑式的电影么，这么棒的电影真的值得你去一看。</p><hr><h3 id="2018-06-27-WEEK201-寻梦环游记"><a href="#2018-06-27-WEEK201-寻梦环游记" class="headerlink" title="2018-06-27 WEEK201 寻梦环游记"></a>2018-06-27 WEEK201 寻梦环游记</h3><p>寻梦环游记——————————————Coco<br><img src="https://img.piegg.cn/week201.jpg?imgslim" alt="寻梦环游记"></p><!--more--><ul><li>导演：李·昂克里奇&#x2F;阿德里安·莫利纳</li><li>主演：安东尼·冈萨雷斯&#x2F;盖尔·加西亚·贝纳尔&#x2F;本杰明·布拉特&#x2F;阿兰纳·乌巴奇&#x2F;芮妮·维克托&#x2F;杰米·卡米尔&#x2F;阿方索·阿雷奥&#x2F;赫 伯特·西古恩萨&#x2F;加布里埃尔·伊格莱西亚斯&#x2F;隆巴多·博伊尔&#x2F;安娜·奥菲丽亚·莫吉亚&#x2F;娜塔丽·科尔多瓦&#x2F;赛琳娜·露娜&#x2F;爱德华·詹姆斯·奥莫斯&#x2F;索菲亚·伊斯皮诺萨&#x2F;卡拉·梅迪纳&#x2F;黛娅娜·欧特里&#x2F;路易斯·瓦尔德斯&#x2F;布兰卡·阿拉切利&#x2F;萨尔瓦多·雷耶斯&#x2F;切奇·马林&#x2F;奥克塔维·索利斯&#x2F;约翰·拉岑贝格</li><li>片长：105分钟</li><li>影  片类型：喜剧&#x2F;动画&#x2F;音乐&#x2F;奇幻</li><li>豆  瓣评分：9.0&#x2F;10(from509,070users)</li><li>IMDB评分：8.5&#x2F;10(from188,620users)</li></ul><p>Hi，各位好久不见。本周给大家推荐的是来自皮克斯的动画电影《寻梦环游记》。这部电影在当时上映的时候获得了很高的评价。不过我直到最近才看了它。不得不说确实是一部很赞的电影。关于这部电影有个有趣的段子，引进的时候，由于把审片的人都看哭了，所以本来是不能上映的亡灵题材的电影也过审了。暂且不考虑这个说法的真实性，从这个段子里你也能看出这部电影的硬实力确实很强。<br>这部是一部关于音乐，梦想和爱的电影。故事的构思很巧妙，背景设置在墨西哥也是别有一番风味。电影给我们营造了一个及其绚丽的亡灵世界。在这里大部分的生活是快乐的，不过也有令人揪心的问题——如果在人间没有人还记得你的话那么你将会在亡灵世界里消逝。这点真的非常赞，完美诠释了那句话：「人会死三次，第一次是在他停止呼吸的时候 ，从生物学上说他死了，他失去了思考的能力；第二次是在他下葬的时候，人们来参加他的葬礼，怀念他的过往和人生，然后在社会上他死了，活着的世界里不再会有他的位置；第三次是世界上最后一个记得他的人把他忘记的时候，那时候他才能算是真正的死了，永远的死了。」这个设定也带来了剧情的里的矛盾点和发展点。后续的情节铺开张弛有度，有情理之中也有意料之外。感动人心，好电影值得一看~</p><hr><h3 id="2018-06-13-WEEK200-燃烧"><a href="#2018-06-13-WEEK200-燃烧" class="headerlink" title="2018-06-13 WEEK200 燃烧"></a>2018-06-13 WEEK200 燃烧</h3><p>燃烧——————————————버닝<br><img src="https://img.piegg.cn/week200.png?imgslim" alt="燃烧"></p><!--more--><ul><li>导演：李沧东</li><li>主演：刘亚仁&#x2F;史蒂文·元&#x2F;全钟瑞&#x2F;金秀京&#x2F;崔承浩&#x2F;玉子妍</li><li>片长：148分钟</li><li>影  片类型：剧情&#x2F;悬疑</li><li>豆  瓣评分：8.0&#x2F;10(from23,263users)</li><li>IMDB评分：7.9&#x2F;10(from418users)</li></ul><p>Hi，各位好久不见。本周给大家推荐的是今年韩国一部大热的电影《燃烧》。这部电影并不是一部容易读懂的电影。改编自村上春树的《烧仓房》，不过导演也为这部电影注入了很多自己的思想。（建议可以看看《烧仓房》，是部短篇小说）这部电影的节奏比较缓慢，很多细节是慢慢地又完整地展现在你面前。而电影里最让人困惑，或者最烧脑，或者说加入了作者最深入的思考的部分，却又是那些可有可无的「线索」。它们有些到最后都没有得到导演给出的解释。开放式的结局，甚至开放式的剧情都是这部电影非常让人难以缓过神来的地方。而平淡地讲述故事的同时，也有着「最美之舞」的唯美画面。</p><p>聚焦着当下韩国年轻人的痛点，在讲述一场可疑的案件的同时，又让你不得不思考，自己活着是一个「little hunger」还是一个「greate hunger」。而为何有些人，莫名其妙地，年纪轻轻就成了「盖茨比」。不平等的阶层注定不平等的追求。好电影，值得一看。</p><hr><h3 id="2018-05-27-WEEK199-国际市场"><a href="#2018-05-27-WEEK199-国际市场" class="headerlink" title="2018-05-27 WEEK199 国际市场"></a>2018-05-27 WEEK199 国际市场</h3><p>国际市场——————————————국제시장<br><img src="https://img.piegg.cn/week199.jpg?imgslim" alt="国际市场"></p><!--more--><ul><li>导演：尹齐均</li><li>主演：黄晸玟&#x2F;金允珍&#x2F;吴达洙&#x2F;张荣男&#x2F;郑镇荣&#x2F;罗美兰&#x2F;金瑟祺&#x2F;郑允浩&#x2F;Stella Choe</li><li>片长：126分钟</li><li>影  片类型：剧情&#x2F;家庭</li><li>豆  瓣评分：8.3&#x2F;10(from34,907users)</li><li>IMDB评分：7.8&#x2F;10(from2,500users)</li></ul><p>Hi，各位好久不见。本周给大家推荐的是来自韩国的《国际市场》。初看这部电影的名字，并不能看出什么名堂。不过这部电影类似于《阿甘正传》一样，描述了一部韩国的现代史。从6·25事件（可以认为是朝鲜战争全面爆发）开始，一直延续到如今。</p><p>这是目前韩国影史票房第二的电影。能获得如此巨大的成功，我想除了过硬的演员素质（黄晸玟、金允珍、吴达洙等实力派演员），编剧和导演对于情节穿插的到位把控，还有就是唤起了很多韩国人对于朝鲜战争过后，韩国从无到有，从落后朝鲜到超过朝鲜的那段历史的回忆。这部电影以主角德秀的成长作为主线，对父亲的承诺，对妹妹的愧疚，对姑姑的感恩等等。并不断加入一些暗藏的线索或者说「彩蛋」，在整体氛围是催人泪下的情况下，还能掺杂着不少喜剧的成分，非常具有这几年韩国高分电影的风格。</p><p>影片末尾的那句，「但是爸，我真的好累啊」让我不禁落泪。这部电影能直击你内心最容易被触动的角落，好电影，值得一看。</p><hr><h3 id="2018-05-23-WEEK198-暴烈无声"><a href="#2018-05-23-WEEK198-暴烈无声" class="headerlink" title="2018-05-23 WEEK198 暴烈无声"></a>2018-05-23 WEEK198 暴烈无声</h3><p>暴烈无声——————————————Wrath of Silence<br><img src="https://img.piegg.cn/week198.png?imgslim" alt="暴烈无声"></p><!--more--><ul><li>导演：忻钰坤</li><li>主演：宋洋&#x2F;姜武&#x2F;袁文康&#x2F;谭卓&#x2F;王梓尘&#x2F;安琥</li><li>片长：120分钟</li><li>影  片类型：剧情&#x2F;悬疑&#x2F;犯罪</li><li>豆  瓣评分：8.3&#x2F;10(from100,190users)</li><li>IMDB评分：7.2&#x2F;10(from250users)</li></ul><p>Hi，各位好久不见！本周给大家推荐的是来自《心迷宫》导演的第二部佳作《暴烈无声》。好吧依然是一部从名字里看不出说啥的电影。有了《心迷宫》的成功铺垫，《暴烈无声》在经费上也算是大大地改善了。而且宋洋、姜武等实力派演员的出演也是给这部电影添色不少。</p><p>不过相比心迷宫的烧脑，这部电影打出的宣传语是：「烧脑，更烧心」。是的，这部电影虽然是一部悬疑片，但是不仅仅是一部悬疑片。导演其实是想借着这部电影，隐喻现实中的三类人。一类是在社会底层摸爬滚打，有苦说不出，有难没处诉的人；一类是生活过得还可以，努力工作努力养家糊口的中产阶级；一类是生活在社会顶层，物质生活富裕，但是依仗钱、权背地里做些违法乱纪之事的人。</p><p>我们常听「邪不压正，正义或许会迟到，但绝不会缺席」。不过看完这部电影，你会发现其实还是有不少的事，正义的缺席带给你我的，是多么无助，多么的无力。暴烈无声，拳头能换来的，是说不出的绝望。好电影，值得一看。</p><hr><h3 id="2018-05-14-WEEK197-至暗时刻"><a href="#2018-05-14-WEEK197-至暗时刻" class="headerlink" title="2018-05-14 WEEK197 至暗时刻"></a>2018-05-14 WEEK197 至暗时刻</h3><p>至暗时刻——————————————Darkest Hour<br><img src="https://img.piegg.cn/week197.png?imgslim" alt="至暗时刻"></p><!--more--><ul><li>导演：乔·赖特</li><li>主演：加里·奥德曼&#x2F;克里斯汀·斯科特·托马斯&#x2F;莉莉·詹姆斯&#x2F;本·门德尔森&#x2F;斯蒂芬·迪兰&#x2F;萨缪尔·韦斯特&#x2F;汉娜·斯蒂尔&#x2F;罗纳德·皮卡普&#x2F;乔丹·沃勒&#x2F;理查德·拉姆斯登&#x2F;安娜·伯内特&#x2F;尼古拉斯·琼斯&#x2F;查理·帕尔默·罗斯韦尔&#x2F;布赖恩·佩蒂福&#x2F;菲利普·马丁·布朗&#x2F;安杰莉克·琼&#x2F;希尔顿·麦克雷&#x2F;詹姆斯·伊莱斯&#x2F;杰瑞米·查亚德&#x2F;马汀·麦格&#x2F;迈克尔·海登&#x2F;迈克尔·博特</li><li>片长：125分钟</li><li>影  片类型：剧情&#x2F;传记&#x2F;历史</li><li>豆  瓣评分：8.6&#x2F;10(from145,583users)</li><li>IMDB评分：7.4&#x2F;10(from95,095users)</li></ul><p>Hi，各位好久不见。这段时间确实比较忙，一直拖更我表示非常愧疚。本周给大家带来的是一部来自英国的电影《至暗时刻》。别看名字好像阴森恐怖，但是实际上它并不是一部恐怖电影。它是一部讲述英国著名首相丘吉尔的电影。</p><p>丘吉尔在面对重重困难，做出了一系列后世看来非常正确的决策。但是鲜有人知这些决策背后的故事。如果你知道敦刻尔克大撤退，那么你未必知道为了让敦刻尔克的30万英法联军撤回英国，为了多争取时间，牺牲了临近的一个英国旅（4000人）；你未必知道当时的情况下，征用民船已经是无奈之举，而且几乎是千钧一发之际才正好赶上撤军；你也许知道丘吉尔爱抽雪茄，但是你未必知道丘吉尔还嗜酒，个性分明……</p><p>这部电影里，加里·奥德曼把丘吉尔演绎得惟妙惟肖，十分令人印象深刻。也因此大家都在说他要因为出演丘吉尔这个角色拿到奥斯卡小金人了。好电影，值得一看！</p><hr><h3 id="2018-04-23-WEEK196-1987-黎明到来的那一天"><a href="#2018-04-23-WEEK196-1987-黎明到来的那一天" class="headerlink" title="2018-04-23 WEEK196 1987:黎明到来的那一天"></a>2018-04-23 WEEK196 1987:黎明到来的那一天</h3><p>1987:黎明到来的那一天——————————————일구팔칠<br><img src="https://img.piegg.cn/week196.jpg?imgslim" alt="1987:黎明到来的那一天"></p><!--more--><ul><li>导演：张俊焕</li><li>主演：金允锡&#x2F;河正宇&#x2F;柳海真&#x2F;金泰梨&#x2F;朴喜洵&#x2F;李熙俊&#x2F;..</li><li>片长：129分钟</li><li>影  片类型：剧情</li><li>豆  瓣评分：无</li><li>IMDB评分：8.0&#x2F;10(from768users)</li></ul><p>Hi，各位好久不见！本周给大家推荐的一部电影是国内豆瓣都「没有办法出现」的韩国电影《1987:黎明到来的那一天》。因为某些原因这部电影在国内被封杀，所以我也不好说得太多。去看看吧，好电影值得一看。</p><hr><h3 id="2018-04-08-WEEK195-头号玩家"><a href="#2018-04-08-WEEK195-头号玩家" class="headerlink" title="2018-04-08 WEEK195 头号玩家"></a>2018-04-08 WEEK195 头号玩家</h3><p>头号玩家——————————————Ready Player One<br><img src="https://img.piegg.cn/week195.jpg?imgslim" alt="头号玩家"></p><!--more--><ul><li>导演：史蒂文·斯皮尔伯格</li><li>主演：泰伊·谢里丹&#x2F; 奥利维亚·库克&#x2F;本·门德尔森&#x2F;马克·里朗斯&#x2F;丽娜·维特&#x2F;森崎温&#x2F;赵家正&#x2F;西蒙·佩吉&#x2F;T·J·米勒&#x2F;汉娜·乔恩-卡门&#x2F;拉尔夫·尹爱森&#x2F;苏珊·林奇&#x2F;克莱尔·希金斯&#x2F;劳伦斯·斯佩尔曼&#x2F;佩蒂塔·维克斯&#x2F;艾萨克·安德鲁斯</li><li>片长：140分钟</li><li>影  片类型：动作&#x2F;科幻&#x2F;冒险</li><li>豆  瓣评分：8.9&#x2F;10(from311,268users)</li><li>IMDB评分：7.9&#x2F;10(from68,363users)</li></ul><p>Hi，各位好久不见！本周给大家推荐的是最近影院的大热门《头号玩家》。这部电影的豆瓣评分有点「虚高」，不过不可否认确实是一部非常棒的科幻电影。这是斯皮尔伯格送给年轻人、玩家、动漫迷们一份最好的礼物。</p><p>和过往的科幻电影有所不同的是，故事发生在不远的未来，不过科技并没有发展到「变态」的程度。所以电影里的很多东西，包括VR都是在现有的基础上进行的升华。而营造出来的虚拟世界无疑是最吸引眼球的。有人说这部电影是一部彩蛋里插播电影的电影。确实这部电影里彩蛋特别多，但是不用担心，没有人会真的了解所有的彩蛋，所以哪怕你并不关心游戏、动漫、电影，你也能在电影院感受两个半小时的视听盛宴。</p><p>大概最感动的地方就是遇到你所认识、你所熟知的角色、游戏在电影中的一闪而过。你会想起当年在家里打红白机、小霸王的那个年代，你会想起当年守在电视机前只为等待一部好看的动画片的自己。这部电影想要告诉你的也是一样——那些在你脑海中挥之不去的，那些回忆，那些童年才是最真实的。现实世界终究是追求自由追求真实的，在虚拟世界里再如何成功也不过是过眼烟云，现实中的伙伴，生活才是你最应该珍惜的。好电影，值得去一看。</p><hr><h3 id="2018-03-14-WEEK194-启示录"><a href="#2018-03-14-WEEK194-启示录" class="headerlink" title="2018-03-14 WEEK194 启示录"></a>2018-03-14 WEEK194 启示录</h3><p>启示录——————————————Apocalypto<br><img src="https://img.piegg.cn/week194.jpg?imgslim" alt="启示录"></p><!--more--><ul><li>导演：梅尔·吉布森</li><li>主演：鲁迪·杨布拉德&#x2F;达利娅·埃尔南德斯&#x2F;乔纳森·布雷维尔&#x2F;莫里斯·博德耶洛海德&#x2F;劳尔·特鲁希洛&#x2F;赫拉多·塔拉塞纳&#x2F;卡洛斯·伊米里奥·巴厄兹&#x2F;阿米尔卡尔·拉米瑞兹&#x2F;伊斯雷尔·康特雷拉斯&#x2F;伊斯雷尔·里<br>奥斯&#x2F;玛利亚·迪亚兹&#x2F;埃斯皮里迪恩·阿科斯塔·卡奇&#x2F;梅拉·萨布罗&#x2F;伊阿祖娅·拉里奥斯&#x2F;艾贝尔·伍尔里奇</li><li>片长：139分钟</li><li>影  片类型：剧情&#x2F;动作&#x2F;冒险</li><li>豆  瓣评分：8.5&#x2F;10(from46,606users)</li><li>IMDB评分：7.8&#x2F;10(from257,854users)</li></ul><p>Hi，各位好久不见！这部电影是来自公众号的粉丝推荐的一部好电影。很开心来一起分享它！这部电影的背景是玛雅文明衰落时期的故事，但是实际上有些情节又与当时鼎盛的阿兹特克文明有所重合。不过抛去历史背景，这个发生在热带雨林里的故事，却是惊心动魄，让人叹为观止。</p><p>影片的节奏松紧有度。开篇的情节把主要的人物性格、特点都印刻在观众的脑海中。而到了中途，就开始了震撼的追逐。如果从片名里直译的《启示录》里你看不到大致的情节的话，台译版的《阿波卡猎逃》可能就会让你的肾上腺素有所提升。不过如果你读过或者知道圣经里的《启示录》的话，那么这个题目真是太恰当不过了。一场文明的毁灭与新的文明的重生。这是一部让你无法忘却的电影，「文明」世界带去的文明，无非也是野蛮的征服。好电影，值得一看。</p><hr><h3 id="2018-03-14-WEEK193-红海行动"><a href="#2018-03-14-WEEK193-红海行动" class="headerlink" title="2018-03-14 WEEK193 红海行动"></a>2018-03-14 WEEK193 红海行动</h3><p>红海行动——————————————红海行动<br><img src="https://img.piegg.cn/week193.jpg?imageslim" alt="红海行动"></p><!--more--><ul><li>导演：林超贤</li><li>主演：张译&#x2F;黄景瑜&#x2F;海清&#x2F;杜江&#x2F;蒋璐霞&#x2F;尹昉&#x2F;王强&#x2F;郭郁滨&#x2F;王雨甜&#x2F;麦亨利&#x2F;张涵予&#x2F;王彦霖</li><li>片长：138分钟</li><li>影  片类型：剧情&#x2F;动作&#x2F;犯罪</li><li>豆  瓣评分：8.5&#x2F;10(from321,371users)</li><li>IMDB评分：7.6&#x2F;10(from1,254users)</li></ul><p>Hi，各位好久不见！前不久我刚去看了最近大热的《红海行动》。记得当初也给大家推荐过《湄公河行动》和《战狼2》。不得不说这两年来我们自己拍出来的战争、动作片的水准是越来越高了。本片的导演也是《湄公河行动》的导演林超贤。可以说自《湄公河行动》后的两年，真的是卷土重来。并且带来了质量更好，水平更高，更加真实而震撼的场面。</p><p>本片根据真实事件改编，还原度相当高。不仅影片出现的枪械、装备、坦克等都非常写实，而且一些镜头例如汽车炸弹、精密狙击、迫击炮狂轰滥炸等等都有很强的视觉冲击。而最震撼人心的，还有出现的很多「血腥」的场景——以往在国产电影里被剪掉无法搬上荧幕的战争的一些遗体、残骸。而整体剧情也非常紧凑，从头到尾都无尿点啊。而海陆空全面的镜头也让人大呼过瘾。同时，反战的主题也深入人心啊。好电影，值得一看！</p><hr><h3 id="2018-03-01-WEEK192-弱点"><a href="#2018-03-01-WEEK192-弱点" class="headerlink" title="2018-03-01 WEEK192 弱点"></a>2018-03-01 WEEK192 弱点</h3><p>弱点——————————————The Blind Side<br><img src="https://img.piegg.cn/week192.jpg?imageslim" alt="弱点"></p><!--more--><ul><li>导演：约翰·李·汉考克</li><li>主演：桑德拉·布洛克&#x2F;蒂姆·麦格罗&#x2F;昆东·亚伦&#x2F;杰·海德&#x2F;莉莉·柯林斯&#x2F;雷·迈克金农&#x2F;凯西·贝茨</li><li>片长：USA: 129 分钟</li><li>影  片类型：剧情&#x2F;家庭&#x2F;传记&#x2F;运动</li><li>豆  瓣评分：8.4&#x2F;10(from104,599users)</li><li>IMDB评分：7.7&#x2F;10(from248,692users)</li></ul><p>Hi，各位好久不见，新年快乐呀！趁新年还未完全过去，赶紧来给大家推荐每周一部的好电影，拖更了好久哈哈。本周给大家推荐的是一部来自美国的温情电影《弱点》。不过我一直觉得翻译有问题，翻译成「盲点」应该更好点。</p><p>这部电影是根据原著《The Blind Side: Evolution of the Game》改编的电影，而原著的原型也是来自于真实的故事。所以说这部电影的真实感让人非常感动——片中的人大多都超出你的想象的好。与大部分的电影不同的是，它的矛盾点、冲突点特别少。虽然在一些细节的处理上有些过快，不过能够在两个小时里塞进一个橄榄球传奇球员从默默无闻青年时代到最后脱颖而出的选秀状元，可以说是真的很不容易了。我在看的时候一直在惯性思考着等着导演「耍把戏」，不过从头到尾都非常地温馨，非常的动人。</p><p>它是一部能够打动你的泪腺的电影，一部好电影，献给新年的第一部推荐。</p><hr><h3 id="2018-02-04-WEEK191-抓住那个家伙"><a href="#2018-02-04-WEEK191-抓住那个家伙" class="headerlink" title="2018-02-04 WEEK191 抓住那个家伙"></a>2018-02-04 WEEK191 抓住那个家伙</h3><p>抓住那个家伙——————————————몽타주<br><img src="https://img.piegg.cn/week191.jpg?imageslim" alt="抓住那个家伙"></p><!--more--><ul><li>导演：郑根燮</li><li>主演：金相庆&#x2F;严正化&#x2F;宋永彰&#x2F;曹熙奉&#x2F;刘承睦&#x2F;李俊赫&#x2F;朴哲民</li><li>片长：120分钟</li><li>影  片类型：剧情&#x2F;惊悚&#x2F;犯罪</li><li>豆  瓣评分：7.9&#x2F;10(from25,085users)</li><li>IMDB评分：7.5&#x2F;10(from3,342users)</li></ul><p>Hi，各位好久不见！本周依然给大家推荐一部有悬疑色彩的犯罪电影。这部来自韩国的电影从一开始就让我们感到一丝伤感。导演很擅长用暗色调渲染这种压抑而忧伤的气氛。所以从一开始我们就逐渐掉进这个陷阱中了。</p><p>整体上电影是双线并进，两条时间线互相交错，以至于到最后汇合的时候碰撞出的火花恐怕要让你拍案叫绝。两起案件，两个真相。和里面的警察一样，我们大多数人都会被眼前的“证据”蒙蔽，相信一些说不过去的“真相”。而很多事情是需要推敲，需要冷静的。</p><p>受害者也是加害者，这样的双重身份在电影里互相交织，让人性这个词又得以从电影剧本里脱颖而出。事实上，这部电影也是一部关于人性的思考，关于悔过的思考。好电影，值得一看~</p><hr><h3 id="2018-01-28-WEEK190-目击者追凶"><a href="#2018-01-28-WEEK190-目击者追凶" class="headerlink" title="2018-01-28 WEEK190 目击者追凶"></a>2018-01-28 WEEK190 目击者追凶</h3><p>目击者追凶——————————————目擊者<br><img src="https://img.piegg.cn/week190.jpg?imageslim" alt="目击者追凶"></p><!--more--><ul><li>导演：程伟豪</li><li>主演：庄凯勋&#x2F;许玮甯&#x2F;柯佳嬿&#x2F;李铭顺&#x2F;李淳&#x2F;陈彦允&#x2F;郑志伟&#x2F;汤志伟&#x2F;卜国耕</li><li>片长：117分钟</li><li>影  片类型：悬疑&#x2F;惊悚&#x2F;犯罪</li><li>豆  瓣评分：8.1&#x2F;10(from55,592users)</li><li>IMDB评分：7.0&#x2F;10(from1,020users)</li></ul><p>Hi，各位好久不见！本周给大家推荐的是一部来自台湾的悬疑电影《目击者追凶》。我记得去年我曾推荐过一部大热的西班牙悬疑电影《看不见的客人》，精妙的剧本，峰会路转的剧情让很多人拍案叫绝。而这部来自宝岛台湾的电影也不逊色。</p><p>明线、暗线的堆叠，从一开始就埋下的伏笔。导演在一些细节的处理，比如一些闪回的镜头上做的是真的不错的。一次次小规模的反转铸就了最后令人吃惊而毛骨悚然的结局。这部电影里可以说基本没有“好人”。恐怕唯一的好人就是可怜的阿吉了。而所有其他出现在镜头里的主要角色，都有着外表和内在不同的反差。这也是这部电影可圈可点的地方。</p><p>而一部好的悬疑电影自然是到最后一刻才会完美收官。本片也不例外，片末的“鬼故事”，实在是画龙点睛之笔。好电影，值得一看。</p><hr><h3 id="2018-01-21-WEEK189-无问西东"><a href="#2018-01-21-WEEK189-无问西东" class="headerlink" title="2018-01-21 WEEK189 无问西东"></a>2018-01-21 WEEK189 无问西东</h3><p>无问西东——————————————Forever Young<br><img src="https://img.piegg.cn/week189.jpg?imageslim" alt="无问西东"></p><!--more--><ul><li>导演：李芳芳</li><li>主演：章子怡&#x2F;黄晓明&#x2F;张震&#x2F;王力宏&#x2F;陈楚生&#x2F;韩童生&#x2F;王盛德&#x2F;米雪&#x2F;保罗·菲利普·克拉克&#x2F;祖锋&#x2F;铁政&#x2F;章泽天</li><li>片长：138分钟</li><li>影  片类型：剧情&#x2F;爱情&#x2F;战争</li><li>豆  瓣评分：7.5&#x2F;10(from140,810users)</li><li>IMDB评分：6.5&#x2F;10(from138users)</li></ul><p>Hi，各位好久不见！本次给大家推荐的是一部时隔5年重见天日的电影——《无问西东》。这部电影本来是打算献礼给清华大学建校100周年的，因为某些不可知的原因，一再推迟到如今才能上映。剧中的主演也都在这5年中结婚生子，变化之大也令人感慨。</p><p>既然是献礼给清华大学的电影，剧中自然少不了清华大学的身影。电影分成了4段故事来讲述。每段时间内发生的故事都有鲜明的时代特色。而4个故事之间的关联，也在影片“不经意”之间透露出来。而这样的安排也引起了一堆的不认可。然而我却觉得这样的安排非常棒。和云图的前世今生相比，这样的安排不仅更加贴近真实而且更加动人。</p><p>而每个故事里的主人公的演技我认为在当时，哪怕放到现在也都是非常不错的。就像豆瓣里有人说的，“最棒的王力宏和非常好的黄晓明”。两个小时，4个故事，横跨100年。这样庞大的题材，虽然导演确实在某些细节上处理的有些牵强，不过依旧不改这部电影交出的高分答卷。应对人生的选择，人声的苦难，你该如何继续？如果你没有看过《南渡北归》，你无法明白当年西南联大有多么不容易，当年的那些大家能够给本科生上课是有多么珍贵。电影最后给出的一个个那些年顶尖的学者，真是满满的感动和自豪。无问西东，砥砺前行。好电影，值得一看。</p><hr><h3 id="2018-01-08-WEEK188-三块广告牌"><a href="#2018-01-08-WEEK188-三块广告牌" class="headerlink" title="2018-01-08 WEEK188 三块广告牌"></a>2018-01-08 WEEK188 三块广告牌</h3><p>三块广告牌——————————————Three Billboards Outside Ebbing, Missouri<br><img src="https://img.piegg.cn/week188.jpg?imageslim" alt="三块广告牌"></p><!--more--><ul><li>导演：马丁·麦克唐纳</li><li>主演：弗兰西斯·麦克多蒙德&#x2F;伍迪·哈里森&#x2F;山姆·洛克威尔&#x2F;艾比·考尼什&#x2F;卢卡斯·赫奇斯&#x2F;彼特·丁拉基&#x2F;约翰·浩克斯&#x2F;卡赖伯·兰德里·琼斯&#x2F;凯瑟琳·牛顿&#x2F;凯瑞·康顿&#x2F;泽利科·伊万内克&#x2F;萨玛拉·维文&#x2F;克拉克·彼得斯&#x2F;尼克·西塞</li><li>片长：115分钟</li><li>影  片类型：剧情&#x2F;犯罪</li><li>豆  瓣评分：8.7&#x2F;10(from20,455users)</li><li>IMDB评分：8.3&#x2F;10(from31,903users)</li></ul><p>Hi，各位好久不见！忙完考试之后终于有时间来写本周的电影推荐了。本周给大家推荐的是去年底在美国上映（中国将在18年3月上映）的电影《三块广告牌》。</p><p>在这部充满美式幽默和美式愤怒的电影里，看到了久违的不靠煽情而让你动容的一部电影。不过我认为，真正让这部电影拥有如此好评的原因在于两点：</p><ol><li>导演（同时也是编剧）的讲故事的功力深厚</li><li>演员（尤其是科恩嫂）的演技强劲</li></ol><p>这部是一部讲述愤怒与善良，爱与恨的电影。愤怒不能解决问题，但是爱可以。影片塑造的多个人物都具有两面性——这也是本部电影最棒的地方。没有一个人是可以用好或者不好来形容的。每个人都有自己的阳光和阴暗面——而一开始我们不免进入了导演给我们设置的俗套。而随着剧情的发展你才会发现这一切都不是那么简单。“坏”警长其实不坏——相反还非常受人敬仰，“烂”警察其实不烂——相反他还自损三千地只为抓犯人等等。</p><p>而在被套路或者反套路的同时，你也逐渐了解到美国社会的诸多矛盾以及人的诸多美好品质。愤怒不能解决问题，但是善良与爱是可以的。好电影，值得一看。</p><hr><h3 id="2018-01-01-WEEK187-我能说"><a href="#2018-01-01-WEEK187-我能说" class="headerlink" title="2018-01-01 WEEK187 我能说"></a>2018-01-01 WEEK187 我能说</h3><p>我能说——————————————아이 캔 스피크<br><img src="https://img.piegg.cn/week187.jpg?imageslim" alt="我能说" title="我能说"></p><!--more--><ul><li>导演：金炫锡</li><li>主演：罗文姬&#x2F;李帝勋&#x2F;廉惠兰&#x2F;朴哲民&#x2F;李相喜&#x2F;李知勋&#x2F;郑妍周&#x2F;金素真</li><li>片长：119分钟</li><li>影  片类型：剧情&#x2F;喜剧</li><li>豆  瓣评分：8.8&#x2F;10(from19,693users)</li><li>IMDB评分：7.7&#x2F;10(from195users)</li></ul><p>Hi，各位新年快乐<del>转眼之间一周一部好电影已经来到了第5个年头。前四个年头通过一周一部好电影我已经推送了186部各种主题，各种类型，各种风格的电影，希望新的一年里能够有更多的好电影能够分享给各位</del></p><p>本周给大家推荐的电影是一部去年韩国的电影《我能说》。一部能用喜剧的形式来讲述“慰安妇”这个沉痛主题的电影，真的无不佩服编剧的功力以及演员的水平。两个主演的表演真的太动人了。</p><p>这部电影能分成两部分。前半部分以喜剧为主，讲述一个“鬼怪奶奶”的各种“鬼怪”行径。而前半部分用尽努力“掩盖”的欢乐，在后半段会被导演“无情”地打碎，同时打碎的还有观众的泪腺。而同时引出的是这部电影的主题啊——那些被人们忽略的受害者们，那些无法说出，不愿说出，不想说出自己曾经经历的受害者们。她们真的需要更多我们的关怀和帮助。国内今年的《二十二》也是同样的主题。不同的角度，不过都是同样的出发点和同样的愿望——日本政府的一句道歉。好电影，值得一看。</p>]]></content>
    
    
    <summary type="html">&lt;h3 id=&quot;2018-11-11-WEEK210-网络迷踪&quot;&gt;&lt;a href=&quot;#2018-11-11-WEEK210-网络迷踪&quot; class=&quot;headerlink&quot; title=&quot;2018-11-11 WEEK210 网络迷踪&quot;&gt;&lt;/a&gt;2018-11-11 WEEK210 网络迷踪&lt;/h3&gt;&lt;p&gt;网络迷踪——————————————Searching&lt;br&gt;&lt;img src=&quot;https://img.piegg.cn/week210.jpg?imgslim&quot; alt=&quot;网络迷踪&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导演：阿尼什·查甘蒂&lt;/li&gt;
&lt;li&gt;主演：约翰·赵&amp;#x2F;米切尔·拉&amp;#x2F;黛博拉·梅辛&amp;#x2F;约瑟夫·李&amp;#x2F;萨拉·米博·孙&amp;#x2F;亚历克丝·杰恩·高&amp;#x2F;梅金·刘&amp;#x2F;刘卡雅&amp;#x2F;多米尼克·霍夫曼&amp;#x2F;西尔维亚·米纳西安&amp;#x2F;梅丽莎·迪斯尼&amp;#x2F;康纳·麦克雷斯&amp;#x2F;科林·伍德尔&amp;#x2F;约瑟夫·约翰·谢尔勒&amp;#x2F;阿什丽·艾德纳&amp;#x2F;托马斯·巴布萨卡&amp;#x2F;朱莉·内桑森&amp;#x2F;罗伊·阿布拉姆森&amp;#x2F;盖奇·&lt;br&gt;比尔托福&amp;#x2F;肖恩·奥布赖恩&amp;#x2F;瑞克·萨拉比亚&amp;#x2F;布拉德·阿布瑞尔&amp;#x2F;加布里埃尔D·安吉尔&lt;/li&gt;
&lt;li&gt;片长：102分钟&lt;/li&gt;
&lt;li&gt;影  片类型：剧情&amp;#x2F;悬疑&amp;#x2F;惊悚&lt;/li&gt;
&lt;li&gt;豆  瓣评分：8.7&amp;#x2F;10(from85,981users)&lt;/li&gt;
&lt;li&gt;IMDB评分：7.8&amp;#x2F;10(from38,178users)&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    <category term="日志" scheme="https://molunerfinn.com/categories/%E6%97%A5%E5%BF%97/"/>
    
    <category term="周电" scheme="https://molunerfinn.com/categories/%E6%97%A5%E5%BF%97/%E5%91%A8%E7%94%B5/"/>
    
    
    <category term="电影" scheme="https://molunerfinn.com/tags/%E7%94%B5%E5%BD%B1/"/>
    
  </entry>
  
  <entry>
    <title>PicGo的star数破1000的心路历程</title>
    <link href="https://molunerfinn.com/note-for-picgo/"/>
    <id>https://molunerfinn.com/note-for-picgo/</id>
    <published>2018-06-14T15:28:00.000Z</published>
    <updated>2026-03-08T01:14:37.986Z</updated>
    
    <content type="html"><![CDATA[<p>大概半年前（2017年11月28日）我在GitHub上开源了一个基于<a href="https://github.com/SimulatedGREG/electron-vue">electron-vue</a>的开源桌面应用<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>。其出发点是为了改善我在写博客的时候贴图困难的问题。在经过了半年的持续维护和一些宣传（<a href="https://sspai.com/post/42310">《PicGo：基于 Electron 的图片上传工具》</a>、<a href="https://sspai.com/post/44495">《图床上传工具PicGo v1.5更新：支持腾讯云COSv5版本、支持GitHub图床、支持上传前重命名文件等等》</a>等等）后，6月12日，它的star数也终于突破了1000的关卡。在这过程中我也学习了不少东西。在和大家交流的过程中，我才发现原来大家都有着这些需求，才发现我一开始的实现思路并非到位等等。谨以此文记录与PicGo有关的我的心路历程。</p><blockquote><p>赶巧前不久也有一个开发者chyingp的开源项目破了1000star，也有着类似的<a href="https://juejin.im/post/5b1717a86fb9a01e3e5ce540">文章</a>，祝贺！</p></blockquote><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/8700af19ly1fs892cewamj21ks0emq5n.jpg"></p><span id="more"></span><h2 id="项目诞生"><a href="#项目诞生" class="headerlink" title="项目诞生"></a>项目诞生</h2><p>我以前写博客的时候，由于一开始用的是七牛的图床，所以遇到要在markdown里贴图的时候就必须登录七牛，然后手动上传图片，再找到按钮来复制链接，然后复制到markdown里。要在markdown里显示一张图片我得经过上述4个步骤。</p><p>我自己的笔记本用的是mac，所以我就接触到了一款叫做<a href="https://toolinbox.net/iPic/">iPic</a>的图床神器。在用它的时候我也知道了微博图床。iPic的功能和体验真的特别好。不过如果需要使用七牛等其他图床的时候，我就需要付费了。其实如果iPic支持windows的话，我可能就真的去付费了。（因为实验室的电脑是windows，所以我平时在实验室里得用windows来写东西）。仅为mac一个平台付费我有点不能接受。</p><p>于是我就在想能否我自己写一个工具来简化我的上传图片的流程呢，这个应用可以实现拖拽图片就上传，然后上传完自动复制链接到剪贴板里，我就只要粘贴到markdown里就好了。一开始想用swift写mac的应用，用C#写windows的应用。后来发现工作量不仅大，而且学习成本也很高。于是最后还是选择投入electron的怀抱。</p><p>一开始不选择electron主要是因为我的印象中electron的应用体积都挺大的（100MB以上），所以感觉体积可能有点不友好。不过后来我在用了<code>electron-vue</code>打包出来后发现体积是可以接受的范围（mac端大概50M，windows端大概38M），于是就决定用它来写了。用<code>electron-vue</code>的主要原因是我写vue比较多，想要学习成本低一些，这样开发只要学electron的部分就好了。</p><p>说干就干，在去年年底（11月下旬）我开始写这个应用。</p><h2 id="项目发布"><a href="#项目发布" class="headerlink" title="项目发布"></a>项目发布</h2><blockquote><p>文末会给出electron-vue开发的系列经验教程</p></blockquote><p>经过20多天的间断性地开发，我在12月12号发布了1.0.0版本。由于一开始是在mac上开发的，所以1.0.0版本也只支持了macOS。一开始支持的图床也不多，只支持了微博和七牛两个图床。</p><p>1.0.0版本的截图如下：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/34242310-b5056510-e655-11e7-8568-60ffd4f71910.gif"></p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/34242857-d177930a-e658-11e7-9688-7405851dd5e5.gif"></p><p>基本实现了我预期的功能，类似iPic能够通过拖拽到顶部栏图标上传。并且为了今后支持windows平台（windows平台的任务栏图标不支持拖拽事件），我就做了一个主窗口，在主窗口里也有拖拽上传的区域。因为有了主窗口，我就顺便把图床的配置也放到了主窗口里。</p><p>应用做出来了，我也想让更多的人用到。于是我在北邮人论坛、cnode、v2ex还有掘金都发了文章。不过一开始看到的人寥寥无几，发了文章也没多少人看到和使用。后来我在少数派上发了同样的文章，意外地被推荐到了首页。</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/8700af19ly1fmvr6uah8rj21z20vk7wh"></p><p>这次的契机让PicGo意外地有了些用户和star数。在跟使用者交流的过程中我也开始逐步往PicGo里加功能和修复bug。在1月10日的时候，PicGo更新v1.3.1版本支持了windows系统。</p><p>因为开始有用户了，PicGo早期确实存在着不少功能的缺失，比如<code>快捷键上传</code>，其他常用图床的缺失等等。所以那时候是PicGo迭代最快的一段时间。通过大家在issue里的反馈，我也在不断打磨PicGo。可以看到截止6月14日，已经有61个被关闭的issue了。</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/5b2223c52853f.png"></p><h2 id="项目改进"><a href="#项目改进" class="headerlink" title="项目改进"></a>项目改进</h2><p>用户体验这个东西真的并不是开发者在开发的时候能够立马就想到的。这点在开发PicGo上我体会很深。</p><p>比如增加<code>快捷键上传</code>这个功能，我一开始觉得自定义快捷键写起来比较麻烦，干脆我定一个大家基本用不到的快捷键吧。于是我默认给了一个【command&#x2F;ctrl+shift+p】的快捷键。我自己用的时候没有什么问题。结果有人给我反馈说，快捷键跟某某某软件冲突了，能否给一个快捷键自定义的功能。这是我无法回避的一个问题。于是我就开始去学习如何加入自定义快捷键。并在不久之后实现了个这个<a href="https://github.com/Molunerfinn/PicGo/commit/37a784225e90c9d115367f056957dac88ebcf816">功能</a>。</p><p>比如自定义链接格式的问题。我一开始给了大家4种复制链接的格式，分别是<code>markdown</code>、<code>HTML</code>、<code>URL</code>、<code>UBB</code>。本来以为这4种格式就足够大家平时使用了。后来有人提了一个<a href="https://github.com/Molunerfinn/PicGo/issues/25">issue</a>，问PicGo能否自定义链接格式，因为他想基于HTML增加一些属性，比如大小居中等。我觉得这个使用场景确实是有的，于是我便在后来的某个<a href="https://github.com/Molunerfinn/PicGo/commit/4010a09fe48d8109456c3c1b37695f177336f2e4">提交</a>里实现了这个功能。</p><p>当然并不是大家有这个需求我就一定要做。还有一些需求我觉得并不符合我对于PicGo的定位的，那么我就会给予回绝。比如<a href="https://github.com/Molunerfinn/PicGo/issues/53">后期能否支持上传视频文件？</a>，由于PicGo的开发初衷只针对图片，所以在流程上（图片-&gt;base64）就不允许上传视频文件。于是我拒绝了这个需求。</p><p>还有一个对我以及PicGo这个项目影响深远的<a href="https://github.com/Molunerfinn/PicGo/issues/26">issue</a>，ZetaoYang提出了一个想法：</p><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/5b2228f31219a.png"></p><p>这个建议改变了我对PicGo开发的后续想法。我思考了好久，发现确实一步步增加默认的图床支持是不长远的。一个是重复性劳动太多（图床上传除了协议和加密方式不同之外，接收文件，转成base64和最后上传成功后存到本地的流程是一样的），一个是无止尽的图床支持其实也不应该。相比之下，把PicGo做成一个Core+Plugin模式的应用会更好。其中Core的部分可以单独只做图片接收和转码，并预留一些生命周期，供上传过程中不同的需求来调用。Core的部分可以单独发布成一个npm包。Plugin可以实现接入Core的生命周期，可以实现自己的上传逻辑，可以实现图片压缩、加水印等等其他功能。而PicGo只是在Core+Plugin的基础上套了一层electron的皮方便普通用户使用，而Core和Plugin可以独立拆出方便开发者使用和开发。这个也是PicGo的2.0版本将要做的事。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在开发PicGo的过程中我也深刻了解到，写一个DEMO不难，给这个DEMO注入你自己的思想和灵魂是难的。PicGo从一个一开始只是我想简化上传图片流程的玩具应用，发展到现在已经是不少用户的效率工具而言，其实一路走来也并不容易。现在大家对用户体验的要求越来越高，如果只沉醉在自己的DEMO里无法自拔，只会被更好的产品所淘汰。</p><p>开发PicGo也是一件很开心的事。大家给予我的赞赏和感谢，都是给我继续开发的动力。而我也发现越来越多的文章里，都提到了PicGo。如下：</p><ul><li><a href="https://juejin.im/post/5af0021e518825671547926e">《老司机的神兵利器-效率工具》</a></li><li><a href="https://imwyc.com/picgo/">《PicGo 强大的免费图床工具》</a></li><li><a href="https://lai.yuweining.cn/archives/2035/">《PicGo：开源的图片管理工具》</a></li><li><a href="https://blog.csdn.net/weixin_39200308/article/details/80644336">《图床神器PicGo》</a></li><li><a href="https://zhuanlan.zhihu.com/p/37873730">《提升生活品质——个人效率工具与资讯网站推荐》</a></li><li><a href="https://sspai.com/post/44150">《7 款 Windows 国产软件推荐》</a></li></ul><p>我想，得到你们的认可，把它写进你们的文章，这是对我最大的肯定，这个比star数更令我感到开心。</p><p>我在开发PicGo的过程中，也写了一个系列文章<a href="https://molunerfinn.com/tags/Electron-vue/">Electron-vue开发实战</a>。如果你也想学习electron或者electron-vue的开发的话，希望我的文章能够给你带来帮助。如果你之前没有听说过PicGo，那么不妨试试；如果你觉得它挺好用的，不妨点个star~</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;大概半年前（2017年11月28日）我在GitHub上开源了一个基于&lt;a href=&quot;https://github.com/SimulatedGREG/electron-vue&quot;&gt;electron-vue&lt;/a&gt;的开源桌面应用&lt;a href=&quot;https://github.com/Molunerfinn/PicGo&quot;&gt;PicGo&lt;/a&gt;。其出发点是为了改善我在写博客的时候贴图困难的问题。在经过了半年的持续维护和一些宣传（&lt;a href=&quot;https://sspai.com/post/42310&quot;&gt;《PicGo：基于 Electron 的图片上传工具》&lt;/a&gt;、&lt;a href=&quot;https://sspai.com/post/44495&quot;&gt;《图床上传工具PicGo v1.5更新：支持腾讯云COSv5版本、支持GitHub图床、支持上传前重命名文件等等》&lt;/a&gt;等等）后，6月12日，它的star数也终于突破了1000的关卡。在这过程中我也学习了不少东西。在和大家交流的过程中，我才发现原来大家都有着这些需求，才发现我一开始的实现思路并非到位等等。谨以此文记录与PicGo有关的我的心路历程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;赶巧前不久也有一个开发者chyingp的开源项目破了1000star，也有着类似的&lt;a href=&quot;https://juejin.im/post/5b1717a86fb9a01e3e5ce540&quot;&gt;文章&lt;/a&gt;，祝贺！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-1251750343.cos.ap-beijing.myqcloud.com/8700af19ly1fs892cewamj21ks0emq5n.jpg&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="Nodejs" scheme="https://molunerfinn.com/tags/Nodejs/"/>
    
    <category term="Vue" scheme="https://molunerfinn.com/tags/Vue/"/>
    
    <category term="Electron-vue" scheme="https://molunerfinn.com/tags/Electron-vue/"/>
    
  </entry>
  
  <entry>
    <title>小记VSCode插件amVim的改进以及插件开发</title>
    <link href="https://molunerfinn.com/vscode-extension-develop-1/"/>
    <id>https://molunerfinn.com/vscode-extension-develop-1/</id>
    <published>2018-06-13T14:09:00.000Z</published>
    <updated>2026-03-08T01:14:37.987Z</updated>
    
    <content type="html"><![CDATA[<p>前一段时间在Mac上用VSCode的时候，发现<code>VSCodeVim</code>这个插件严重拖慢了我的开发效率。本来用<code>Vim</code>模式难道不应该是提高效率么？问题是在<code>Normal</code>模式下，光标的移动会有肉眼可见的长延时。比如我按着<code>j</code>，等我松开<code>j</code>后，光标还在移动，而且还移动了一会儿。预期的效果应该是按下移动，松开停止。为此我查了一下相关<a href="https://github.com/VSCodeVim/Vim/issues/2021">issue</a>，发现跟我一样的情况的人还不少。（不过也有不少人没有这个问题，貌似跟显卡有关系？我的mac是集显的）。</p><p>卸载了<code>VSCodeVim</code>之后，光标移动的速度又恢复了正常，不过没有<code>Vim</code>模式的话非常别扭。所以我就开始看看VSCode还有没有其他<code>Vim</code>模式的插件。于是我又试了另外两个插件：<a href="https://github.com/74th/vscode-vim">vimStyle</a>和<a href="https://github.com/aioutecism/amVim-for-VSCode">amVim</a>。最终我选择了后者。不仅是支持的Vim命令更多，还有就是开发者的维护一直在继续。而且很关键的一点，<code>amVim</code>的光标移动体验就是 <strong>如丝般顺滑</strong> ！</p><p>不过它有个让我很不习惯的地方：不支持<code>:</code>号调起VSCode的<code>Command Line</code>窗口，实现诸如<code>:w</code>保存，<code>:wq</code>退出等常见功能。这些功能在<code>VSCodeVim</code>里是支持的。于是我就在想有没有办法「移植」一下<code>VSCodeVim</code>的功能到<code>amVim</code>来，既能保持光标移动体验顺滑，又能用上<code>Command Line</code>的一些常用命令。所以开启了魔改模式，并在跟开发者的一系列交流后最终我提交的PR被<a href="https://github.com/aioutecism/amVim-for-VSCode/pull/199">merge</a>了。<br><img src="https://i.loli.net/2018/06/06/5b179b533f190.png"><br>本文记录一下我第一次对VSCode插件（修改）开发的过程。</p><span id="more"></span><h2 id="修改插件"><a href="#修改插件" class="headerlink" title="修改插件"></a>修改插件</h2><h3 id="开发前的准备"><a href="#开发前的准备" class="headerlink" title="开发前的准备"></a>开发前的准备</h3><p>VSCode的插件通常是用<code>TypeScript</code>来写的。如果你需要开发或者修改它，先要拥有<code>TypeScript</code>的开发环境。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm install -g typescript</span><br><span class="line"><span class="comment"># or</span></span><br><span class="line">yarn global add typescript</span><br></pre></td></tr></table></figure><p>通常<code>TypeScript</code>的项目都会用上<code>tslint</code>。所以你也最好全局安装它：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm install -g tslint</span><br><span class="line"><span class="comment"># or</span></span><br><span class="line">yarn global add tslint</span><br></pre></td></tr></table></figure><p>然后打开VSCode，安装一下<code>tslint</code>这个插件，它将通过我们上面安装在系统里的<code>tslint</code>给我们的项目提供代码检查。</p><p>修改别人的插件，可以先<code>fork</code>一份别人的代码。也为了之后方便提PR做准备。</p><p>然后就可以把插件<code>clone</code>到本地了。比如本文的<a href="https://github.com/aioutecism/amVim-for-VSCode">amVim-for-VSCode</a>。</p><h3 id="运行插件"><a href="#运行插件" class="headerlink" title="运行插件"></a>运行插件</h3><p>用VSCode打开这个项目，点击左侧的<code>debug</code>可以看到一个<code>launch extension</code>的配置：</p><p><img src="https://i.loli.net/2018/06/06/5b17a25905266.png"></p><p>运行它，你会得到另外一个窗口，这个就是可以调试插件功能的窗口了：</p><p><img src="https://i.loli.net/2018/06/06/5b17a2e5d0a1b.png"></p><h3 id="改进插件"><a href="#改进插件" class="headerlink" title="改进插件"></a>改进插件</h3><blockquote><p>我的改进源码在这里：<a href="https://github.com/Molunerfinn/amVim-for-VSCode">https://github.com/Molunerfinn/amVim-for-VSCode</a> 作者合并之后做了一些修改，本文是以我的版本为主。</p></blockquote><p>为了实现<code>VSCodeVim</code>通过<code>:</code>调起VSCode的<code>inputBox</code>效果，我需要翻阅一下<code>VSCodeVim</code>的源代码。</p><p>大致效果如下：</p><p><img src="https://user-images.githubusercontent.com/12621342/40241750-61d5160c-5aee-11e8-9d21-6f96cbc4fa88.gif"></p><p>在查看了<code>amVim</code>和<code>VSCodeVim</code>在实现命令上的部分源码后，发现二者的实现上差距还是不小的。不过相比<code>VSCodeVim</code>代码的庞大（甚至还有neoVim的支持），<code>amVim</code>在实现上就比较精巧了。</p><p>在我的PR未被merge之前，<code>amVim</code>插件提供了一个功能，按<code>:</code>打开一个<code>GoToLine</code>的<code>inputBox</code>：</p><p><img src="https://i.loli.net/2018/06/06/5b17a73b1bf15.png"></p><p>不过只能用于输入数字并跳转到相应行数。好在查看release更新日志，追溯这个<a href="https://github.com/aioutecism/amVim-for-VSCode/pull/192">commit</a>，我们可以很容易找到它是如何实现的。</p><p><img src="https://i.loli.net/2018/06/06/5b17aa66c2030.png"></p><p>代码不多，就几行：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/Modes/Normal.ts</span></span><br><span class="line">&#123; <span class="attr">keys</span>: <span class="string">&#x27;:&#x27;</span>, <span class="attr">actions</span>: [<span class="title class_">ActionCommand</span>.<span class="property">goToLine</span>] &#125;, <span class="comment">// 增加`:`打开GoToLine的inputBox的快捷键</span></span><br></pre></td></tr></table></figure><p>具体实现代码如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/Actions/Command.ts</span></span><br><span class="line"><span class="keyword">import</span> &#123;commands&#125; <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">ActionCommand</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> <span class="title function_">goToLine</span>(): <span class="title class_">Thenable</span>&lt;boolean | <span class="literal">undefined</span>&gt; &#123;</span><br><span class="line">        <span class="keyword">return</span> commands.<span class="title function_">executeCommand</span>(<span class="string">&#x27;workbench.action.gotoLine&#x27;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以是通过<code>vscode</code>的<code>commands</code>来打开的<code>gotoLine</code>的<code>inputBox</code>窗口。</p><p>再来看看<code>VSCodeVim</code>是如何打开<code>inputBox</code>的：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/cmd_line/commandLine.ts</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">CommandLine</span> &#123;</span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> <span class="title class_">PromptAndRun</span>(<span class="attr">initialText</span>: <span class="built_in">string</span>, <span class="attr">vimState</span>: <span class="title class_">VimState</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">if</span> (!vscode.<span class="property">window</span>.<span class="property">activeTextEditor</span>) &#123;</span><br><span class="line">      <span class="title class_">Logger</span>.<span class="title function_">debug</span>(<span class="string">&#x27;CommandLine: No active document&#x27;</span>);</span><br><span class="line">      <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> cmd = <span class="keyword">await</span> vscode.<span class="property">window</span>.<span class="title function_">showInputBox</span>(<span class="variable language_">this</span>.<span class="title function_">getInputBoxOptions</span>(initialText)); <span class="comment">// 通过showInputBox打开</span></span><br><span class="line">    <span class="keyword">if</span> (cmd &amp;&amp; cmd[<span class="number">0</span>] === <span class="string">&#x27;:&#x27;</span> &amp;&amp; configuration.<span class="property">cmdLineInitialColon</span>) &#123;</span><br><span class="line">      cmd = cmd.<span class="title function_">slice</span>(<span class="number">1</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">_history</span>.<span class="title function_">add</span>(cmd);</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">_history</span>.<span class="title function_">save</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> <span class="title class_">CommandLine</span>.<span class="title class_">Run</span>(cmd!, vimState);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ...</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">static</span> <span class="title function_">getInputBoxOptions</span>(<span class="attr">text</span>: <span class="built_in">string</span>): vscode.<span class="property">InputBoxOptions</span> &#123; <span class="comment">// inputBox的Options</span></span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">      <span class="attr">prompt</span>: <span class="string">&#x27;Vim command line&#x27;</span>,</span><br><span class="line">      <span class="attr">value</span>: configuration.<span class="property">cmdLineInitialColon</span> ? <span class="string">&#x27;:&#x27;</span> + text : text,</span><br><span class="line">      <span class="attr">ignoreFocusOut</span>: <span class="literal">false</span>,</span><br><span class="line">      <span class="attr">valueSelection</span>: [</span><br><span class="line">        configuration.<span class="property">cmdLineInitialColon</span> ? text.<span class="property">length</span> + <span class="number">1</span> : text.<span class="property">length</span>,</span><br><span class="line">        configuration.<span class="property">cmdLineInitialColon</span> ? text.<span class="property">length</span> + <span class="number">1</span> : text.<span class="property">length</span>,</span><br><span class="line">      ],</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到关键的部分是通过<code>vscode.window.showInputBox</code>打开的<code>inputBox</code>。所以我也根据这个关键的入口来一步步实现我想要的功能。</p><h4 id="功能分析"><a href="#功能分析" class="headerlink" title="功能分析"></a>功能分析</h4><p>参考<code>VSCodeVim</code>的实现，在<code>amVim</code>里可以大概分四个部分：</p><ol><li><code>src/Modes/Normal.ts</code>作为入口文件，当用户输入<code>:</code>键时触发后续功能。【已有】</li><li><code>src/Actions/CommandLine/CommandLine.ts</code>作为打开<code>inputBox</code>的入口函数，打开<code>inputBox</code>，然后负责把用户输入的内容传给下一级的<code>parser</code>，用于解析并执行相应命令。</li><li><code>src/Actions/CommandLine/Parser.ts</code>，负责接收上一级传进来的命令，然后找到命令对应的函数，并执行该函数。如果找不到相应则返回。</li><li><code>src/Actions/CommandLine/Commands/*</code>，存放各个命令的实现函数。</li></ol><p>其中<code>src/Actions/CommandLine/CommandLine.ts</code>的逻辑跟<code>VSCodeVim</code>的<code>src/cmd_line/commandLine.ts</code>非常类似。</p><h4 id="具体实现"><a href="#具体实现" class="headerlink" title="具体实现"></a>具体实现</h4><ol><li>src&#x2F;Actions&#x2F;CommandLine&#x2F;CommandLine.ts</li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; parser &#125; <span class="keyword">from</span> <span class="string">&#x27;./Parser&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">class</span> <span class="title class_">CommandLine</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> <span class="title class_">Run</span>(<span class="attr">command</span>: <span class="built_in">string</span> | <span class="literal">undefined</span>): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">      <span class="keyword">if</span> (!command || command.<span class="property">length</span> === <span class="number">0</span>) &#123; <span class="comment">// 如果命令为空则直接返回</span></span><br><span class="line">          <span class="keyword">return</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">          <span class="keyword">const</span> cmd = <span class="title function_">parser</span>(command); <span class="comment">// 将命令传给parser并返回一个可执行的函数</span></span><br><span class="line">          <span class="keyword">if</span> (cmd) &#123;</span><br><span class="line">              <span class="keyword">await</span> cmd.<span class="title function_">execute</span>(command); <span class="comment">// 调用该函数的execute方法</span></span><br><span class="line">          &#125;</span><br><span class="line">      &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">          <span class="variable language_">console</span>.<span class="title function_">error</span>(e);</span><br><span class="line">      &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">async</span> <span class="title class_">PromptAndRun</span>(): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">      <span class="keyword">if</span> (!vscode.<span class="property">window</span>.<span class="property">activeTextEditor</span>) &#123; <span class="comment">// 如果当前没有打开的激活的文本，则命令不执行，返回空。</span></span><br><span class="line">          <span class="keyword">return</span>;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">          <span class="keyword">let</span> cmd = <span class="keyword">await</span> vscode.<span class="property">window</span>.<span class="title function_">showInputBox</span>(<span class="title class_">CommandLine</span>.<span class="title function_">getInputBoxOptions</span>()); <span class="comment">// 打开inputBox</span></span><br><span class="line">          <span class="keyword">if</span> (cmd &amp;&amp; cmd[<span class="number">0</span>] === <span class="string">&#x27;:&#x27;</span>) &#123;</span><br><span class="line">              cmd = cmd.<span class="title function_">slice</span>(<span class="number">1</span>); <span class="comment">// 如果命令带有:则将它去掉并传给parser</span></span><br><span class="line">          &#125;</span><br><span class="line">          <span class="keyword">return</span> <span class="keyword">await</span> <span class="title class_">CommandLine</span>.<span class="title class_">Run</span>(cmd);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (e) &#123;</span><br><span class="line">          <span class="variable language_">console</span>.<span class="title function_">error</span>(e);</span><br><span class="line">      &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">static</span> <span class="title function_">getInputBoxOptions</span>(): vscode.<span class="property">InputBoxOptions</span> &#123; <span class="comment">// 打开的inputBox框里的文本和一些其他配置</span></span><br><span class="line">      <span class="keyword">return</span> &#123;</span><br><span class="line">          <span class="attr">prompt</span>: <span class="string">&#x27;Vim command line&#x27;</span>,</span><br><span class="line">          <span class="attr">value</span>: <span class="string">&#x27;:&#x27;</span>,</span><br><span class="line">          <span class="attr">ignoreFocusOut</span>: <span class="literal">false</span>,</span><br><span class="line">          <span class="attr">valueSelection</span>: [<span class="number">1</span>, <span class="number">1</span>]</span><br><span class="line">      &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>src&#x2F;Actions&#x2F;CommandLine&#x2F;Parser.ts</li></ol><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">CommandBase</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./Commands/Base&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">WriteCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/Write&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">WallCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/WriteAll&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">QuitCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/Quit&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">QuitAllCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/QuitAll&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">WriteQuitCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/WriteQuit&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">WriteQuitAllCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/WriteQuitAll&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">VisualSplitCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/VisualSplit&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">NewFileCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/NewFile&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">VerticalNewFileCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/VerticalNewFile&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">GoToLineCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Commands/GoToLine&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> commandParsers = &#123; <span class="comment">// 对于命令的解析，用哈希表做映射</span></span><br><span class="line">    <span class="attr">w</span>: <span class="title class_">WriteCommand</span>,</span><br><span class="line">    <span class="attr">write</span>: <span class="title class_">WriteCommand</span>,</span><br><span class="line">    <span class="attr">wa</span>: <span class="title class_">WallCommand</span>,</span><br><span class="line">    <span class="attr">wall</span>: <span class="title class_">WallCommand</span>,</span><br><span class="line"></span><br><span class="line">    <span class="attr">q</span>: <span class="title class_">QuitCommand</span>,</span><br><span class="line">    <span class="attr">quit</span>: <span class="title class_">QuitCommand</span>,</span><br><span class="line">    <span class="attr">qa</span>: <span class="title class_">QuitAllCommand</span>,</span><br><span class="line">    <span class="attr">qall</span>: <span class="title class_">QuitAllCommand</span>,</span><br><span class="line"></span><br><span class="line">    <span class="attr">wq</span>: <span class="title class_">WriteQuitCommand</span>,</span><br><span class="line">    <span class="attr">x</span>: <span class="title class_">WriteQuitCommand</span>,</span><br><span class="line"></span><br><span class="line">    <span class="attr">wqa</span>: <span class="title class_">WriteQuitAllCommand</span>,</span><br><span class="line">    <span class="attr">wqall</span>: <span class="title class_">WriteQuitAllCommand</span>,</span><br><span class="line">    <span class="attr">xa</span>: <span class="title class_">WriteQuitAllCommand</span>,</span><br><span class="line">    <span class="attr">xall</span>: <span class="title class_">WriteQuitAllCommand</span>,</span><br><span class="line"></span><br><span class="line">    <span class="attr">vs</span>: <span class="title class_">VisualSplitCommand</span>,</span><br><span class="line">    <span class="attr">vsp</span>: <span class="title class_">VisualSplitCommand</span>,</span><br><span class="line"></span><br><span class="line">    <span class="attr">new</span>: <span class="title class_">NewFileCommand</span>,</span><br><span class="line">    <span class="attr">vne</span>: <span class="title class_">VerticalNewFileCommand</span>,</span><br><span class="line">    <span class="attr">vnew</span>: <span class="title class_">VerticalNewFileCommand</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">parser</span>(<span class="params"><span class="attr">input</span>: <span class="built_in">string</span></span>): <span class="title class_">CommandBase</span> | <span class="literal">undefined</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (commandParsers[input]) &#123;</span><br><span class="line">        <span class="keyword">return</span> commandParsers[input]; <span class="comment">// 接收inputBox里传来的命令</span></span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="title class_">Number</span>.<span class="title function_">isInteger</span>(<span class="title class_">Number</span>(input))) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="title class_">GoToLineCommand</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">undefined</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>命令的实现</li></ol><p>由于命令很多，我就举三个例子。一个是<code>w</code>，一个是<code>q</code>，和一个<code>wq</code>。VSCode自己的一些功能比如关闭当前文件、保存文件等都是有自己的command的。在实现Vim模式的时候，实际上最后也是去调用VSCode自带的功能而已。</p><h5 id="Write"><a href="#Write" class="headerlink" title="Write"></a>Write</h5><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">CommandBase</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./Base&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">WriteCommand</span> <span class="keyword">extends</span> <span class="title class_ inherited__">CommandBase</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">execute</span>(): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123; <span class="comment">// 暴露execute方法用于调用</span></span><br><span class="line">    <span class="keyword">await</span> vscode.<span class="property">commands</span>.<span class="title function_">executeCommand</span>(<span class="string">&#x27;workbench.action.files.save&#x27;</span>); <span class="comment">// 调用vscode的命令保存文件</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">new</span> <span class="title class_">WriteCommand</span>();</span><br></pre></td></tr></table></figure><h5 id="Quit"><a href="#Quit" class="headerlink" title="Quit"></a>Quit</h5><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> vscode <span class="keyword">from</span> <span class="string">&#x27;vscode&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">CommandBase</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./Base&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">QuitCommand</span> <span class="keyword">extends</span> <span class="title class_ inherited__">CommandBase</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">execute</span>(): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> vscode.<span class="property">commands</span>.<span class="title function_">executeCommand</span>(<span class="string">&#x27;workbench.action.closeActiveEditor&#x27;</span>); <span class="comment">// 调用vscode的命令关闭当前的文件</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">new</span> <span class="title class_">QuitCommand</span>();</span><br></pre></td></tr></table></figure><h5 id="WriteQuit"><a href="#WriteQuit" class="headerlink" title="WriteQuit"></a>WriteQuit</h5><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">CommandBase</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;./Base&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">WriteCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Write&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="title class_">QuitCommand</span> <span class="keyword">from</span> <span class="string">&#x27;./Quit&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">WriteQuitCommand</span> <span class="keyword">extends</span> <span class="title class_ inherited__">CommandBase</span> &#123;</span><br><span class="line">  <span class="title function_">constructor</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="variable language_">super</span>();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">async</span> <span class="title function_">execute</span>(): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt; &#123;</span><br><span class="line">    <span class="keyword">await</span> <span class="title class_">WriteCommand</span>.<span class="title function_">execute</span>();</span><br><span class="line">    <span class="keyword">await</span> <span class="title class_">QuitCommand</span>.<span class="title function_">execute</span>();</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">new</span> <span class="title class_">WriteQuitCommand</span>();</span><br></pre></td></tr></table></figure><p>这一步就很有意思了，因为我们之前实现了<code>Write</code>和<code>Quit</code>的功能，所以可以在这里调用它们。看到这里你可能会有问题，虽然我知道VSCode有这些功能，但是你是怎么知道这些功能是怎么写的呢？</p><p>如果只是我这篇文章的话，我在实现Vim模式的这些命令的时候，大部分是参考了<code>VSCodeVim</code>的一些写法。它主要的命令实现在<code>src/cmd_line/commands/*</code>里。但是只这样显然还是不够的。因此我给出几个比较有用的地方供大家开发插件的时候参考：</p><ol><li>VSCode官方文档里的<a href="https://code.visualstudio.com/docs/extensions/overview">Extending Visual Studio Code</a>，介绍扩展VSCode的原理和给出了一些例子。</li><li>VSCode官方文档里的<a href="https://code.visualstudio.com/docs/extensionAPI/overview">Extensibility Reference</a>，介绍VSCode扩展的api文档。</li><li>VSCode官方文档里的<a href="https://code.visualstudio.com/docs/getstarted/keybindings">Key Bindings for Visual Studio Code</a>，介绍VSCode的快捷键和相应的<strong>命令id</strong>。</li><li>VSCode本身的快捷键编辑面板：<br><img src="https://i.loli.net/2018/06/13/5b20c5d23fda2.png"></li></ol><p>说实话VSCode的文档写得不是特别好。我要实现一个功能，查找文档查了半天。其实其中很大一部分操作，你可以在上面的第3点、第4点里通过快捷键的提供的<code>Command id</code>去实现：</p><p><img src="https://i.loli.net/2018/06/13/5b20c6a5bcccd.png"></p><p>比如你要实现一个剪切的功能，有了<code>Command id</code>，你就可以通过<code>vscode.commands.executeCommand(&#39;editor.action.clipboardCutAction&#39;)</code>来实现。因此我推荐，如果你要实现的功能有些可以用已有快捷键实现的，那么就能在这个列表里找到对应的<code>Command id</code>来手动实现了。</p><p>至于其他的一些非快捷键提供的功能，就还需要阅读第2点的api文档做出更深层次的修改了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在改进完这个插件之后，我向作者提交了PR。在和作者交流后做出了一些修改，并最终被作者接受并合并。为开源项目贡献代码的感觉是真的很不错。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;前一段时间在Mac上用VSCode的时候，发现&lt;code&gt;VSCodeVim&lt;/code&gt;这个插件严重拖慢了我的开发效率。本来用&lt;code&gt;Vim&lt;/code&gt;模式难道不应该是提高效率么？问题是在&lt;code&gt;Normal&lt;/code&gt;模式下，光标的移动会有肉眼可见的长延时。比如我按着&lt;code&gt;j&lt;/code&gt;，等我松开&lt;code&gt;j&lt;/code&gt;后，光标还在移动，而且还移动了一会儿。预期的效果应该是按下移动，松开停止。为此我查了一下相关&lt;a href=&quot;https://github.com/VSCodeVim/Vim/issues/2021&quot;&gt;issue&lt;/a&gt;，发现跟我一样的情况的人还不少。（不过也有不少人没有这个问题，貌似跟显卡有关系？我的mac是集显的）。&lt;/p&gt;
&lt;p&gt;卸载了&lt;code&gt;VSCodeVim&lt;/code&gt;之后，光标移动的速度又恢复了正常，不过没有&lt;code&gt;Vim&lt;/code&gt;模式的话非常别扭。所以我就开始看看VSCode还有没有其他&lt;code&gt;Vim&lt;/code&gt;模式的插件。于是我又试了另外两个插件：&lt;a href=&quot;https://github.com/74th/vscode-vim&quot;&gt;vimStyle&lt;/a&gt;和&lt;a href=&quot;https://github.com/aioutecism/amVim-for-VSCode&quot;&gt;amVim&lt;/a&gt;。最终我选择了后者。不仅是支持的Vim命令更多，还有就是开发者的维护一直在继续。而且很关键的一点，&lt;code&gt;amVim&lt;/code&gt;的光标移动体验就是 &lt;strong&gt;如丝般顺滑&lt;/strong&gt; ！&lt;/p&gt;
&lt;p&gt;不过它有个让我很不习惯的地方：不支持&lt;code&gt;:&lt;/code&gt;号调起VSCode的&lt;code&gt;Command Line&lt;/code&gt;窗口，实现诸如&lt;code&gt;:w&lt;/code&gt;保存，&lt;code&gt;:wq&lt;/code&gt;退出等常见功能。这些功能在&lt;code&gt;VSCodeVim&lt;/code&gt;里是支持的。于是我就在想有没有办法「移植」一下&lt;code&gt;VSCodeVim&lt;/code&gt;的功能到&lt;code&gt;amVim&lt;/code&gt;来，既能保持光标移动体验顺滑，又能用上&lt;code&gt;Command Line&lt;/code&gt;的一些常用命令。所以开启了魔改模式，并在跟开发者的一系列交流后最终我提交的PR被&lt;a href=&quot;https://github.com/aioutecism/amVim-for-VSCode/pull/199&quot;&gt;merge&lt;/a&gt;了。&lt;br&gt;&lt;img src=&quot;https://i.loli.net/2018/06/06/5b179b533f190.png&quot;&gt;&lt;br&gt;本文记录一下我第一次对VSCode插件（修改）开发的过程。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    <category term="TypeScript" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/TypeScript/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Nodejs" scheme="https://molunerfinn.com/tags/Nodejs/"/>
    
    <category term="TypeScript" scheme="https://molunerfinn.com/tags/TypeScript/"/>
    
    <category term="VSCode" scheme="https://molunerfinn.com/tags/VSCode/"/>
    
  </entry>
  
  <entry>
    <title>基于Koa2开发微信二维码扫码支付相关流程</title>
    <link href="https://molunerfinn.com/koa2-wechatpay/"/>
    <id>https://molunerfinn.com/koa2-wechatpay/</id>
    <published>2018-05-15T13:50:00.000Z</published>
    <updated>2026-03-08T01:14:37.984Z</updated>
    
    <content type="html"><![CDATA[<p>前段时间在开发一个功能，要求是通过微信二维码进行扫码支付。这个情景我们屡见不鲜了，各种电子商城、线下的自动贩卖机等等都会有这个功能。平时只是使用者，如今变为开发者，也是有不小的坑。所以特此写一篇博客记录一下。</p><blockquote><p><strong>注</strong>： 要开发微信二维码支付，你必须要有相应的商户号的权限，否则你是无法开发的。若无相应权限，本文不推荐阅读。</p></blockquote><span id="more"></span><h2 id="两种模式"><a href="#两种模式" class="headerlink" title="两种模式"></a>两种模式</h2><p>打开微信支付的文档，我们可以看到两种支付模式：<a href="https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4">模式一</a>和<a href="https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5">模式二</a>。这二者的流程图微信的文档里都给出了（不过说实话画得真的有点丑）。</p><p>文档里指出了二者的区别：</p><blockquote><p>模式一开发前，商户必须在公众平台后台设置支付回调URL。URL实现的功能：接收用户扫码后微信支付系统回调的productid和openid。</p></blockquote><blockquote><p>模式二与模式一相比，流程更为简单，不依赖设置的回调支付URL。商户后台系统先调用微信支付的统一下单接口，微信后台系统返回链接参数code_url，商户后台系统将code_url值生成二维码图片，用户使用微信客户端扫码后发起支付。注意：code_url有效期为2小时，过期后扫码不能再发起支付。</p></blockquote><p>模式一是我们平时在网购的时候比较常见的，会弹出一个专门的页面用于扫码支付，然后支付成功后这个页面会再次跳转回回调页面，通知你支付成功。第二种的话想对少一些，不过第二种开发起来相对简单点。<strong>本文主要介绍模式二的开发</strong>。</p><h2 id="搭建Koa2的简单开发环境"><a href="#搭建Koa2的简单开发环境" class="headerlink" title="搭建Koa2的简单开发环境"></a>搭建Koa2的简单开发环境</h2><p>快速搭建Koa2的开发环境我推荐可以使用<a href="https://github.com/17koa/koa-generator">koa-generator</a>。脚手架能帮我们省去Koa项目一开始的一些基本中间件的书写步骤。（如果你想学习Koa最好自己搭建一个。如果你已经会Koa了就可以使用一些快速脚手架了。）</p><p>首先全局安装<code>koa-generator</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">npm install -g koa-generator</span><br><span class="line"></span><br><span class="line"><span class="comment">#or</span></span><br><span class="line"></span><br><span class="line">yarn global add koa-generator</span><br></pre></td></tr></table></figure><p>然后找一个目录用来存放Koa项目，我们打算给这个项目取个名字叫做<code>koa-wechatpay</code>，然后就可以输入<code>koa2 koa-wechatpay</code>。然后脚手架会自动创建相应文件夹<code>koa-wechatpay</code>，并生成基本骨架。进入这个文件夹，安装相应的插件。输入：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">npm install</span><br><span class="line"></span><br><span class="line"><span class="comment">#or</span></span><br><span class="line"></span><br><span class="line">yarn</span><br></pre></td></tr></table></figure><p>接着你可以输入<code>npm start</code> 或者 <code>yarn start</code>来运行项目（默认监听在3000端口）。</p><p>如果不出意外，你的项目跑起来了，然后我们用postman测试一下：</p><blockquote><p>这条路由是在<code>routes/index.js</code>里。</p></blockquote><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/8700af19ly1frc14ddfn9j21iq0r2n0p.jpg"></p><p>如果你看到了</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;koa2 json&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>就说明没问题。（如果有问题，检查一下是不是端口被占用了等等。）</p><p>接下来在<code>routes</code>文件夹里我们新建一个<code>wechatpay.js</code>的文件用来书写我们的流程。</p><h2 id="签名"><a href="#签名" class="headerlink" title="签名"></a>签名</h2><p>跟微信的服务器交流很关键的一环是签名必须正确，如果签名不正确，那么一切都白搭。</p><p>首先我们需要去公众号的后台获取我们所需要的如下相应的id或者key的信息。其中<code>notify_url</code>和<code>server_ip</code>是用于当我们支付成功后，微信会主动往这个url<code>post</code>支付成功的信息。</p><p>签名算法如下：<a href="https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3">https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=4_3</a></p><p>为了签名正确，我们需要安装一下<code>md5</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">npm install md5 --save</span><br><span class="line"></span><br><span class="line"><span class="comment">#or</span></span><br><span class="line"></span><br><span class="line">yarn add md5</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> md5 = <span class="built_in">require</span>(<span class="string">&#x27;md5&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> appid = <span class="string">&#x27;xxx&#x27;</span></span><br><span class="line"><span class="keyword">const</span> mch_id = <span class="string">&#x27;yyy&#x27;</span></span><br><span class="line"><span class="keyword">const</span> mch_api_key = <span class="string">&#x27;zzz&#x27;</span></span><br><span class="line"><span class="keyword">const</span> notify_url = <span class="string">&#x27;http://xxx/api/notify&#x27;</span> <span class="comment">// 服务端可访问的域名和接口</span></span><br><span class="line"><span class="keyword">const</span> server_ip = <span class="string">&#x27;xx.xx.xx.xx&#x27;</span> <span class="comment">// 服务端的ip地址</span></span><br><span class="line"><span class="keyword">const</span> trade_type = <span class="string">&#x27;NATIVE&#x27;</span> <span class="comment">// NATIVE对应的是二维码扫码支付</span></span><br><span class="line"><span class="keyword">let</span> body = <span class="string">&#x27;XXX的充值支付&#x27;</span> <span class="comment">// 用于显示在支付界面的提示词</span></span><br></pre></td></tr></table></figure><p>然后开始写签名函数：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">signString</span> = (<span class="params">fee, ip, nonce</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> tempString = <span class="string">`appid=<span class="subst">$&#123;appid&#125;</span>&amp;body=<span class="subst">$&#123;body&#125;</span>&amp;mch_id=<span class="subst">$&#123;mch_id&#125;</span>&amp;nonce_str=<span class="subst">$&#123;nonce&#125;</span>&amp;notify_url=<span class="subst">$&#123;notify_url&#125;</span>&amp;out_trade_no=<span class="subst">$&#123;nonce&#125;</span>&amp;spbill_create_ip=<span class="subst">$&#123;ip&#125;</span>&amp;total_fee=<span class="subst">$&#123;fee&#125;</span>&amp;trade_type=<span class="subst">$&#123;trade_type&#125;</span>&amp;key=<span class="subst">$&#123;mch_api_key&#125;</span>`</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">md5</span>(tempString).<span class="title function_">toUpperCase</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中<code>fee</code>是要充值的费用，以分为单位。比如要充值1块钱，<code>fee</code>就是100。ip是个比较随意的选项，只要符合规则的ip经过测试都是可以的，下文里我用的是<code>server_ip</code>。<code>nonce</code>就是微信要求的不重复的32位以内的字符串，通常可以使用订单号等唯一标识的字符串。</p><p>由于跟微信的服务器交流都是用xml来交流，所以现在我们要手动组装一下post请求的<code>xml</code>:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">xmlBody</span> = (<span class="params">fee, nonce_str</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> xml = <span class="string">`</span></span><br><span class="line"><span class="string">    &lt;xml&gt;</span></span><br><span class="line"><span class="string">    &lt;appid&gt;<span class="subst">$&#123;appid&#125;</span>&lt;/appid&gt;</span></span><br><span class="line"><span class="string">    &lt;body&gt;<span class="subst">$&#123;body&#125;</span>&lt;/body&gt;</span></span><br><span class="line"><span class="string">    &lt;mch_id&gt;<span class="subst">$&#123;mch_id&#125;</span>&lt;/mch_id&gt;</span></span><br><span class="line"><span class="string">    &lt;nonce_str&gt;<span class="subst">$&#123;nonce_str&#125;</span>&lt;/nonce_str&gt;</span></span><br><span class="line"><span class="string">    &lt;notify_url&gt;<span class="subst">$&#123;notify_url&#125;</span>&lt;/notify_url&gt;</span></span><br><span class="line"><span class="string">    &lt;out_trade_no&gt;<span class="subst">$&#123;nonce_str&#125;</span>&lt;/out_trade_no&gt;</span></span><br><span class="line"><span class="string">    &lt;total_fee&gt;<span class="subst">$&#123;fee&#125;</span>&lt;/total_fee&gt;</span></span><br><span class="line"><span class="string">    &lt;spbill_create_ip&gt;<span class="subst">$&#123;server_ip&#125;</span>&lt;/spbill_create_ip&gt;</span></span><br><span class="line"><span class="string">    &lt;trade_type&gt;NATIVE&lt;/trade_type&gt;</span></span><br><span class="line"><span class="string">    &lt;sign&gt;<span class="subst">$&#123;signString(fee, server_ip, nonce_str)&#125;</span>&lt;/sign&gt;</span></span><br><span class="line"><span class="string">    &lt;/xml&gt;</span></span><br><span class="line"><span class="string">  `</span></span><br><span class="line">  <span class="keyword">return</span> &#123;</span><br><span class="line">    xml,</span><br><span class="line">    <span class="attr">out_trade_no</span>: nonce_str</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><blockquote><p>如果你怕自己的签名的<code>xml</code>串有问题，可以提前在微信提供的<a href="https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=20_1">签名校验工具</a>里先校验一遍，看看是否能通过。</p></blockquote><h2 id="发送请求"><a href="#发送请求" class="headerlink" title="发送请求"></a>发送请求</h2><p>因为需要跟微信服务端发请求，所以我选择了<code>axios</code>这个在浏览器端和node端都能发起ajax请求的库。</p><p>安装过程不再赘述。继续在<code>wechatpay.js</code>写发请求的逻辑。</p><p>由于微信给我们返回的也将是一个xml格式的字符串。所以我们需要预先写好解析函数，将xml解析成js对象。为此你可以安装一个<a href="https://github.com/Leonidas-from-XIV/node-xml2js">xml2js</a>。安装过程跟上面的类似，不再赘述。</p><p>微信会给我们返回一个诸如下面格式的<code>xml</code>字符串：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">xml</span>&gt;</span><span class="tag">&lt;<span class="name">return_code</span>&gt;</span>&lt;![CDATA[SUCCESS]]&gt;<span class="tag">&lt;/<span class="name">return_code</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">return_msg</span>&gt;</span>&lt;![CDATA[OK]]&gt;<span class="tag">&lt;/<span class="name">return_msg</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">appid</span>&gt;</span>&lt;![CDATA[wx742xxxxxxxxxxxxx]]&gt;<span class="tag">&lt;/<span class="name">appid</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">mch_id</span>&gt;</span>&lt;![CDATA[14899xxxxx]]&gt;<span class="tag">&lt;/<span class="name">mch_id</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">nonce_str</span>&gt;</span>&lt;![CDATA[R69QXXXXXXXX6O]]&gt;<span class="tag">&lt;/<span class="name">nonce_str</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">sign</span>&gt;</span>&lt;![CDATA[79F0891XXXXXX189507A184XXXXXXXXX]]&gt;<span class="tag">&lt;/<span class="name">sign</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">result_code</span>&gt;</span>&lt;![CDATA[SUCCESS]]&gt;<span class="tag">&lt;/<span class="name">result_code</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">prepay_id</span>&gt;</span>&lt;![CDATA[wx152316xxxxxxxxxxxxxxxxxxxxxxxxxxx]]&gt;<span class="tag">&lt;/<span class="name">prepay_id</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">trade_type</span>&gt;</span>&lt;![CDATA[NATIVE]]&gt;<span class="tag">&lt;/<span class="name">trade_type</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">code_url</span>&gt;</span>&lt;![CDATA[weixin://wxpay/xxxurl?pr=dQNakHH]]&gt;<span class="tag">&lt;/<span class="name">code_url</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">xml</span>&gt;</span></span><br></pre></td></tr></table></figure><p>我们的目标是转为如下的js对象，好让我们用js来操作数据：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">return_code</span>: <span class="string">&#x27;SUCCESS&#x27;</span>, <span class="comment">// SUCCESS 或者 FAIL</span></span><br><span class="line">  <span class="attr">return_msg</span>: <span class="string">&#x27;OK&#x27;</span>,</span><br><span class="line">  <span class="attr">appid</span>: <span class="string">&#x27;wx742xxxxxxxxxxxxx&#x27;</span>,</span><br><span class="line">  <span class="attr">mch_id</span>: <span class="string">&#x27;14899xxxxx&#x27;</span>,</span><br><span class="line">  <span class="attr">nonce_str</span>: <span class="string">&#x27;R69QXXXXXXXX6O&#x27;</span>,</span><br><span class="line">  <span class="attr">sign</span>: <span class="string">&#x27;79F0891XXXXXX189507A184XXXXXXXXX&#x27;</span>,</span><br><span class="line">  <span class="attr">result_code</span>: <span class="string">&#x27;SUCCESS&#x27;</span>,</span><br><span class="line">  <span class="attr">prepay_id</span>: <span class="string">&#x27;wx152316xxxxxxxxxxxxxxxxxxxxxxxxxxx&#x27;</span>,</span><br><span class="line">  <span class="attr">trade_type</span>: <span class="string">&#x27;NATIVE&#x27;</span>,</span><br><span class="line">  <span class="attr">code_url</span>: <span class="string">&#x27;weixin://wxpay/xxxurl?pr=dQNakHH&#x27;</span> <span class="comment">// 用于生成支付二维码的链接</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>于是我们写一个函数，调用<code>xml2js</code>来解析xml：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 将XML转为JS对象</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">parseXML</span> = (<span class="params">xml</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function">(<span class="params">res, rej</span>) =&gt;</span> &#123;</span><br><span class="line">    xml2js.<span class="title function_">parseString</span>(xml, &#123;<span class="attr">trim</span>: <span class="literal">true</span>, <span class="attr">explicitArray</span>: <span class="literal">false</span>&#125;, <span class="function">(<span class="params">err, json</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (err) &#123;</span><br><span class="line">        <span class="title function_">rej</span>(err)</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="title function_">res</span>(json.<span class="property">xml</span>)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面的代码返回了一个<code>Promise</code>对象，因为<code>xml2js</code>的操作是在回调函数里返回的结果，所以为了配合Koa2的<code>async</code>、<code>await</code>，我们可以将其封装成一个<code>Promise</code>对象，将解析完的结果通过<code>resolve</code>返回回去。这样就能用<code>await</code>来取数据了：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> axios = <span class="built_in">require</span>(<span class="string">&#x27;axios&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> url = <span class="string">&#x27;https://api.mch.weixin.qq.com/pay/unifiedorder&#x27;</span> <span class="comment">// 微信服务端地址</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">pay</span> = <span class="keyword">async</span> (<span class="params">ctx</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> form = ctx.<span class="property">request</span>.<span class="property">body</span> <span class="comment">// 通过前端传来的数据</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> orderNo = <span class="string">&#x27;XXXXXXXXXXXXXXXX&#x27;</span> <span class="comment">// 不重复的订单号</span></span><br><span class="line">  <span class="keyword">const</span> fee = form.<span class="property">fee</span> <span class="comment">// 通过前端传来的费用值</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> data = <span class="title function_">xmlBody</span>(fee, orderNo) <span class="comment">// fee是费用，orderNo是订单号（唯一）</span></span><br><span class="line">  <span class="keyword">const</span> res = <span class="keyword">await</span> axios.<span class="title function_">post</span>(url, &#123;</span><br><span class="line">    <span class="attr">data</span>: data.<span class="property">xml</span></span><br><span class="line">  &#125;).<span class="title function_">then</span>(<span class="keyword">async</span> res =&gt; &#123;</span><br><span class="line">    <span class="keyword">const</span> resJson = <span class="keyword">await</span> <span class="title function_">parseXML</span>(res.<span class="property">data</span>)</span><br><span class="line">    <span class="keyword">return</span> resJson <span class="comment">// 拿到返回的数据</span></span><br><span class="line">  &#125;).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(err)</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">if</span> (res.<span class="property">return_code</span> === <span class="string">&#x27;SUCCESS&#x27;</span>) &#123; <span class="comment">// 如果返回的</span></span><br><span class="line">    <span class="keyword">return</span> ctx.<span class="property">body</span> = &#123;</span><br><span class="line">      <span class="attr">success</span>: <span class="literal">true</span>,</span><br><span class="line">      <span class="attr">message</span>: <span class="string">&#x27;请求成功&#x27;</span>,</span><br><span class="line">      <span class="attr">code_url</span>: res.<span class="property">code_url</span>, <span class="comment">// code_url就是用于生成支付二维码的链接</span></span><br><span class="line">      <span class="attr">order_no</span>: orderNo <span class="comment">// 订单号</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  ctx.<span class="property">body</span> = &#123;</span><br><span class="line">    <span class="attr">success</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">message</span>: <span class="string">&#x27;请求失败&#x27;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">router.<span class="title function_">post</span>(<span class="string">&#x27;/api/pay&#x27;</span>, pay)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = router</span><br></pre></td></tr></table></figure><p>然后我们要将这个router挂载到根目录的<code>app.js</code>里去。</p><p>找到之前默认的两个路由，一个<code>index</code>，一个<code>user</code>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> index = <span class="built_in">require</span>(<span class="string">&#x27;./routes/index&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> users = <span class="built_in">require</span>(<span class="string">&#x27;./routes/users&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> wechatpay = <span class="built_in">require</span>(<span class="string">&#x27;./routes/wechatpay&#x27;</span>) <span class="comment">// 加在这里</span></span><br></pre></td></tr></table></figure><p>然后到页面底下挂载这个路由：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// routes</span></span><br><span class="line">app.<span class="title function_">use</span>(index.<span class="title function_">routes</span>(), index.<span class="title function_">allowedMethods</span>())</span><br><span class="line">app.<span class="title function_">use</span>(users.<span class="title function_">routes</span>(), users.<span class="title function_">allowedMethods</span>())</span><br><span class="line">app.<span class="title function_">use</span>(wechatpay.<span class="title function_">routes</span>(), users.<span class="title function_">allowedMethods</span>()) <span class="comment">// 加在这里</span></span><br></pre></td></tr></table></figure><p>于是你就可以通过发送<code>/api/pay</code>来请求二维码数据啦。（如果有跨域需要自己考虑解决跨域方案，可以跟Koa放在同域里，也可以开一层proxy来转发，也可以开CORS头等等）</p><p><strong>注意</strong>， 本例里是用前端来生成二维码，其实也可以通过后端生成二维码，然后再返回给前端。不过为了简易演示，本例采用前端通过获取<code>code_url</code>后，在前端生成二维码。</p><h2 id="展示支付二维码"><a href="#展示支付二维码" class="headerlink" title="展示支付二维码"></a>展示支付二维码</h2><p>前端我用的是<code>Vue</code>，当然你可以选择你喜欢的前端框架。这里关注点在于通过拿到刚才后端传过来的<code>code_url</code>来生成二维码。</p><p>在前端，我使用的是<a href="https://github.com/xkeshi/vue-qrcode">@xkeshi&#x2F;vue-qrcode</a>这个库来生成二维码。它调用特别简单：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="title class_">VueQrcode</span> <span class="keyword">from</span> <span class="string">&#x27;@xkeshi/vue-qrcode&#x27;</span></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span><br><span class="line">  <span class="attr">components</span>: &#123;</span><br><span class="line">    <span class="title class_">VueQrcode</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="comment">// ...其他代码</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后就可以在前端里用<code>&lt;vue-qrcode&gt;</code>的组件来生成二维码了：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">vue-qrcode</span> <span class="attr">:value</span>=<span class="string">&quot;codeUrl&quot;</span> <span class="attr">:options</span>=<span class="string">&quot;&#123; size: 200 &#125;&quot;</span>&gt;</span></span><br></pre></td></tr></table></figure><p>放到Dialog里就是这样的效果：</p><blockquote><p>文本是我自己添加的</p></blockquote><p><img src="https://blog-1251750343.cos.ap-beijing.myqcloud.com/wechat-pay.png"></p><h2 id="付款成功自动刷新页面"><a href="#付款成功自动刷新页面" class="headerlink" title="付款成功自动刷新页面"></a>付款成功自动刷新页面</h2><p>有两种将支付成功写入数据库的办法。</p><p>一种是在打开了扫码对话框后，不停向微信服务端轮询支付结果，如果支付成功，那么就向后端发起请求，告诉后端支付成功，让后端写入数据库。</p><p>一种是后端一直开着接口，等微信主动给后端的<code>notify_url</code>发起post请求，告诉后端支付结果，让后端写入数据库。然后此时前端向后端轮询的时候应该是去数据库取轮询该订单的支付结果，如果支付成功就关闭Dialog。</p><p>第一种比较简单但是不安全：试想万一用户支付成功的同时关闭了页面，或者用户支付成功了，但是网络有问题导致前端没法往后端发支付成功的结果，那么后端就一直没办法写入支付成功的数据。</p><p>第二种虽然麻烦，但是保证了安全。所有的支付结果都必须等微信主动向后端通知，后端存完数据库后再返回给前端消息。这样哪怕用户支付成功的同时关闭了页面，下次再打开的时候，由于数据库已经写入了，所以拿到的也是支付成功的结果。</p><p>所以<code>付款成功自动刷新页面</code>这个部分我们分为两个部分来说：</p><h3 id="前端部分"><a href="#前端部分" class="headerlink" title="前端部分"></a>前端部分</h3><p>Vue的data部分</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">data</span>: &#123;</span><br><span class="line">  <span class="attr">payStatus</span>: <span class="literal">false</span>, <span class="comment">// 未支付成功</span></span><br><span class="line">  <span class="attr">retryCount</span>: <span class="number">0</span>, <span class="comment">// 轮询次数，从0-200</span></span><br><span class="line">  <span class="attr">orderNo</span>: <span class="string">&#x27;xxx&#x27;</span>, <span class="comment">// 从后端传来的order_no</span></span><br><span class="line">  <span class="attr">codeUrl</span>: <span class="string">&#x27;xxx&#x27;</span> <span class="comment">// 从后端传来的code_url</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在methods里写一个查询订单信息的方法：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"><span class="title function_">handleCheckBill</span> () &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">setTimeout</span>(<span class="function">() =&gt;</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">payStatus</span> &amp;&amp; <span class="variable language_">this</span>.<span class="property">retryCount</span> &lt; <span class="number">120</span>) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">retryCount</span> += <span class="number">1</span></span><br><span class="line">      axios.<span class="title function_">post</span>(<span class="string">&#x27;/api/check-bill&#x27;</span>, &#123; <span class="comment">// 向后端请求订单支付信息</span></span><br><span class="line">        <span class="attr">orderNo</span>: <span class="variable language_">this</span>.<span class="property">orderNo</span></span><br><span class="line">      &#125;)</span><br><span class="line">        .<span class="title function_">then</span>(<span class="function"><span class="params">res</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="keyword">if</span> (res.<span class="property">data</span>.<span class="property">success</span>) &#123;</span><br><span class="line">            <span class="variable language_">this</span>.<span class="property">payStatus</span> = <span class="literal">true</span></span><br><span class="line">            location.<span class="title function_">reload</span>() <span class="comment">// 偷懒就用reload重新刷新页面</span></span><br><span class="line">          &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="variable language_">this</span>.<span class="title function_">handleCheckBill</span>()</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =&gt;</span> &#123;</span><br><span class="line">          <span class="variable language_">console</span>.<span class="title function_">log</span>(err)</span><br><span class="line">        &#125;)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      location.<span class="title function_">reload</span>()</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;, <span class="number">1000</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在打开二维码Dialog的时候，这个方法就启用了。然后就开始轮询。我订了一个时间，200s后如果还是没有付款信息也自动刷新页面。实际上你可以自己根据项目的需要来定义这个时间。</p><h3 id="后端部分"><a href="#后端部分" class="headerlink" title="后端部分"></a>后端部分</h3><p>前端到后端只有一个接口，但是后端有两个接口。一个是用来接收微信的推送，一个是用来接收前端的查询请求。</p><p>先来写最关键的微信的推送请求处理。由于我们接收微信的请求是在Koa的路由里，并且是以流的形式传输的。需要让Koa支持解析xml格式的body，所以需要安装一个<a href="https://github.com/stream-utils/raw-body">rawbody</a>来获取xml格式的body。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 处理微信支付回传notify</span></span><br><span class="line"><span class="comment">// 如果收到消息要跟微信回传是否接收到</span></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">handleNotify</span> = <span class="keyword">async</span> (<span class="params">ctx</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> xml = <span class="keyword">await</span> <span class="title function_">rawbody</span>(ctx.<span class="property">req</span>, &#123;</span><br><span class="line">    <span class="attr">length</span>: ctx.<span class="property">request</span>.<span class="property">length</span>,</span><br><span class="line">    <span class="attr">limit</span>: <span class="string">&#x27;1mb&#x27;</span>,</span><br><span class="line">    <span class="attr">encoding</span>: ctx.<span class="property">request</span>.<span class="property">charset</span> || <span class="string">&#x27;utf-8&#x27;</span></span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">const</span> res = <span class="keyword">await</span> <span class="title function_">parseXML</span>(xml) <span class="comment">// 解析xml</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (res.<span class="property">return_code</span> === <span class="string">&#x27;SUCCESS&#x27;</span>) &#123;</span><br><span class="line">    <span class="keyword">if</span> (res.<span class="property">result_code</span> === <span class="string">&#x27;SUCCESS&#x27;</span>) &#123; <span class="comment">// 如果都为SUCCESS代表支付成功</span></span><br><span class="line">      <span class="comment">// ... 这里是写入数据库的相关操作</span></span><br><span class="line"></span><br><span class="line">      <span class="comment">// 开始回传微信</span></span><br><span class="line">      ctx.<span class="property">type</span> = <span class="string">&#x27;application/xml&#x27;</span> <span class="comment">// 指定发送的请求类型是xml</span></span><br><span class="line">      <span class="comment">// 回传微信，告诉已经收到</span></span><br><span class="line">      <span class="keyword">return</span> ctx.<span class="property">body</span> = <span class="string">`&lt;xml&gt;</span></span><br><span class="line"><span class="string">        &lt;return_code&gt;&lt;![CDATA[SUCCESS]]&gt;&lt;/return_code&gt;</span></span><br><span class="line"><span class="string">        &lt;return_msg&gt;&lt;![CDATA[OK]]&gt;&lt;/return_msg&gt;</span></span><br><span class="line"><span class="string">      &lt;/xml&gt;</span></span><br><span class="line"><span class="string">      `</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果支付失败，也回传微信</span></span><br><span class="line">  ctx.<span class="property">status</span> = <span class="number">400</span></span><br><span class="line">  ctx.<span class="property">type</span> = <span class="string">&#x27;application/xml&#x27;</span></span><br><span class="line">  ctx.<span class="property">body</span> = <span class="string">`&lt;xml&gt;</span></span><br><span class="line"><span class="string">    &lt;return_code&gt;&lt;![CDATA[FAIL]]&gt;&lt;/return_code&gt;</span></span><br><span class="line"><span class="string">    &lt;return_msg&gt;&lt;![CDATA[OK]]&gt;&lt;/return_msg&gt;</span></span><br><span class="line"><span class="string">  &lt;/xml&gt;</span></span><br><span class="line"><span class="string">  `</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">router.<span class="title function_">post</span>(<span class="string">&#x27;/api/notify&#x27;</span>, handleNotify)</span><br></pre></td></tr></table></figure><p>这里的坑就是Koa处理微信回传的xml。如果不知道是以<code>raw-body</code>的形式回传的，会调试半天。。</p><p>接下来这个就是比较简单的给前端回传的了。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">checkBill</span> = <span class="keyword">async</span> (<span class="params">ctx</span>) =&gt; &#123;</span><br><span class="line">  <span class="keyword">const</span> form = ctx.<span class="property">request</span>.<span class="property">body</span></span><br><span class="line">  <span class="keyword">const</span> orderNo = form.<span class="property">orderNo</span></span><br><span class="line">  <span class="keyword">const</span> result = <span class="keyword">await</span> 数据库操作</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (result) &#123; <span class="comment">// 如果订单支付成功</span></span><br><span class="line">    <span class="keyword">return</span> ctx.<span class="property">body</span> = &#123;</span><br><span class="line">      <span class="attr">success</span>: <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  ctx.<span class="property">status</span> = <span class="number">400</span></span><br><span class="line">  ctx.<span class="property">body</span> = &#123;</span><br><span class="line">    <span class="attr">success</span>: <span class="literal">false</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">router.<span class="title function_">post</span>(<span class="string">&#x27;/api/check-bill&#x27;</span>, checkBill)</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>至此，一整个基于Koa2的微信二维码支付流程就简单演示完了，由于不是公开的项目，所以没有实际的GitHub仓库。不过基本上关键的代码我都已经注释出来啦。我参考了不少人的实现，曾考虑过用一些比如<code>wechatpay</code>的npm库，不过最终还是自己解决了。这里面感谢很多前人的分享，也希望我这篇文章能给你一些帮助。</p><h2 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h2><p>微信支付文章</p><p><a href="https://www.itbaby.me/blog/59e21af45d21b31fcd4e02c6">https://www.itbaby.me/blog/59e21af45d21b31fcd4e02c6</a></p><p><a href="https://juejin.im/post/5a8e84faf265da4e7e10c92f">https://juejin.im/post/5a8e84faf265da4e7e10c92f</a></p><p>返回接口</p><p><a href="http://webcache.googleusercontent.com/search?q=cache:iFC0HZuFB1gJ:jeffdeng.me/wx/2017/03/13/wx-platform-conect.html+&cd=4&hl=zh-CN&ct=clnk&gl=us">http://webcache.googleusercontent.com/search?q=cache:iFC0HZuFB1gJ:jeffdeng.me/wx/2017/03/13/wx-platform-conect.html+&amp;cd=4&amp;hl=zh-CN&amp;ct=clnk&amp;gl=us</a></p><p>XML流处理</p><p><a href="https://blog.csdn.net/yxz1025/article/details/52313221">https://blog.csdn.net/yxz1025/article/details/52313221</a></p><p><a href="https://juejin.im/post/5a6c558ef265da3e4b77030f">https://juejin.im/post/5a6c558ef265da3e4b77030f</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;前段时间在开发一个功能，要求是通过微信二维码进行扫码支付。这个情景我们屡见不鲜了，各种电子商城、线下的自动贩卖机等等都会有这个功能。平时只是使用者，如今变为开发者，也是有不小的坑。所以特此写一篇博客记录一下。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注&lt;/strong&gt;： 要开发微信二维码支付，你必须要有相应的商户号的权限，否则你是无法开发的。若无相应权限，本文不推荐阅读。&lt;/p&gt;
&lt;/blockquote&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    <category term="Nodejs" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/Nodejs/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Nodejs" scheme="https://molunerfinn.com/tags/Nodejs/"/>
    
    <category term="Koa" scheme="https://molunerfinn.com/tags/Koa/"/>
    
  </entry>
  
  <entry>
    <title>【NOTE】观察者模式VS订阅发布模式</title>
    <link href="https://molunerfinn.com/observer-vs-pubsub-pattern/"/>
    <id>https://molunerfinn.com/observer-vs-pubsub-pattern/</id>
    <published>2018-05-12T23:34:00.000Z</published>
    <updated>2026-03-08T01:14:37.986Z</updated>
    
    <content type="html"><![CDATA[<p>最近在看了一篇<a href="https://juejin.im/post/5af05d406fb9a07a9e4d2799">《不好意思，观察者模式跟发布订阅模式就是不一样》</a>的文章之后对于这两个模式产生了比较浓厚的兴趣。不过奈何我的水平有限，看完那篇文章还是不能理解。不过在和朋友讨论之后，我想我应该是弄懂了。所以特地记下一篇笔记，以便回头翻阅的时候能够想起来。如果理解有误，欢迎在下方评论指出，一起讨论！</p><span id="more"></span><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>有人说这两个模式其实是一个模式。我想这句话的对错对半分吧。它们有类似的地方，不过也不能说完全一致。先来一张图，这张图解释了<code>观察者模式</code>和<code>发布订阅模式</code>在流程上的一些区别：</p><p><img src="https://img.piegg.cn/observer-pubsub.png?imageslim"></p><p>左边是观察者模式，右边是订阅发布模式。</p><p>简单阐述二者的模型：</p><p>观察者模式里，观察者（Observer）直接订阅（subscribe）主题（Subject），而当主题被激活的时候，会触发（fire）观察者里的事件。</p><p>订阅发布模式里，订阅者（Subscriber）通过监听（on）事件总线（Event Bus）里的事件，当事件总线里的事件被触发（emit）的时候，订阅者将会执行相应的操作。而这里需要注意的是，事件总线里的事件是通过发布者（Publisher）进行发布（publish）和 通知事件总线 <strong>触发</strong> 的。</p><blockquote><p>注：事件总线也有说法叫为调度中心。本质上是一样的。不过因为写Vue时候习惯用Event Bus来说了，所以本文的调度中心皆以事件总线称呼。</p></blockquote><p>所以事件总线本身不独自发布和触发事件，它会借由发布者来操作。这是跟观察者模式有着比较大的区别的地方。</p><p>当然只看这两张图和上面的解释，应该还是无法很好的理解。下面这张图能把流程讲得更清楚点。</p><p><img src="https://img.piegg.cn/observer-vs-pubsub-2.png?imageslim"></p><p>这个例子可以理解为这样：左边是微信里的<code>微商-顾客</code>之间的关系。右边是<code>商家-淘宝-顾客</code>之间的关系。</p><p>观察者模式：顾客关注了微商的商品，微商会记住顾客关注的商品，一旦上新就直接 <strong>私聊</strong> 通知所有关注这个商品的顾客。这里的顾客就相当于观察者，这里的微商就相当于主题。<br>订阅发布模式：顾客通过淘宝（APP或者网站）关注了商家的商品，商家一旦上新就通过淘宝（APP或者网站）向关注了它的顾客 <strong>群发</strong> 消息。这里的顾客就是订阅者，这里的淘宝就是事件总线，这里的商家就是发布者。</p><p>所以可以看出，观察者模式的模型跟发布订阅模型里，差距就差在有没有一个中央的事件总线。如果有这个事件总线，我们就可以认为是个发布订阅模型。如果没有，那么就可以认为是个观察者模型。因为其实它们都实现了一个关键的功能：发布事件-订阅事件并触发事件。</p><p>下面用代码简单解释一下。</p><h2 id="观察者模式"><a href="#观察者模式" class="headerlink" title="观察者模式"></a>观察者模式</h2><blockquote><p>由于最近在学习TypeScript，所以下面的代码也会用TypeScript来书写。</p></blockquote><p>我们先写一个定义观察者和主题的文件。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// observer-pattern.ts</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">Subjects</span> &#123;</span><br><span class="line">  [<span class="attr">key</span>: <span class="built_in">string</span>]: <span class="built_in">any</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 观察者</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Observer</span> &#123;</span><br><span class="line">  <span class="attr">subject</span>: <span class="built_in">string</span></span><br><span class="line">  <span class="title function_">constructor</span> (<span class="params"><span class="attr">subject</span>: <span class="built_in">string</span></span>) &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">subject</span> = subject</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">notify</span> () &#123;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`This <span class="subst">$&#123;<span class="variable language_">this</span>.subject&#125;</span> was fired!`</span>)</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">subject</span> = <span class="string">`Done`</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 主题</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Subject</span> &#123;</span><br><span class="line">  <span class="comment">// 根据主题的不同收集相应的订阅者</span></span><br><span class="line">  <span class="attr">subjects</span>: <span class="title class_">Subjects</span> = &#123;&#125;</span><br><span class="line">  <span class="comment">// 订阅</span></span><br><span class="line">  <span class="title function_">add</span> (subject, <span class="attr">observer</span>: <span class="title class_">Observer</span>): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">subjects</span>[subject]) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">subjects</span>[subject] = []</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">subjects</span>[subject].<span class="title function_">push</span>(observer)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 解除订阅</span></span><br><span class="line">  <span class="title function_">remove</span> (subject, <span class="attr">observer</span>: <span class="title class_">Observer</span>): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">subjects</span>[subject].<span class="title function_">forEach</span>(<span class="function">(<span class="params">item, index</span>) =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (item === observer) &#123;</span><br><span class="line">        <span class="variable language_">this</span>.<span class="property">subjects</span>[subject].<span class="title function_">splice</span>(index, <span class="number">1</span>)</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 触发事件</span></span><br><span class="line">  <span class="title function_">fire</span> (subject): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">subjects</span>[subject].<span class="title function_">forEach</span>(<span class="function"><span class="params">item</span> =&gt;</span> item.<span class="title function_">notify</span>())</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> &#123;</span><br><span class="line">  <span class="title class_">Observer</span>,</span><br><span class="line">  <span class="title class_">Subject</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>于是在调用的时候，是这样调用的：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> op <span class="keyword">from</span> <span class="string">&#x27;./observer-pattern&#x27;</span></span><br><span class="line"><span class="keyword">let</span> observer = <span class="keyword">new</span> op.<span class="title class_">Observer</span>(<span class="string">&#x27;click&#x27;</span>)</span><br><span class="line"><span class="keyword">let</span> subjects = <span class="keyword">new</span> op.<span class="title class_">Subject</span>()</span><br><span class="line">subjects.<span class="title function_">add</span>(<span class="string">&#x27;click&#x27;</span>, observer)</span><br><span class="line">subjects.<span class="title function_">fire</span>(<span class="string">&#x27;click&#x27;</span>) <span class="comment">// subjects 主动通知</span></span><br></pre></td></tr></table></figure><p>经过上述调用，subjects触发观察者订阅的click事件，<code>observer.subject</code>的值将会变为<code>Done</code>（原先为<code>click</code>）。</p><h2 id="订阅发布模式"><a href="#订阅发布模式" class="headerlink" title="订阅发布模式"></a>订阅发布模式</h2><p>接下来我们来实现一些订阅发布模式。订阅发布模式最关键的地方就在于中间的<code>Event Bus</code>部分。它接管着事件总线的订阅和发布。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// pubsub.ts</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">Subjects</span> &#123;</span><br><span class="line">  [<span class="attr">key</span>: <span class="built_in">string</span>]: <span class="built_in">any</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 定义Event Bus</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">EventBus</span> &#123;</span><br><span class="line">  <span class="attr">subjects</span>: <span class="title class_">Subjects</span> = &#123;&#125;</span><br><span class="line">  <span class="title function_">on</span> (subject, callback): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="comment">/* istanbul ignore next */</span></span><br><span class="line">    <span class="keyword">if</span> (!<span class="variable language_">this</span>.<span class="property">subjects</span>[subject]) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">subjects</span>[subject] = []</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">subjects</span>[subject].<span class="title function_">push</span>(callback)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">off</span> (subject, callback = <span class="literal">null</span>): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (callback === <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">subjects</span>[subject] = []</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="variable language_">this</span>.<span class="property">subjects</span>[subject].<span class="title function_">forEach</span>(<span class="function">(<span class="params">item, index</span>) =&gt;</span> &#123;</span><br><span class="line">        <span class="comment">/* istanbul ignore next */</span></span><br><span class="line">        <span class="keyword">if</span> (item === callback) &#123;</span><br><span class="line">          <span class="variable language_">this</span>.<span class="property">subjects</span>[subject].<span class="title function_">splice</span>(index, <span class="number">1</span>)</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title function_">emit</span> (subject, data = <span class="literal">null</span>): <span class="built_in">void</span> &#123;</span><br><span class="line">    <span class="variable language_">this</span>.<span class="property">subjects</span>[subject].<span class="title function_">forEach</span>(<span class="function"><span class="params">item</span> =&gt;</span> <span class="title function_">item</span>(data))</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">new</span> <span class="title class_">EventBus</span>()</span><br></pre></td></tr></table></figure><p>可以看出在这里的<code>EventBus</code>和观察者模式里的<code>Subject</code>几乎一致对吧。但是需要注意的是，最后一行里，我们<code>export default new EventBus()</code>，所以我们在项目里不同的地方<code>import</code>它，都会指向同一个<code>Event Bus</code>实例，这样的话就可以起到一个事件总线的作用了。它不在乎谁来监听，谁来发布。只要有人监听了，就把它放进监听队列中。只要有人发布了事件，就从相应的监听队列中触发回调。不过所有相关的事件都必须经过<code>Event Bus</code>这个实例，而不能越过它直接由发布者通知监听者。</p><blockquote><p>再次祭出这张图</p></blockquote><p><img src="https://img.piegg.cn/observer-vs-pubsub-2.png?imageslim"></p><p>所以在订阅发布模型里，发布者或者订阅者的身份已经被弱化。发布者可以在任何时候发布事件，而订阅者可能只是一个回调函数。而最关键的事件总线部分，则是发布订阅模型的核心。</p><p>如果你用过Vue的<code>Event Bus</code>，相信不会陌生。接下来我们来用用我们刚才写的简单的<code>Event Bus</code>。</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> bus <span class="keyword">from</span> <span class="string">&#x27;./pubsub.ts&#x27;</span></span><br><span class="line"><span class="keyword">const</span> people = <span class="keyword">function</span> (<span class="params">val</span>) &#123;</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&#x27;我收到了新的商品通知：&#x27;</span>, val) <span class="comment">// 收到消息</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">bus.<span class="title function_">on</span>(<span class="string">&#x27;newItem&#x27;</span>, people) <span class="comment">// 订阅newItem这个消息</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> merchant = <span class="keyword">function</span> (<span class="params">val</span>) &#123; <span class="comment">// 由商户向event bus发布新商品</span></span><br><span class="line">  <span class="keyword">const</span> item = &#123;</span><br><span class="line">    <span class="attr">item</span>: val</span><br><span class="line">  &#125;</span><br><span class="line">  bus.<span class="title function_">emit</span>(<span class="string">&#x27;newItem&#x27;</span>, item)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">merchant</span>(<span class="string">&#x27;Book&#x27;</span>) <span class="comment">// 发布</span></span><br></pre></td></tr></table></figure><p>所以你可以看到，这个事件总线是可以单独抽离出来的。如果要把我们这个文件丢到一个现有的项目里也是完全没问题的。</p><p>其实在写Vue组件通信的时候，你如果用到了<code>Event Bus</code>的话，也是一样的。在全局声明一个<code>new Vue()</code>做<code>Event Bus</code>总线，然后在不同的组件里只要引入了这个事件总线，就能订阅或者发布不同的消息。这个就是一个非常典型的订阅发布模型。</p><p>而如果只是Vue的父子组件通信，子组件用的是<code>this.$emit</code>来触发事件，父组件用的是<code>this.$on</code>这样的方式去订阅事件，那么你可以认为这个就是一个简单的观察者模型。因为它们之间的联系是紧密耦合的。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>不管是观察者模式也好，订阅发布模式也好，关键在于实现了在某个特定时间触发某个特定事件，从而触发监听这个特定事件的组件进行相应操作的功能。这个设计模式在很多时候非常有用。平时只是用到了它，但是没有深入去看看如何实现，这次借由这个机会把二者的关系和区别记录下来，也算是给自己加深了印象。</p><p>本文的代码你可以在我的学习仓库<a href="https://github.com/Molunerfinn/FE-Learning/tree/master/design-pattern">FE-Learning</a>找到。如有错误欢迎指出！</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><p><a href="https://www.zcfy.cc/article/observer-vs-pub-sub-pattern-hacker-noon">https://www.zcfy.cc/article/observer-vs-pub-sub-pattern-hacker-noon</a></p><p><a href="http://blog.zxbing0066.com/design-patterns/2016/09/12/observer-pattern.html">http://blog.zxbing0066.com/design-patterns/2016/09/12/observer-pattern.html</a></p><p><a href="https://juejin.im/post/5af05d406fb9a07a9e4d2799">https://juejin.im/post/5af05d406fb9a07a9e4d2799</a></p><p><a href="https://www.cnblogs.com/weebly/p/5279952.html">https://www.cnblogs.com/weebly/p/5279952.html</a></p><p><a href="https://www.jianshu.com/p/3098b1176357">https://www.jianshu.com/p/3098b1176357</a></p><p><a href="https://www.zhihu.com/question/23486749/answer/314072549">https://www.zhihu.com/question/23486749/answer/314072549</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近在看了一篇&lt;a href=&quot;https://juejin.im/post/5af05d406fb9a07a9e4d2799&quot;&gt;《不好意思，观察者模式跟发布订阅模式就是不一样》&lt;/a&gt;的文章之后对于这两个模式产生了比较浓厚的兴趣。不过奈何我的水平有限，看完那篇文章还是不能理解。不过在和朋友讨论之后，我想我应该是弄懂了。所以特地记下一篇笔记，以便回头翻阅的时候能够想起来。如果理解有误，欢迎在下方评论指出，一起讨论！&lt;/p&gt;</summary>
    
    
    
    <category term="笔记" scheme="https://molunerfinn.com/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Nodejs" scheme="https://molunerfinn.com/tags/Nodejs/"/>
    
    <category term="JS" scheme="https://molunerfinn.com/tags/JS/"/>
    
    <category term="note" scheme="https://molunerfinn.com/tags/note/"/>
    
  </entry>
  
  <entry>
    <title>【NOTE】进程-线程-协程 关系与区别</title>
    <link href="https://molunerfinn.com/process-thread-coroutine/"/>
    <id>https://molunerfinn.com/process-thread-coroutine/</id>
    <published>2018-05-11T22:02:00.000Z</published>
    <updated>2026-03-08T01:14:37.986Z</updated>
    
    <content type="html"><![CDATA[<p>在平时总会听到「进程」、「线程」，甚至最近由于Golang的火热我还听到了「协程」。但是平时我对这三个概念并不能很好的理解，甚至不知它们之间的区别和联系。所以专门找了时间了解了一下它们。本文仅为个人笔记，如有错误或者侵权行为请及时在下方评论里指出！感谢。</p><span id="more"></span><h2 id="进程"><a href="#进程" class="headerlink" title="进程"></a>进程</h2><p>一个进程好比是一个程序，它是 <strong>资源分配的最小单位</strong> 。同一时刻执行的进程数不会超过核心数。不过如果问单核CPU能否运行多进程？答案又是肯定的。单核CPU也可以运行多进程，只不过不是同时的，而是极快地在进程间来回切换实现的多进程。举个简单的例子，就算是十年前的单核CPU的电脑，也可以聊QQ的同时看视频。</p><p>电脑中有许多进程需要处于「同时」开启的状态，而利用CPU在进程间的快速切换，可以实现「同时」运行多个程序。而进程切换则意味着需要保留进程切换前的状态，以备切换回去的时候能够继续接着工作。所以进程拥有自己的地址空间，全局变量，文件描述符，各种硬件等等资源。操作系统通过调度CPU去执行进程的记录、回复、切换等等。</p><h2 id="线程"><a href="#线程" class="headerlink" title="线程"></a>线程</h2><p>如果说进程和进程之间相当于程序与程序之间的关系，那么线程与线程之间就相当于程序内的任务和任务之间的关系。所以线程是依赖于进程的，也称为 <strong>「微进程」</strong> 。它是 <strong>程序执行过程中的最小单元</strong> 。</p><p>一个程序内包含了多种任务。打个比方，用播放器看视频的时候，视频输出的画面和声音可以认为是两种任务。当你拖动进度条的时候又触发了另外一种任务。拖动进度条会导致画面和声音都发生变化，如果进程里没有线程的话，那么可能发生的情况就是：</p><p>拖动进度条-&gt;画面更新-&gt;声音更新。你会明显感到画面和声音和进度条不同步。</p><p>但是加上了线程之后，线程能够共享进程的大部分资源，并参与CPU的调度。意味着它能够在进程间进行切换，实现「并发」，从而反馈到使用上就是拖动进度条的同时，画面和声音都同步了。所以我们经常能听到的一个词是「多线程」，就是把一个程序分成多个任务去跑，让任务更快处理。不过线程和线程之间由于某些资源是独占的，会导致锁的问题。例如Python的GIL多线程锁。</p><h2 id="协程"><a href="#协程" class="headerlink" title="协程"></a>协程</h2><p>协程在线程中实现调度。你可以理解为它是 <strong>「微线程」</strong> 。它的调度不来自于CPU，而是完全来自于用户控制（可以理解为用代码控制流程）。协程的执行效率非常高，它的切换不是线程切换，没有线程切换的开销。而且只要线程越多，协程的性能优势就越明显。协程不需要多线程的锁机制，只需要判断状态即可。不过协程本身无法利用多核CPU，因为它基于线程，而线程又依赖于进程。</p><p>在JS里，常见的协程就是ES6的<code>yield Generator</code>或者ES7的<code>async await</code>。我们知道JS引擎是单线程的。所以在处理异步任务队列的时候，以往我们会陷入「回调金字塔」或者「回调地狱」。而有了协程之后我们可以在代码层面上来控制我们的程序。</p><p>比如我们有这么一个需求，等两个请求都返回之后，用它们的返回值共同做些事。（此处不用<code>Promise.all()</code>来实现，不是说不行，而是为了更好地说明主题）</p><p><strong>ES6 + co</strong> 的写法：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> axios = <span class="built_in">require</span>(<span class="string">&#x27;axios&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> co = <span class="built_in">require</span>(<span class="string">&#x27;co&#x27;</span>)</span><br><span class="line"><span class="title function_">co</span>(<span class="keyword">function</span>* ()&#123;</span><br><span class="line">  <span class="keyword">const</span> getData = <span class="keyword">yield</span> axios.<span class="title function_">get</span>(<span class="string">&#x27;xxx&#x27;</span>)</span><br><span class="line">  <span class="keyword">const</span> postData = <span class="keyword">yield</span> axios.<span class="title function_">post</span>(<span class="string">&#x27;xxx&#x27;</span>)</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(getData, postData)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><strong>ES7</strong> 的写法：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> axios = <span class="built_in">require</span>(<span class="string">&#x27;axios&#x27;</span>)</span><br><span class="line">(<span class="keyword">async</span> <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">const</span> getData = <span class="keyword">await</span> axios.<span class="title function_">get</span>(<span class="string">&#x27;xxx&#x27;</span>)</span><br><span class="line">  <span class="keyword">const</span> postData = <span class="keyword">await</span> axios.<span class="title function_">post</span>(<span class="string">&#x27;xxx&#x27;</span>)</span><br><span class="line">  <span class="variable language_">console</span>.<span class="title function_">log</span>(getData, postData)</span><br><span class="line">&#125;)()</span><br></pre></td></tr></table></figure><p>上述用「同步」的方式写的代码实际上依然是异步执行的。不过因为了有协程，在单线程的JS里也能够让我们在代码层面上实现了任务调度。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>可以说三者虽然是不同的东西，但是有着很密切的关系和类似的特性。它们的关系是从大到小，从上而下的。没有进程也就没有线程也就没有协程。总的来说，在多核处理器的情况下，多进程+多协程可以发挥最优的性能。</p><h2 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h2><ol><li><a href="https://www.jianshu.com/p/f11724034d50">进程，线程，协程与并行，并发</a></li><li><a href="http://www.cnblogs.com/lxmhhy/p/6041001.html">进程和线程、协程的区别</a></li><li><a href="https://blog.csdn.net/blateyang/article/details/78088851">进程、线程和协程的比较</a></li><li><a href="http://jsonliangyoujun.iteye.com/blog/2358274">线程、进程与处理器</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;在平时总会听到「进程」、「线程」，甚至最近由于Golang的火热我还听到了「协程」。但是平时我对这三个概念并不能很好的理解，甚至不知它们之间的区别和联系。所以专门找了时间了解了一下它们。本文仅为个人笔记，如有错误或者侵权行为请及时在下方评论里指出！感谢。&lt;/p&gt;</summary>
    
    
    
    <category term="笔记" scheme="https://molunerfinn.com/categories/%E7%AC%94%E8%AE%B0/"/>
    
    
    <category term="Nodejs" scheme="https://molunerfinn.com/tags/Nodejs/"/>
    
    <category term="note" scheme="https://molunerfinn.com/tags/note/"/>
    
  </entry>
  
  <entry>
    <title>基于Electron-vue的图床上传工具PicGo v1.5更新说明</title>
    <link href="https://molunerfinn.com/picgo-v1.5-update/"/>
    <id>https://molunerfinn.com/picgo-v1.5-update/</id>
    <published>2018-05-08T21:25:00.000Z</published>
    <updated>2026-03-08T01:14:37.986Z</updated>
    
    <content type="html"><![CDATA[<p>经过一个多月的努（lan）力（duo）开发，基于electron的图床上传工具<a href="https://github.com/Molunerfinn/PicGo">PicGo</a>终于迎来了一个minor版本的更新。如果你对此感兴趣，不妨看看都更新了哪些有趣而实用的功能吧。</p><span id="more"></span><h3 id="支持GitHub图床"><a href="#支持GitHub图床" class="headerlink" title="支持GitHub图床"></a>支持GitHub图床</h3><p>早先PicGo所支持的图床基本上都是属于国内的服务商提供的图床（如七牛、腾讯云COS等），这次更新加入了GitHub图床的支持。用GitHub做图床其实是不少写博客的朋友的做法。免费、原生支持HTTPS、GitHub仓库易于管理、和issue等功能无缝衔接都是它的优点。如果能接受GitHub在国内的访问速度不是特别快的缺点的话，用它来做你的图床是个不错的选择。来看看在PicGo里如何配置它：</p><p>**1. **首先你得有一个GitHub账号。注册GitHub就不用我多言。</p><p>**2. **新建一个仓库</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/create_new_repo.png"></p><p>记下你取的仓库名。</p><p>**3. **生成一个token用于PicGo操作你的仓库：</p><p>访问：<a href="https://github.com/settings/tokens">https://github.com/settings/tokens</a></p><p>然后点击<code>Generate new token</code>。</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/generate_new_token.png"></p><p>把repo的勾打上即可。然后翻到页面最底部，点击<code>Generate token</code>的绿色按钮生成token。</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/20180508210435.png"></p><p>**注意：**这个token生成后只会显示一次！你要把这个token复制一下存到其他地方以备以后要用。</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/copy_token.png"></p><p>**4. **配置PicGo</p><p>**注意：**仓库名的格式是<code>用户名/仓库</code>，比如我创建了一个叫做<code>test</code>的仓库，在PicGo里我要设定的仓库名就是<code>Molunerfinn/test</code>。一般我们选择<code>master</code>分支即可。然后记得点击确定以生效，然后可以点击<code>设为默认图床</code>来确保上传的图床是GitHub。</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/setup_github.png"></p><p>至此配置完毕，已经可以使用了。当你上传的时候，你会发现你的仓库里也会增加新的图片了：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/success.png"></p><h3 id="支持腾讯云COS-v5版本"><a href="#支持腾讯云COS-v5版本" class="headerlink" title="支持腾讯云COS v5版本"></a>支持腾讯云COS v5版本</h3><blockquote><p>在支持腾讯云COS的路上，我可谓是费了一番心血。首先是官方提供的node-sdk对我来说基本属于瘫痪状态，只能上传具体文件而不能上传base64编码后的文件。而且居然还有v4和v5两个版本的COS，甚至两个版本的认证签名、上传url等等都<strong>完！全！不！同！</strong>。由于之前我只有v4版本的COS权限，只能开发和测试出v4版本的上传。而近来发现很多朋友用的都已经是v5版本的了，所以我提交了一个工单向腾讯云申请了v5版本的权限，没想到很快就给我派发权限了。于是就有了v5版本的面世。目前市面上能同时支持v4、v5版本COS的估计也只有PicGo了！</p></blockquote><p>如果你是v5用户，但是之前下载了PicGo却不能用的话，别担心，v1.5版本的配置跟之前的配置几乎一致，而且可以一键切换v4\v5版本。</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/v5_setup.png"></p><p>**1. **获取你的APPID、SecretId和SecretKey</p><p>访问：<a href="https://console.cloud.tencent.com/cam/capi">https://console.cloud.tencent.com/cam/capi</a></p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/get_key_id_secret.png"></p><p>**2. **获取bucket名以及存储区域代号</p><p>访问：<a href="https://console.cloud.tencent.com/cos5/bucket">https://console.cloud.tencent.com/cos5/bucket</a></p><p>创建一个存储桶。然后找到你的存储桶名和存储区域代号：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/get_bucket_area.png"></p><p>v5版本的存储桶名称格式是<code>bucket-appId</code>，类似于<code>xxxx-12312313</code>。存储区域代码和v4版本的也有所区别，v5版本的如我的是<code>ap-beijing</code>，别复制错了。</p><p>**3. **选择v5版本并点击确定</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/choose_v5.png"></p><p>然后记得点击<code>设为默认图床</code>，这样上传才会默认走的是腾讯云COS。</p><h3 id="支持编辑相册的图片信息"><a href="#支持编辑相册的图片信息" class="headerlink" title="支持编辑相册的图片信息"></a>支持编辑相册的图片信息</h3><p>有些时候可能上传的图片的url事后需要更改，比如修改http到https，比如加上一些操作后缀（例：七牛图床支持的<code>?imgslim</code>）等等。PicGo本次的更新也让你能够更方便地管理你的图片库。</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo_edit_info.gif"></p><h3 id="支持上传图片前重命名文件名"><a href="#支持上传图片前重命名文件名" class="headerlink" title="支持上传图片前重命名文件名"></a>支持上传图片前重命名文件名</h3><p>PicGo总共有三种上传模式：</p><ol><li>menubar图标拖拽上传（仅支持macOS）</li><li>主窗口拖拽或者选择图片上传</li><li>剪贴板图片（最常见的是截图）上传（支持自定义快捷键）</li></ol><p>其中前两种都是可以明确获得文件名，而第三种无法获取文件名（因为剪贴板里有些图片比如截图根本就不存在文件名），所以PicGo此前采取的规则是使用时间戳来命名剪贴板里的图片。这也导致了无法自定义文件名的问题。本次更新你可以选择开启「上传前重命名」这个选项：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/rename_before_upload.png"></p><p>之后你在上传的时候就会弹出一个小窗口让你重命名文件。如果你不想重命名，点击确定、取消或者直接关闭这个窗口都是可以的。如果你想要重命名就在输入框里输入想要更改的名字，然后点击确定即可。另外这个特性也支持批量上传，如下：</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picgo_rename.gif"></p><h3 id="支持查看当前上传的图床"><a href="#支持查看当前上传的图床" class="headerlink" title="支持查看当前上传的图床"></a>支持查看当前上传的图床</h3><p>在主窗口的上传区，你可以直观地看到当前默认上传的图床，再也不用到处找当前的默认图床是哪个啦。</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/current_picbed.png"></p><h3 id="支持显示或隐藏相应的图床"><a href="#支持显示或隐藏相应的图床" class="headerlink" title="支持显示或隐藏相应的图床"></a>支持显示或隐藏相应的图床</h3><p>很多时候你并不会使用上PicGo给你提供的全部的图床。所以为了精简显示你可以只选择你想要的图床来显示，这样侧边栏也就不会出现滚动条了。不过需要注意的是，这个仅仅是显示&#x2F;隐藏而并不是剔除相应的功能。假如你隐藏了七牛云，你依然是可以通过七牛云来上传图片的。</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/picbed-choose.gif"></p><h3 id="支持开机自启动"><a href="#支持开机自启动" class="headerlink" title="支持开机自启动"></a>支持开机自启动</h3><p>如果你觉得每次开机要主动开启PicGo是一件麻烦事，不妨试试让它开机自启吧~</p><p><img src="https://raw.githubusercontent.com/Molunerfinn/test/master/picgo/autoStart.png"></p><h3 id="修复若干bugs"><a href="#修复若干bugs" class="headerlink" title="修复若干bugs"></a>修复若干bugs</h3><p>v1.5不光更新了上述功能，也修复了不少问题。其中一个尤为重要的是从v1.4.1开始的一个bug——macOS的menubar无法拖拽上传。该bug也在这个版本被修复。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>PicGo第一个稳定版本是在少数派上发布的，详见<a href="https://sspai.com/post/42310">PicGo：基于 Electron 的图片上传工具</a>。支持macOS和windows双平台，开源免费，界面美观，也得到了很多朋友的认可。本次更新也是充分聆听了大家的<a href="https://github.com/Molunerfinn/PicGo/issues/29">意见</a>。如果你对它有什么意见或者建议，也欢迎在<a href="https://github.com/Molunerfinn/PicGo/issues">issues</a>里指出。如果你喜欢它，不妨给它点个star或者请我喝杯咖啡（PicGo的GitHub<a href="https://github.com/Molunerfinn/PicGo">首页</a>有赞助的二维码）？</p><blockquote><p>下载地址：<a href="https://github.com/Molunerfinn/PicGo/releases">https://github.com/Molunerfinn/PicGo/releases</a></p></blockquote><blockquote><p>Windows用户请下载<code>.exe</code>文件，macOS用户请下载<code>.dmg</code>文件。</p></blockquote><p>Happy uploading！</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;经过一个多月的努（lan）力（duo）开发，基于electron的图床上传工具&lt;a href=&quot;https://github.com/Molunerfinn/PicGo&quot;&gt;PicGo&lt;/a&gt;终于迎来了一个minor版本的更新。如果你对此感兴趣，不妨看看都更新了哪些有趣而实用的功能吧。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://molunerfinn.com/categories/Web/"/>
    
    <category term="开发" scheme="https://molunerfinn.com/categories/Web/%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="前端" scheme="https://molunerfinn.com/tags/%E5%89%8D%E7%AB%AF/"/>
    
    <category term="Electron" scheme="https://molunerfinn.com/tags/Electron/"/>
    
    <category term="Vue" scheme="https://molunerfinn.com/tags/Vue/"/>
    
    <category term="Electron-vue" scheme="https://molunerfinn.com/tags/Electron-vue/"/>
    
  </entry>
  
</feed>
