Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Electron 应用实战 (架构篇) #13

Open
sorrycc opened this issue Dec 8, 2016 · 35 comments

Comments

Projects
None yet
@sorrycc
Copy link
Owner

commented Dec 8, 2016

以下内容已整合到脚手架:https://github.com/sorrycc/dva-boilerplate-electron

近期,我们在内部做了一个类似 IDE 性质的应用,基于 electron。过程中趟过不少坑,也有了些心得,记录如下。

包含:

  • 数据通讯
  • 架构方案
  • Two-Package 目录结构
  • 源码打包
  • 应用打包

数据通讯

数据通讯方案决定整体的架构方案。

翻翻 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 端。

最终方案

上述两个方案的缺点是:

  1. main 和 renderer 均包含业务逻辑
  2. 通讯逻辑书写复杂

我们的最终方案是:

  1. main 端无逻辑,全部抽象为 services,提供函数级的方案,就和 restful 的服务端一样,写完后等着被调
  2. 由于打包的原因,我们把 main 和 renderer 分别打包成一个文件。所以 main 的 services 要暴露到全局变量,比如 global.services,这样在 renderer 里才能通过 remote.getGlobal('services') 调用到
  3. renderer 端包含大量业务逻辑,需和 main 通讯时通过 remote 来调
  4. renderer 端我们选择 react + dva 来组织代码,把所有逻辑存于 model,保证数据和视图的彻底分离,以及逻辑的清晰
  5. 目前还没有遇到 main 主动推消息到 renderer 的需求

参考

Two-Package 目录结构

定完整体架构之后,就要确定目录结构了,以及如何做构建和打包等等。我们在这也是绕了好大一圈,因为 electron 官网没有推荐这个,后面慢慢翻文档才发现这种组织方式的好处。

先说结论,我们采用的是 Two-Package 的目录结构,并且基于 webpack 打包 main 和 renderer 。

啥是 Two-Package Structure?

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

为啥用 Two-Package Structure?

最大的好处是可以很好地分离开发依赖和生成环境依赖。开发依赖存 package.json,生产依赖存 app/package.json,这样在 pack 后交付给用户时就不会包含 webpack, mocha 等等的开发依赖了。

那么怎么区别依赖类型呢? 比如:

  • main 端依赖了 fs-extra 是不是生产环境依赖?
  • renderer 端依赖了 react 是不是生产环境依赖?

这没有标准答案,和源码打包策略有关,即 src 目录的源码是如何到 app/dist 下的。

资源

源码打包

首先打包我们是用的 webpack + babel,分别把 src/mainsrc/renderer 下的文件打包为 app/dist/main.jsapp/dist/renderer.js。打包 renderer 可以理解,打包 main 可能有人会有疑问。我们打包 main 是为了编码风格的一致。

externals

我们需要 externals 掉一些不能或不应该被打包到一起的依赖。

  1. renderer 端我们只 externals 了 electron
  2. main 端我们 externals 了所有的依赖

这样,renderer 端所有的依赖都是开发依赖,main 端的所有依赖都是生产依赖。

所以,在这种打包机制下,前面的问题就有了答案:

  • main 端依赖了 fs-extra 是不是生产环境依赖? --
  • renderer 端依赖了 react 是不是生产环境依赖? -- 不是

externals 配置

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 的约定,以及自动更新等等功能。

Rebuild native-module

由于我们用了 pty.js,包含 C++ 的原生实现。所以在 papck 前需先用 electron-rebuild 做 rebuild。

npm scripts

{
  "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"
}

Tips

  • rebuild 如果很慢,可能是要翻墙,可尝试 cnpmjs.org 提供的镜像,electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/

资源

(完)

@oldj

This comment has been minimized.

Copy link

commented Dec 9, 2016

如果正在编辑的文件,被其他程序改了,是否会需要 main 主动推消息到 renderer 以便让后者更新内容?

目前还没有遇到 main 主动推消息到 renderer 的需求

@sorrycc

This comment has been minimized.

