昨日(2016.09.13)本文发表后,获得了一定的阅读和转发量,但经部分网友反馈和仔细审核后发现,在与 NW.js 对比的环节,言辞欠妥,且数据的真实性有待考究,特此将争议部分删除,同时借此诚挚地向 NW.js 的作者以及各位读者反馈表示感谢,期待更多深入的交流和分享,修订后的版本如下:
Stack Overflow 联合创始人 Jeff Wood 曾说过,任何一个能用 JavaScript 编写的应用系统,最终都必将使用 JavaScript 实现。
简介
Electron 是一款可以通过 Web前端技术 构建跨平台桌面应用的框架。其原名为 Atom Shell, 是 Github 社区原本为 Atom 编辑器设计的一个跨平台应用外壳,它将 Chromium 和 Node.js 的事件循环整合在一起,并提供了一些与原生系统交互的 API。
简单地说,通过 Electron,我们可以使用自己所熟悉的前端技术轻松构建出一款能运行在Windows, Linux 和 Mac 上的桌面级应用程序。
现阶段已有许多优秀的桌面应用都是基于 Electron 开发,其中如 Atom 编辑器,VS Code 和 Postman 等等都是我们所熟知的,下面列出这当中的部分应用,是不是看到了许多熟悉的图标呢?
Hello World
案例运行
使用下面三步即可构建最简单的 Hello World 桌面程序。
代码语言:javascript复制1. 克隆官方的例子
$ git clone https://github.com/electron/electron-quick-start
2. 进入项目目录
$ cd electron-quick-start
3. 安装项目依赖后执行该程序
$ npm install && npm start
运行效果如下:
案例分析
察看目录结构如下:
代码语言:javascript复制.
├── .gitignore
├── index.html
├── LICENSE.md
├── main.js
├── package.json
├── README.md
└── renderer.js
主要分为三个部分:
- package.json: 定义了项目的依赖以及程序的主入口文件 main.js。
- main.js: 负责创建应用窗口,并赋予其与当前操作系统的原生GUI交互的功能。
- index.html: 定义了页面的渲染内容,即 “Hello World” 字符串。
Electron 程序启动时,会产生两条进程,分别是主进程和渲染进程,main.js 脚本执行的环境就是主进程,负责管理和维护着渲染进程的生命周期,拥有绝大部分 node模块 的调用能力;而在 main.js 中创建的每一个窗体则对应着一个渲染进程,它们之间相互独立,且都具备部分 Node模块 的功能。
主进程与渲染进程的关系如下图所示,它们之间通过 IPC 模块进行消息交互,关于 IPC 模块的使用,下面会提到。
功能模块
这个部分将介绍 Electron 里面常用到的几个功能模块。
IPC
上面提到,Electron 中包含了主进程和渲染进程,事实上主进程就是一个后台进程,掌控着渲染进程的创建与销毁动作,且官方提供的绝大部分模块也只能在该进程中调用。
主进程与渲染进程之间的通信通过 IPC(进程间通信)模块完成,IPC模块可划分为 ipcMain 和 ipcRenderer 两个部分,其中 ipcMain 对应 主进程中的 IPC模块,而 ipcRenderer 则是在渲染进程中使用,下面直接看个例子:
main.js:
代码语言:javascript复制// 引入 ipcMain 模块
const ipcMain = require('electron').ipcMain;
// 监听 ‘blabla’ 通道,收到消息后输出,并向 'blibli' 通道发送消息
ipcMain.on('blabla', function(event, arg) {
console.log(arg);
event.sender.send('blibli', 'hello client!');
})
index.html:
代码语言:javascript复制// 引入 ipcRenderer 模块
const ipcRenderer = require('electron').ipcRenderer;
// 向 'blabla' 通道发送消息
ipcRenderer.send('blabla', 'hello server!');
// 监听 ‘blibli’ 通道, 收到消息后输出
ipcRenderer.on('blibli', function(event,arg) {
console.log(arg);
});
输出效果正如你所期望那样:
main.js:
代码语言:javascript复制hello server!
index.html:
代码语言:javascript复制hello client!
remote
上面提到了大部分模块只能在主进程中调用,为了突破这种限制,Electron 官方还提供了 remote 模块以简化进程间的通讯。
通过 remote 模块,渲染进程可以方便地引用主进程中的模块和全局变量等。
main.js:
代码语言:javascript复制global.person = {
name: 'boxizen',
sex : 'male',
age : 24
}
index.html:
代码语言:javascript复制// 引用 remote 模块
const remote = require('electron').remote;
// 输出 main.js 中定义的 person 全局对象的 age 属性值
console.log(remote.getGlobal('person').age);
输出效果(index.html):
代码语言:javascript复制24
webview
webview 是个比较有趣的标签,可以将线上的页面嵌入进 Electron app 中,与 iframe 不同的是,webview 和应用运行的是不同的进程,不拥有渲染进程的权限。
下面将演示如何将微信网页版嵌入进 Electron 应用里,只需要简单的两步:
index.html:
代码语言:javascript复制<webview autosize="on" src="https://wx.qq.com/" style="display:inline-flex; width:1000px; height:764px"></webview>
main.js:
代码语言:javascript复制mainWindow = new BrowserWindow({width: 1000, height: 764, resizable: false})
效果图:
这样一个PC版的微信就大功告成了,实际上就是利用 webview 标签加载微信网页版的在线地址,再在main.js中调整窗体大小以适配网页版的微信,是不是很简单呢。
webview 对象中包含 insertCSS() 和 executeJavaScript() 两个方法,表示可以插入样式代码和执行 js 脚本,这样我们就可以对加载页面中的样式及交互逻辑进行修改。默认的 webview 没有 node 功能,而如果设置了 nodeintegration 属性,它将整合node,拥有可以使用系统底层的资源。
此外 webview 中的 preload 属性允许在页面的脚本执行前预加载一个指定的脚本,下面我们利用该属性和 executeJavaScript() 方法实现 electron 版微信的未读消息角标展示。
index.html:
代码语言:javascript复制// 这里使用 preload 属性预加载了 badge.js 脚本
<webview id="foo" autosize="on" preload="badge.js" src="https://wx.qq.com/" style="display:inline-flex; width:1000px; height:764px"></webview>
var webview = document.getElementById("foo");
webview.addEventListener('dom-ready', function () {
webview.executeJavaScript('badge.get()');
});
badge.js:
代码语言:javascript复制// 引入 IPC 模块
const ipcRenderer = require('electron').ipcRenderer;
badge = {
get: function () {
// 监听微信左侧面板节点变化
$(".panel").bind('DOMSubtreeModified', function() {
var count = 0;
// 累加所有未读消息
$(".icon.web_wechat_reddot_middle").each(function () {
count = parseInt(this.textContent);
});
// 通过 IPC 发送给主进程
if (count > 0) {
ipcRenderer.send('badge-changed', count.toString());
} else {
ipcRenderer.send('badge-changed', '');
}
})
}
}
main.js:
代码语言:javascript复制const electron = require('electron');
const ipcMain = electron.ipcMain;
const app = electron.app
exports.init = function() {
// 监听角标通道消息
ipcMain.on('badge-changed', function (event, num) {
app.dock.setBadge(num);
});
}
效果图如下,可以发现 Electron 的 dock 角标显示的未读消息数(11)跟微信中面板中未读消息数量一致:
其他
当然 Electron 中还有许多实用的模块,如作为桌面应用必不可少的 Menu 和 Tray 模块、拥有调用当前操作系统功能的 Shell 模块、NW.js 中不具备的自动更新功能 - autoUpdater 模块、自动提交奔溃报告的 crashReporter 模块和全局快捷键模块 global-shortcut 等等,此处不做过多介绍。
打包构建
Electron 打包的方式有很多种,常见的有 electron-builder、electron-packager 和 asar几种,在这里我使用的是 electron-packager 作为应用的打包工具。
首先还是得先安装 electron-packager:
代码语言:javascript复制npm install electron-packager --save-dev
然后在 package.json 中编写构建命令,下面生成了分别在 Windows 和 Mac 下的两条构建命令:
代码语言:javascript复制"scripts": {
"start": "electron .",
"build-win": "electron-packager . doubanFM --platform=win32 --arch=x64 --version=1.2.6 --icon=./fm.ico --overwrite",
"build-mac": "electron-packager . doubanFM --platform=darwin --arch=x64 --version=1.2.6 --icon=./fm.icns --overwrite"
},
执行构建命令, done!
代码语言:javascript复制npm run build-mac
最后贴一张最近利用 Electron 构建的桌面版豆瓣FM的截图:
参考资料:
- Electron 官方文档 http://electron.atom.io/
- 从 node-webkit 到 Electron 1.0 http://cheng.guru/blog/2016/05/13/from-node-webkit-to-electron-1-0.html
- Atom 编辑器嵌入 Node.js 实践 https://speakerdeck.com/zcbenz/practice-on-embedding-node-dot-js-into-atom-editor
- atom-shell vs node-webkit http://blog.iwege.com/posts/atom-shell-vs-node-webkit.html
- 维护一个大型项目是怎样的体验? https://www.zhihu.com/question/36292298/answer/102418523