Electron 应用实战 (架构篇)

lyshengun 7年前
   <p>近期,我们在内部做了一个类似 IDE 性质的应用,基于 electron。过程中趟过不少坑,也有了些心得,记录如下。</p>    <p>包含:</p>    <ul>     <li>数据通讯</li>     <li>架构方案</li>     <li>Two-Package 目录结构</li>     <li>源码打包</li>     <li>应用打包</li>    </ul>    <h2>数据通讯</h2>    <p>数据通讯方案决定整体的架构方案。</p>    <p>翻翻 Electron 文档,应该不难发现,Electron 有两个进程,分别为 main 和 renderer,而两者之间是通过 ipc 进行通讯。main 端有 ipcMain,renderer 端有 ipcRenderer,分别用于通讯。</p>    <p><img src="https://simg.open-open.com/show/66f7a004e4b3bba77b5160baff2d4cae.png"></p>    <p>一个简单的读取文件的例子:</p>    <p>main 端</p>    <pre>  ipcMain.on('readFile', (event, { filePath }) => {    content content = fs.readFileSync(filePath, 'utf-8');    event.sender.send('readFileSuccess', { content });  });</pre>    <p>renderer 端</p>    <pre>  ipcRenderer.on('readFileSuccess', (event, { content }) => {    console.log(`content: ${content}`);  });  ipcRender.send('readFile', {    filePath: '/path/to/file',  });</pre>    <p>我们刚开始也是这么做, <strong>但过了几星期发现太绕,于是重构成通过 remote 方式。</strong> remote 是一种简化的通讯方案,内部也是 ipc,所以运行起来和前面的方案并无差别,但使用上简化很多。比如,上面的例子可简化如下:</p>    <p>main 端</p>    <pre>  无</pre>    <p>renderer 端</p>    <pre>  const content = remote.require('fs').readFileSync('/path/to/file');</pre>    <h3>参考</h3>    <ul>     <li><a href="/misc/goto?guid=4959728697605925459" rel="nofollow,noindex">http://electron.atom.io/docs/api/remote/</a></li>     <li><a href="/misc/goto?guid=4959728697697348490" rel="nofollow,noindex">http://jlord.us/essential-electron/</a></li>    </ul>    <h2>架构选择</h2>    <p>架构方案有多种,选择适合自己的。</p>    <h3>选择</h3>    <p>在架构方案的选择上纠结过很久,不过这很大程度是和前面的通讯方案有关的。</p>    <p>方案一</p>    <p>传统 ipc 方案,main 端用 ipcMain, renderer 端用 ipcRenderer。</p>    <p>方案二</p>    <p>main 端和 renderer 端分别部署一个dva(不了解 dva 的可以理解为 redux),封装 ipc 基于 action 通讯。main 端的 action 如果包含 toRenderer 会自动走到 renderer 端的,反之 renderer 端的 action 如果包含 toMain 则自动走到 main 端。</p>    <h3>最终方案</h3>    <p>上述两个方案的缺点是:</p>    <ol>     <li>main 和 renderer 均包含业务逻辑</li>     <li>通讯逻辑书写复杂</li>    </ol>    <p>我们的最终方案是:</p>    <ol>     <li>main 端无逻辑,全部抽象为 services,提供函数级的方案,就和 restful 的服务端一样,写完后等着被调</li>     <li>由于打包的原因,我们把 main 和 renderer 分别打包成一个文件。所以 main 的 services 要暴露到全局变量,比如 global.services ,这样在 renderer 里才能通过 remote.getGlobals('services') 调用到</li>     <li>renderer 端包含大量业务逻辑,需和 main 通讯时通过 remote 来调</li>     <li>renderer 端我们选择react +dva 来组织代码,把所有逻辑存于 model,保证数据和视图的彻底分离,以及逻辑的清晰</li>     <li>目前还没有遇到 main 主动推消息到 renderer 的需求</li>    </ol>    <h3>参考</h3>    <ul>     <li><a href="/misc/goto?guid=4959009805889079702" rel="nofollow,noindex">https://github.com/dvajs/dva</a></li>    </ul>    <h2>Two-Package 目录结构</h2>    <p>定完整体架构之后,就要确定目录结构了,以及如何做构建和打包等等。我们在这也是绕了好大一圈,因为 electron 官网没有推荐这个,后面慢慢翻文档才发现这种组织方式的好处。</p>    <p>先说结论,我们采用的是 Two-Package 的目录结构,并且基于 webpack 打包 main 和 renderer 。</p>    <h3>啥是 Two-Package Structure?</h3>    <p><a href="/misc/goto?guid=4959728697815302087" rel="nofollow,noindex">Two-Package Structure</a> 是 pack 工具 electron-builder 给的约定,也是目前业界用的较多的方案。</p>    <pre>  + dist            // pack 完后的输出,.dmg, .exe, .zip, .app 等文件  + build           // background.png, icon.icns, icon.ico  + app             // 用于 pack 给用户的目录    + dist          // src 目录打包完放这里    + assets        // 字体、图片等资源文件    + pages         // 存放页面    - package.json  // 生产依赖,存 dependencies  + src             // 源码    + main          // main    + renderer      // renderer    + shared        // main 和 renderer 公用文件  - package.json    // 开发依赖,存 devDependencies</pre>    <h3>为啥用 Two-Package Structure?</h3>    <p>最大的好处是可以很好地分离开发依赖和生成环境依赖。开发依赖存 package.json ,生产依赖存 app/package.json ,这样在 pack 后交付给用户时就不会包含 webpack, mocha 等等的开发依赖了。</p>    <p>那么怎么区别依赖类型呢? 比如:</p>    <ul>     <li>main 端依赖了 fs-extra 是不是生产环境依赖?</li>     <li>renderer 端依赖了 react 是不是生产环境依赖?</li>    </ul>    <p>这没有标准答案,和源码打包策略有关,即 src 目录的源码是如何到 app/dist 下的。</p>    <h3>资源</h3>    <ul>     <li><a href="/misc/goto?guid=4959728697815302087" rel="nofollow,noindex">https://github.com/electron-userland/electron-builder/wiki/Two-package.json-Structure</a></li>    </ul>    <h2>源码打包</h2>    <p>首先打包我们是用的 webpack + babel,分别把 src/main 和 src/renderer 下的文件打包为 app/dist/main.js 和 app/dist/renderer.js 。打包 renderer 可以理解,打包 main 可能有人会有疑问。我们打包 main 是为了编码风格的一致。</p>    <h3>externals</h3>    <p>我们需要 externals 掉一些不能或不应该被打包到一起的依赖。</p>    <ol>     <li>renderer 端我们只 externals 了 electron</li>     <li>main 端我们 externals 了所有的依赖</li>    </ol>    <p>这样,renderer 端所有的依赖都是开发依赖,main 端的所有依赖都是生产依赖。</p>    <p>所以,在这种打包机制下,前面的问题就有了答案:</p>    <ul>     <li>main 端依赖了 fs-extra 是不是生产环境依赖? -- <strong>是</strong></li>     <li>renderer 端依赖了 react 是不是生产环境依赖? -- <strong>不是</strong></li>    </ul>    <h3>externals 配置</h3>    <p>main</p>    <pre>  externals(context, request, callback) {    callback(null, request.charAt(0) === '.' ? false : `require("${request}")`);  },</pre>    <p>renderer</p>    <pre>  externals(context, request, callback) {    let isExternal = false;    const load = [      'electron',    ];    if (load.includes(request)) {      isExternal = `require("${request}")`;    }    callback(null, isExternal);  },</pre>    <h3>资源</h3>    <ul>     <li><a href="/misc/goto?guid=4959642540200927525" rel="nofollow,noindex">http://webpack.github.io/docs/configuration.html#externals</a></li>    </ul>    <h2>应用打包</h2>    <p>翻下 electron 开源应用的源码,我们会发现有些是用 electron-packager,有些是用 electron-builder 。这两个是什么关系?我们应该用哪个呢?</p>    <p>答案是用 electron-builder。electron-builder 是基于 electron-packager 实现的,并在此基础上做了 Two-Package.json Structure 的约定,以及自动更新等等功能。</p>    <h3>Rebuild native-module</h3>    <p>由于我们用了 pty.js,包含 C++ 的原生实现。所以在 papck 前需先用 electron-rebuild 做 rebuild。</p>    <h3>npm scripts</h3>    <pre>  {    "build": "NODE_ENV=production webpack",    "rebuild": "electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/ -m ./app/node_modules",    "pack": "npm run build && npm run rebuild && build"  }</pre>    <h3>Tips</h3>    <ul>     <li>rebuild 如果很慢,可能是国内或许不能访问,可尝试 cnpmjs.org 提供的镜像, electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/ 。</li>    </ul>    <h3>资源</h3>    <ul>     <li><a href="/misc/goto?guid=4959728697960136951" rel="nofollow,noindex">https://github.com/electron/electron-rebuild</a></li>     <li><a href="/misc/goto?guid=4959728698045783106" rel="nofollow,noindex">https://github.com/electron-userland/electron-builder</a></li>     <li><a href="/misc/goto?guid=4959728698126300004" rel="nofollow,noindex">http://electron.atom.io/docs/tutorial/using-native-node-modules/</a></li>    </ul>    <p> </p>    <p> </p>    <p>来自:https://github.com/sorrycc/blog/issues/13</p>    <p> </p>