Copy link
Owner Author

commented Dec 9, 2016

@oldj main 提供 watch services,renderer 里调,这样接收到变更等事件后的处理全部在 renderer 里处理就可以了。

@sorrycc sorrycc added the Electron label Dec 9, 2016

@Jirapo

This comment has been minimized.

Copy link

commented Dec 10, 2016

package.json 里面少了: electron-is, electron-config, electron-log。
remote.getGlobals('services') , 应该更改为: remote.getGlobal('services') .

@sorrycc

This comment has been minimized.

Copy link
Owner Author

commented Dec 10, 2016

package.json 里面少了: electron-is, electron-config, electron-log。

这个依赖在 app/package.json 中。

remote.getGlobals('services') , 应该更改为: remote.getGlobal('services') .

感谢提醒。

@Jirapo

This comment has been minimized.

Copy link

commented Dec 11, 2016

修改文件之后,热加载要等一段时间才刷网页,反应有些慢。
如果是menu里面的操作,比如打开文件,这个是main端发起的请求,并不是renderer端主动发起的,这个是怎么解决呢?还是有其他办法把menu里面的请求变成renderer端发起的?

@sorrycc

This comment has been minimized.

Copy link
Owner Author

commented Dec 12, 2016

修改文件之后,热加载要等一段时间才刷网页,反应有些慢。

这个没办法的,慢是因为要 build 。

如果是menu里面的操作,比如打开文件,这个是main端发起的请求,并不是renderer端主动发起的,这个是怎么解决呢?还是有其他办法把menu里面的请求变成renderer端发起的?

打开文件从 main 端发起吧,renderer 端监听好了。

@wotermelon

This comment has been minimized.

Copy link

commented Dec 14, 2016

1、renderer 端

const content = remote.require('fs').readFileSync('/path/to/file');

electron的renderer端可以直接使用node相关模块,为什么不是这样子:

const content = require('fs').readFileSync('/path/to/file');

2、大量使用remote是否会造成内存问题?

@sorrycc

This comment has been minimized.

Copy link
Owner Author

commented Dec 14, 2016

electron的renderer端可以直接使用node相关模块,为什么不是这样子:

renderer 端并不能直接使用 node 模块吧。

2、大量使用remote是否会造成内存问题?

main 端不存数据,数据全部存于 renderer 端。在 renderer 端做好控制,内存没理由会上涨。

@carlos121493

This comment has been minimized.

Copy link

commented Dec 14, 2016

html用相对路径dist/renderer.js引入本地文件rebuild时感觉会略慢,可以架一个koa服务器基于环境判断引入脚本,用于热更新么?

@sorrycc

This comment has been minimized.

Copy link
Owner Author

commented Dec 14, 2016

@carlos121493 打算做但还没做的,会基于 webpack-dev-server,欢迎 PR 。

@wotermelon

This comment has been minimized.

Copy link

commented Dec 14, 2016

@sorrycc 渲染进程可以直接使用node模块。

@Jirapo

This comment has been minimized.

Copy link

commented Dec 15, 2016

@Zhangmingze
如果渲染进程直接使用node模块,那估计是在index.html中直接使用

<script> require('./src/renderer/index.js') </script>

这样打包出来不会很大么?

@LucasYuNju

This comment has been minimized.

Copy link

commented Dec 31, 2016

@Zhangmingze @sorrycc 渲染进程确实是可以直接require fs的。

Electron doc:

Renderer Process

...
In normal browsers, web pages usually run in a sandboxed environment and are not allowed access to native resources. Electron users, however, have the power to use Node.js APIs in web pages allowing lower level operating system interactions.

另外,externals的配置,是否和target: "electron-renderer"是等价的?

@fomenyesu

This comment has been minimized.

Copy link

commented Jan 5, 2017

renderer 端 可以直接使用 node模块。用过读写文件的模块没有问题。 @sorrycc

@xiaoluoboding

This comment has been minimized.

Copy link

commented Mar 21, 2017

