IM 跨平台技术学习 (二):Electron 初体验 (快速开始、跨进程通信、打包、踩坑等)
1、引言
在上篇《快速了解新一代跨平台桌面技术——Electron》,我们已经对 Electron 跨端框架有了基本的认识。
本篇将带你简单上手 Electron 框架开发跨平台桌面端,内容包括一个快速开始例子、跨进程通信原理、打包和分发、以及一些典型的技术踩坑等。希望能带给你启发。
2、系列文章
本文是系列文章中的第 2 篇,本系列总目录如下:
《IM 跨平台技术学习(三):vivo 的 Electron 详细技术栈选型、踩坑总结等 》(稍后发布.. )
《IM 跨平台技术学习(四):蘑菇街基于 Electron 开发 IM 客户端的技术实践》(稍后发布.. )
《IM 跨平台技术学习(五):融云基于 Electron 的 IM 跨平台 SDK 改造实践总结》(稍后发布.. )
《IM 跨平台技术学习(六):网易云信基于 Electron 的 IM 消息全文检索技术实践》(稍后发布.. )
3、Electron 简介
Electron 是一个赋力前端进行跨平台开发的框架,让开发人员使用 JavaScript、HTML 和 CSS 等前端技术构建跨平台的桌面应用。
Electron 通过将 Chromium(所有类 Chrome 的浏览器都是基于这个开源工程而来) 和 Node.js 合并到同一个运行时环境中(见下图),并将其打包为 Mac,Windows 和 Linux 系统下的应用,而开发人员只需关注前端代码的开发。
▲ 上图引用自《快速了解新一代跨平台桌面技术——Electron》
Chromium、Node.js、Native API 这三者的作用分别是:
1)Chromium :为 Electron 提供了强大的 UI 能力,可以不考虑传统浏览器兼容性的情况下,利用强大的 Web 生态来开发界面;
2)Node.js:让 Electron 有了底层的操作能力(比如文件的读写,甚至是集成 C++等等操作),并可以使用大量开源的 npm 包来完成开发需求。
3)Native API:Native API 让 Electron 有了跨平台和桌面端的原生能力(比如说它有统一的原生界面,窗口、托盘、消息通知这些)。
Electron 就是通过这三者的巧妙组合,让我们开发跨平台应用变的十分高效。
本质上就是 chromium(chrome 开源版本)浏览器,有最新的东西都会在 chromium 测试,所以 electron 可以体验最新的 api,这也是好处之一。
有关 Electron 的基本介绍等,这里就不再赘述,如果您还不曾了解,可以先阅读本文的上篇。
4、快速开始
4.1 资料准备
Electron 官方提供了一个名为electron-quick-start 的项目,可以 clone 下来当成模版使用,本文使用 create-react-app 来一步一步学习。
其它重要的 Electron 开发资源:
1)Electron 官网:https://electronjs.org;
2)Electron Github:https://github.com/electron;
3)Electron 开发手册:https://www.electronjs.org/zh/docs/latest/。
4.2 创建一个 react 项目
# 安装 create-react-app 命令,如果已将安装请忽略
npm install -g create-react-app
# 创建 electron-react 项目
create-react-app electron-react
# 启动项目
cd electron-react && npm start
4.3 配置 Electron 环境
1)在 public 文件夹下新建 index.html,随便写点内容:
...
<div>hello world</div>
...
2)接下来创建 electron 主线程文件(public/main.js),建议写在 public 路径下面:
const{app, BrowserWindow} = require('electron')
// 创建全局变量并在下面引用,避免被 GC
let win
function createWindow () {
// 创建浏览器窗口并设置宽高
win = newBrowserWindow({ width: 800, height: 600 })
// 加载页面
win.loadFile('./index.html')
// 打开开发者工具
win.webContents.openDevTools()
// 添加 window 关闭触发事件
win.on('closed', () => {
win = null// 取消引用
})
}
// 初始化后 调用函数
app.on('ready', createWindow)
// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
// 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
// 否则绝大部分应用及其菜单栏会保持激活。
if(process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
// 在 macOS 上,当单击 dock 图标并且没有其他窗口打开时,
// 通常在应用程序中重新创建一个窗口。
if(win === null) {
createWindow()
}
})
3)接着再修改 package.json 中的 main 字段对应的路径, 并添加 start 命令:
{
...
"main": "main.js",
"scripts": "electron ."
}
4)执行 npm start,就会弹出如下运行界面:
以上就是我简单写的一个页面,大家也可以写一写自己感兴趣的东西。
真如上面演示的这样,一个简单的 Electron 跨平台桌面应用就开发好了,真的 so easy!
5、进程详解
5.1 基本认知
Electron 架构和 Chromium 架构类似,也是具有 1 个主进程和多个渲染进程(如下图所示)。
5.2 主进程的主要特点
Electron 运行 package.json 的 main 脚本的进程被称为主进程 (主进程只有一个)。涉及到具体代码的讲解,将在下一节中展开,本节就不作过多阐述了。
Electron 主进程的具体职责:
1)主进程连接着操作系统和渲染进程,可以把她看做页面和计算机沟通的桥梁;
2)进程间通信、窗口管理
3)全局通用服务;
4)一些只能或适合在主进程做的事情(例如浏览器下载、全局快捷键处理、托盘、session);
5)维护一些必要的全局状态。
5.3 渲染进程的主要特点
渲染进程就是我们所熟悉前端环境了,只是载体改变了,从浏览器变成了 window.
注意:出于安全考虑,渲染进程是不能直接访问本地资源的,因此都需要在主进程完成。
Electron 渲染进程主要特点:
1)Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到;
2)每个 web 页面运行在它自己的渲染进程中(每个渲染进程都是相互独立的,并且只关心他们自己的网页);
3)使用 BrowserWindow 类开启一个渲染进程并将这个实例运行在该进程中,当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止;
4)渲染进程中不能调用原生资源,但是渲染进程中同样包含 Node.js 环境,所以可以引入 Node.js。
5.4 主进程与渲染进程的关系
主进程与渲染进程的关系主要是这样:
1)主进程使用 BrowserWindow 实例创建网页;
2)每个 BrowserWindow 实例都在自己的渲染进程里运行着一个网页(当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止);
3)主进程管理所有页面和与之对应的渲染进程;
4)由于在网页里管理原生 GUI 资源是非常危险而且容易造成资源泄露,所以在网页面调用 GUI 相关的 APIs 是不被允许的(如果你想在网页里使用 GUI 操作,其对应的渲染进程必须与主进程进行通讯,请求主进程进行相关的 GUI 操作)。
具体关系如下图所示:
把它们想象成这样:
即 Chrome(或其他浏览器)的每个标签页(tab)及其页面,就好比 Electron 中的一个单独渲染进程。即使关闭所有标签页,Chrome 依然存在。这好比 Electron 的主进程,能打开新的窗口或关闭这个应用。就像下图这样。
6、从代码角度理解进程
6.1 主进程和渲染进程
先来看看 electron 项目基本目录结构:
app
└─public
└─index.html---------------入口文件
├─main.js----------------------程序启动入口,主进程
├─ipc--------------------------进程间模块
├─appNetwork-------------------应用通信模块
└─src--------------------------窗口管理,渲染进程
├─components---------------通用组件模块
├─store--------------------数据共享模块
├─statics------------------静态资源模块
└─pages----------------------窗口业务模块
├─窗口 A----------------窗口
└─窗口 B----------------窗口
如上所示:package.json 中的 main 字段对应的文件的进程是主进程。Electron 集成了 Chromium 来展示窗口界面,窗口中所看到的内容使用的都是 HTML 渲染出来的。
Chromium 本身是多进程渲染页面的架构(在默认情况下,Chromium 的默认策略是对每一个 tab 新开一个进程,以确保每个页面是独立且互不影响的,避免一个页面的崩溃导致全部页面无法使用),所以 Electron 在展示窗口时,也会使用到 Chromium 的多进程架构。而这种多进程渲染架构在 Electron 中,也就被是渲染进程(render process)啦。
6.2 进程间通信在 Electron 中,GUI 相关的模块(如 dialog,menu 等)仅在主进程可用,在渲染进程中不可用。
为了在渲染进程中使用它们,需要使用 ipc 模块向主进程发送消息,下面是几种进程间通讯的方法。
1)ipcMain & ipcRenderer:
从主进程到渲染进程的异步通信,也可以将消息从主进程发送到渲染进程(参考文档)。
发送消息时,事件名称为 channel。回复同步消息时,需要设置 event.returnValue。
将异步消息发送回发送方,可以使用 event.reply(...),这个辅助方法将自动处理来自渲染进程的消息,然而 event.sender.send(...) 这个方法则始终将消息发送给主进程。
下面是在渲染和主进程之间发送和处理消息的一个例子:
// 在主进程中
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg); // 输出 'ping'
event.reply('asynchronous-reply', 'pong');
})
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // 输出 ‘ping’
event.returnValue = 'pong'
})
// 在渲染进程(网页)中
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // 输出 'pong'
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg); // 输出 'pong'
})
ipcRenderer.send('asynchronous-message', 'ping')
2)remote 模块:
remote 为渲染进程和主进程通信提供了一种简单的方法。你可以调用 main 进程对象的方法,而不必显式发送进程间消息。
例如:从渲染进程创建浏览器窗口
const { BrowserWindow } = require('electron').remote
let win = newBrowserWindow({ width: 800, height: 600 })
win.loadUrl('https://www.mogu.com')
注意: 反过来,如果需要从主进程访问渲染进程,可以使用 webContents.executeJavascript。
3)webContents:
即通过 channel 向渲染进程发送异步消息,可以发送任意参数。在内部,参数会被序列化为 JSON,因此参数对象伤的函数和原型链不会被发送。
除了以上这些方法,也可以使用 localStorage、sessionStorage 等。
7、打包发布
开发完成后,还需要将应用打包成可执行文件,这一环节的坑还是学习 electron 到现在踩的最多的。
目前主流的打包工具有 electron-packager和 electron-builder
7.1 electron-packager
1)安装依赖:
npm i electron-packager --save-dev
2)打包:
electron-packager --platform= --arch= [optional flags...]
也可以直接运行 npm run electron-packager 打包。
7.2 electron-builder
官方解释:
A complete solution to package and build a ready for distribution Electron, Proton Native or Muon app for macOS, Windows and Linux with “auto update” support out of the box.
简单的说:electron-builder 有比 electron-packager 更丰富的功能,支持更多的平台,同时也支持了自动更新。除了这几点外,electron-builder 打出的包更为轻量,并且可以打包出不暴露源码的 setup 安装程序。
另外使用下来感觉比 electron-packager 的坑要少一点。
1)安装依赖:
npm i electron-builder --save-dev
2)打包(在项目的 package.json 文件中定义 build 字段):
{
"build": {
"appId": "com.xxx.app",
"extends": null,
"files": [
"build/**/*"
],
"mac": {
"icon": "icons/icon.icns"
},
"win": {
"target": "nsis",
"icon": "icons/icon.png"
}
}
}
这是最基础的配置,当然打包过程中可能会碰到其他的问题需要修改配置。通常 files 配置只写一个 build 文件夹是不够的,要根据项目结构和打包情况添加其他路径。
添加 scripts 命令
{
"scripts": {
"pack": "electron-builder"
}
}
运行 npm run pack 打包。
打包完成后在 dist 目录下有可执行文件,打开后如果没有报错,则说明打包成功。
8、踩坑总结
我所遇到的大部分都是打包遇到的坑,以下列举几个典型的坑。
8.1 使用 electron-packager 打包报错
Generated checksum for"electron-v6.0.2-darwin-x64.zip"did not match expected checksum。
解决方法:node 版本升级到 8.x 以上就好。
8.2 打开打包生成的可执行文件报错
出现这种问题可能有以下几个原因。
1)项目中可能直接访问了本地路径, 浏览器为了安全考虑不允许访问。
2)package.json 中的 build 配置问题,假如 main.js 在一个很深的路径中,需要在下面单独添加 main.js 的路径:
2)package.json 中的 build 配置问题,假如 main.js 在一个很深的路径中,需要在下面单独添加 main.js 的路径:
"build": {
...
+ "public/main.js"
...
}
3)webpack 配置中的路径直接使用了 __dirname, 可以使用 remote 模块的 getAppPath 方法取得路径:
const remote = require('remote')
const app = remote.require('app')
console.log(app.getAppPath());
参考资料:https://github.com/electron/electron/issues/5107
8.3 dependencies & devDependencies
在 Electron 打包时,一定要分清哪些是生产环境依赖,哪些是开发环境依赖,避免出现此类错误:
8.4 关于打包慢的问题(npm & cnpm)
cnpm 装的各种 node_modules,这种方式下所有的包都是扁平化的安装,一下子 node_modules 展开就有非常多的文件,导致打包的过程非常慢。
但是如果该用 npm 来安装 node_modules 的话,所有的包都是树状结构,层级变深。但是打包速度会快很多(具体资料参见:electron打包过了2小时都没好?)。
9、Electron 的优缺点
文章的最后,基于实践体会,总结一下 Electron 的优缺点。
Electron 优点很明显:
1)上手较简单:HTML、CSS、JS、Node 、npm 包、UI 框架 ,方便高效,能很轻松的实现很好看的 UI;
2)可多端运行:可以快速构建“跨平台”(Windows、MacOs、Linux)的桌面级应用;
3)开发时间短:相对其他跨平台方案(如:QT、GTK+ 等),更稳定、bug 少,毕竟只要浏览器外壳跑起来了就可以了(当然坑是少不了的);
4)兼容性问题:再也不用兼容多浏览器(只针对谷歌,但要兼容 mac、Linux)。
Electron 缺点也同样显而易见:
1)安装包体积大:安装包体积略大(打包了 Chromium),至少包含了一个浏览器的体积 ,每装一个 app 就相当于装一个 chrome;
2)运行性能稍差:性能不如原生应用,Mac 系统下丝滑一些,Window 系统就有点丢帧;
3)内存占用较大:卡、启动慢,新开一个进程,起步价就是一个 NodeJS 的内存开销;
4)网页加载稍慢:loadURL 加载远程页面白屏时间长(优化可采用 VSCode 骨架屏)。
10、参考资料
[1] Electron官方开发者手册
[2] Electron初体验(快速开始、跨进程通信、打包、踩坑等)
学习交流:
- 移动端 IM 开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源 IM 框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步发布于:http://www.52im.net/thread-4039-1-1.html)
评论