electron 构建跨平台桌面应用
昨日(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 桌面程序。
1. 克隆官方的例子
$ git clone https://github.com/electron/electron-quick-start
2. 进入项目目录
$ cd electron-quick-start
3. 安装项目依赖后执行该程序
$ npm install && npm start
运行效果如下:
案例分析
察看目录结构如下:
.
├── .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:
// 引入 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:
// 引入 ipcRenderer 模块
const ipcRenderer = require('electron').ipcRenderer;
// 向 'blabla' 通道发送消息
ipcRenderer.send('blabla', 'hello server!');
// 监听 ‘blibli’ 通道, 收到消息后输出
ipcRenderer.on('blibli', function(event,arg) {
console.log(arg);
});
输出效果正如你所期望那样:
main.js:
hello server!
index.html:
hello client!
remote
上面提到了大部分模块只能在主进程中调用,为了突破这种限制,Electron 官方还提供了 remote 模块以简化进程间的通讯。
通过 remote 模块,渲染进程可以方便地引用主进程中的模块和全局变量等。
main.js:
global.person = {
name: 'boxizen',
sex : 'male',
age : 24
}
index.html:
// 引用 remote 模块
const remote = require('electron').remote;
// 输出 main.js 中定义的 person 全局对象的 age 属性值
console.log(remote.getGlobal('person').age);
输出效果(index.html):
24
webview
webview 是个比较有趣的标签,可以将线上的页面嵌入进 Electron app 中,与 iframe 不同的是,webview 和应用运行的是不同的进程,不拥有渲染进程的权限。
下面将演示如何将微信网页版嵌入进 Electron 应用里,只需要简单的两步:
index.html:
<webview autosize="on" src="https://wx.qq.com/" style="display:inline-flex; width:1000px; height:764px"></webview>
main.js:
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:
// 这里使用 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:
// 引入 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:
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:
npm install electron-packager --save-dev
然后在 package.json 中编写构建命令,下面生成了分别在 Windows 和 Mac 下的两条构建命令:
"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!
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
- Django rest framework(5)----解析器
- Django rest framework(6)----序列化
- 扩展中国剩余定理详解
- Django rest framework(7)----分页
- 洛谷P3807 【模板】卢卡斯定理exgcd
- 洛谷P1586 四方定理
- 【SQLServer】记一次数据迁移-标识重复的简单处理
- Django用户登录与注册系统
- 洛谷P1450 [HAOI2008]硬币购物
- 一个完整的Django入门指南(二)
- 一个完整的Django入门指南(三)
- 1.Django自学课堂 模板的使用
- 23.Django基础
- SpringBoot开发案例之整合Quartz注入Service
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- [算法] 数组排序 - 冒泡排序法与直接选择排序法
- TS 设计模式01 - 工厂模式
- Spring与Mybatis的整合
- Python中的计数 - Counter类
- vue 记账本
- c/c++补完计划(三): 素数统计
- Python正则表达式(上)
- 附001.Nginx location语法规则
- 016.Nginx HTTPS
- 架构师写的BUG,非比寻常
- Python爬虫获取豆瓣TOP250电影详情
- docker容器常用命令
- MapReduce之自定义分区器Partitioner
- MySQL ORDER BY,GROUPBY 与各种JOIN
- (三) Mybatis动态SQL语句 - Titan的Mybatis系列学习笔记