⚠️ ⚠️ ⚠️  It\'s not recommended to use webpack.config.js, since roadhog\'s major or minor
 version upgrades may result in incompatibility. If you insist on doing so, please 
be careful of the compatibility after upgrading roadhog.

按照模版npm install之后执行npm run dev报这样的错,如何解决?需要降低版本嘛?

@sorrycc

This comment has been minimized.

Copy link
Owner Author

commented Mar 21, 2017

@xiaoluoboding 忽略这个警告。

@liihuu

This comment has been minimized.

Copy link

commented Mar 22, 2017

调用c++,生成了.node,在渲染进程直接require引用会报错,原因是在webpack前没有rebuild么?

@lxlzero

This comment has been minimized.

Copy link

commented Mar 26, 2017

我clone下来得项目,经过npm install,npm run dev,npm start都是正常的,可是我npm run pack打包项目的时候【× Rebuild Failed
An unhandled error occurred inside electron-rebuild
ENOENT: no such file or directory, open 'D:\Working\electron\electron-dva\dva-boilerplate-electron\boilerplate\app\n
ode_modules\package.json'】,怎么怎么解呢?

@miaosun009

This comment has been minimized.

Copy link

commented May 3, 2017

执行npm run rebuild 的时候出现以下错误,是什么原因?

PS D:\node\edva> npm run rebuild

@ rebuild D:\node\edva
electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/ -m ./app/node_modules

× Rebuild Failed
An unhandled error occurred inside electron-rebuild
ENOENT: no such file or directory, open 'D:\node\edva\app\node_modules\package.json'

Error: ENOENT: no such file or directory, open 'D:\node\edva\app\node_modules\package.json'

npm ERR! Windows_NT 10.0.14393
npm ERR! argv "C:\Program Files\nodejs\node.exe" "C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js" "run" "rebuild"
npm ERR! node v7.7.1
npm ERR! npm v4.1.2
npm ERR! code ELIFECYCLE
npm ERR! @ rebuild: electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/ -m ./app/node_modules
npm ERR! Exit status 4294967295
npm ERR!
npm ERR! Failed at the @ rebuild script 'electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/ -m ./app/node_modules'.
npm ERR! Make sure you have the latest version of node.js and npm installed.
npm ERR! If you do, this is most likely a problem with the package,
npm ERR! not with npm itself.
npm ERR! Tell the author that this fails on your system:
npm ERR! electron-rebuild -d=https://gh-contractor-zcbenz.cnpmjs.org/atom-shell/dist/ -m ./app/node_modules
npm ERR! You can get information on how to open an issue for this project with:
npm ERR! npm bugs
npm ERR! Or if that isn't available, you can get their info via:
npm ERR! npm owner ls
npm ERR! There is likely additional logging output above.

npm ERR! Please include the following file with any support request:
npm ERR! D:\node\edva\npm-debug.log

@train860

This comment has been minimized.

Copy link

commented Jun 9, 2017

@miaosun009 package.json 里面的rebuild修改为"rebuild": "electron-rebuild -m ./app",

@peace-wr

This comment has been minimized.

Copy link

commented Jun 12, 2017

请教下,直接clone下code后,跑npm run dev 正常,但是不自动打开浏览器,需要手动输入http://localhost:8000,并且这个打开也不是例子中的页面,必须http://localhost:8000/main-dev.html这样才是样例,请问,还需要哪里做啥设置吗?直接npm start,不报错但是弹出的应用是空白的,啥也不显示,请教如何才能正常显示呢?

@peace-wr

This comment has been minimized.

Copy link

commented Jun 12, 2017

经测试发现:npm run dev不关闭的情况下,运行npm start可以跑。再请教个问题,就是打包的问题,打包完毕后不报错,里面的内容都是空的,看console里各种报错,都是路径找不到,在哪里修改路径?

@baiyulong

This comment has been minimized.

Copy link

commented Nov 24, 2017

Hi, 可以升级react 和 react-dom 到 v16吗?

@agalwood

This comment has been minimized.

Copy link

