以下内容已整合到脚手架: https://github.com/sorrycc/dva-boilerplate-electron
近期,我们在内部做了一个类似 IDE 性质的应用,基于 electron。过程中趟过不少坑,也有了些心得,记录如下。
包含:
数据通讯方案决定整体的架构方案。
翻翻 Electron 文档,应该不难发现,Electron 有两个进程,分别为 main 和 renderer,而两者之间是通过 ipc 进行通讯。main 端有 ipcMain,renderer 端有 ipcRenderer,分别用于通讯。
一个简单的读取文件的例子:
main 端
ipcMain.on('readFile', (event, { filePath }) => { content content = fs.readFileSync(filePath, 'utf-8'); event.sender.send('readFileSuccess', { content }); });
renderer 端
ipcRenderer.on('readFileSuccess', (event, { content }) => { console.log(`content: ${content}`); }); ipcRender.send('readFile', { filePath: '/path/to/file', });
我们刚开始也是这么做, 但过了几星期发现太绕,于是重构成通过 remote 方式。 remote 是一种简化的通讯方案,内部也是 ipc,所以运行起来和前面的方案并无差别,但使用上简化很多。比如,上面的例子可简化如下:
main 端
无
renderer 端
const content = remote.require('fs').readFileSync('/path/to/file');
架构方案有多种,选择适合自己的。
在架构方案的选择上纠结过很久,不过这很大程度是和前面的通讯方案有关的。
传统 ipc 方案,main 端用 ipcMain, renderer 端用 ipcRenderer。
main 端和 renderer 端分别部署一个dva(不了解 dva 的可以理解为 redux),封装 ipc 基于 action 通讯。main 端的 action 如果包含 toRenderer
会自动走到 renderer 端的,反之 renderer 端的 action 如果包含 toMain
则自动走到 main 端。
上述两个方案的缺点是:
我们的最终方案是:
global.services
,这样在 renderer 里才能通过 remote.getGlobals('services')
调用到 定完整体架构之后,就要确定目录结构了,以及如何做构建和打包等等。我们在这也是绕了好大一圈,因为 electron 官网没有推荐这个,后面慢慢翻文档才发现这种组织方式的好处。
Two-Package Structure 是 pack 工具 electron-builder 给的约定,也是目前业界用的较多的方案。
+ 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
最大的好处是可以很好地分离开发依赖和生成环境依赖。开发依赖存 package.json
,生产依赖存 app/package.json
,这样在 pack 后交付给用户时就不会包含 webpack, mocha 等等的开发依赖了。
那么怎么区别依赖类型呢? 比如:
这没有标准答案,和源码打包策略有关,即 src
目录的源码是如何到 app/dist
下的。
首先打包我们是用的 webpack + babel,分别把 src/main
和 src/renderer
下的文件打包为 app/dist/main.js
和 app/dist/renderer.js
。打包 renderer 可以理解,打包 main 可能有人会有疑问。我们打包 main 是为了编码风格的一致。
我们需要 externals 掉一些不能或不应该被打包到一起的依赖。
electron
这样,renderer 端所有的依赖都是开发依赖,main 端的所有依赖都是生产依赖。
所以,在这种打包机制下,前面的问题就有了答案:
main
externals(context, request, callback) { callback(null, request.charAt(0) === '.' ? false : `require("${request}")`); },
renderer
externals(context, request, callback) { let isExternal = false; const load = [ 'electron', ]; if (load.includes(request)) { isExternal = `require("${request}")`; } callback(null, isExternal); },
翻下 electron 开源应用的源码,我们会发现有些是用 electron-packager,有些是用 electron-builder 。这两个是什么关系?我们应该用哪个呢?
答案是用 electron-builder。electron-builder 是基于 electron-packager 实现的,并在此基础上做了 Two-Package.json Structure 的约定,以及自动更新等等功能。
由于我们用了 pty.js,包含 C++ 的原生实现。所以在 papck 前需先用 electron-rebuild 做 rebuild。
{ "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" }
electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/
。 (完)