commented Dec 14, 2017

main 端

ipcMain.on('readFile', (event, { filePath }) => {
  content content = fs.readFileSync(filePath, 'utf-8');
  event.sender.send('readFileSuccess', { content });
});
content content = fs.readFileSync(filePath, 'utf-8');

这行楼主是不是输错了,应该是

const content = fs.readFileSync(filePath, 'utf-8');
@zhanyouwei

This comment has been minimized.

Copy link

commented Dec 15, 2017

ipcMain 没有主动send的方法,主进程怎么主动和render进程通信呢

@oldj

This comment has been minimized.

Copy link

commented Dec 15, 2017

@zhanyouwei 我的做法是 render 进程初始化时,先给主进程发一个注册消息,主进程里收到消息后,将对应的 sender 保存起来,这样后面主进程就随时可以给这个 render 进程发消息了。

@zhebocaozuozenmeshuo

This comment has been minimized.

Copy link

commented Jan 8, 2018

下载代码直接打包后,图片资源未能正确加载.......

@JianmingXia

This comment has been minimized.

Copy link

commented Mar 2, 2018

之前基于 antd + dva写了一个项目,如何快速接入electron,重要的是路由这块怎么处理

@zhanyouwei

This comment has been minimized.

Copy link

commented Mar 2, 2018

@JianmingXia 如果你只需要套一个壳的话只需要制定入口以及将路由改成hash模式就可以了

@zhanyouwei

This comment has been minimized.

Copy link

commented Mar 2, 2018

@oldj 后来发现window实例可以通过webcontents API发送消息

@JianmingXia

This comment has been minimized.

Copy link

commented Mar 4, 2018

@zhanyouwei 路由为什么要改成hash模式,能解答一下么

@fancymo

This comment has been minimized.

Copy link

commented Mar 27, 2018

看了下,关于two package.json structure已更新。
Since version 8 electron-builder rebuilds only production dependencies, so, you are not forced to use two package.json structure.

@9777

This comment has been minimized.

Copy link

commented Jul 16, 2018

使用npm run dev,运行项目时。后面报内存溢出了。请问该如何解决呢?错误如下
[0] <--- Last few GCs --->
[0]
[0] 52387 ms: Mark-sweep 682.2 (697.3) -> 682.2 (698.3) MB, 154.1 / 0.0 ms [allocation failure] [GC in old space requested].
[0] 52581 ms: Mark-sweep 682.2 (698.3) -> 682.2 (699.3) MB, 194.1 / 0.0 ms [allocation failure] [GC in old space requested].
[0] 52745 ms: Mark-sweep 682.2 (699.3) -> 682.2 (697.3) MB, 163.7 / 0.0 ms [last resort gc].
[0] 52916 ms: Mark-sweep 682.2 (697.3) -> 682.2 (697.3) MB, 171.6 / 0.0 ms [last resort gc].
[0]
[0]
[0] <--- JS stacktrace --->
[0]
[0] ==== JS stack trace =========================================
[0]
[0] Security context: 1D67B815
[0] 1: fromString(aka fromString) [buffer.js:~186] [pc=06858679] (this=1D6081D9 ,string=0D09FD25 <Very long string[2643859]>,encoding=1D6826E5 <String[4]: utf8>)
[0] 2: new constructor(aka Buffer) [buffer.js:~70] [pc=05B205D4] (this=0D09FD39 <a Buffer with map 1C59334D>,arg=0D09FD25 <Very long string[2643859]>,encodingOrOffset=1D6826E5 <String[4]: utf8>,le
ngth=1D6081D9 )
[0] 3: ...
[0]
[0] FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
[0] npm run dev:renderer exited with code 0

@leaez

This comment has been minimized.

Copy link

commented Apr 4, 2019

既然electron-builder v8 解决了tow package问题, 那么能否提供一个基于umi, 单个package的 electron编译项目实例.

@roroyu

This comment has been minimized.

Copy link

commented Apr 11, 2019

折腾了一天多, 也没弄好。。。。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.