把一个已经跑在浏览器里的后台系统做成能装在电脑上的客户端,同时支持 mac 和 windows,还得有托盘图标、右键菜单、断网提醒这些「像个真软件」的细节,这活儿交给只会写前端的人能不能干?我为了搞清楚这个问题,从头到尾用 Electron 做了一个舆情监控的桌面端出来,源码在文末。结论是能干,这些原生能力 Electron 基本都包好了,剩下的还是写 HTML 和 JavaScript。
这篇是我边做边记的完整笔记,从环境搭建一路到多平台打包,中间卡住过的地方也一并留着。读完你能拿到一条从 electron . 跑起第一个窗口,到把应用打成 .dmg 和 .exe 的完整路径。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- Electron 是什么,它凭什么能把网页变成桌面软件
- 四种搭项目的方式,以及主进程和渲染进程到底怎么分工
- 自定义顶部菜单和右键菜单
- 主进程与渲染进程、渲染进程之间的四种通信姿势
- shell、webview、dialog 这些和系统打交道的模块
- 系统托盘、图标闪烁、消息通知、监听网络变化
- 全局快捷键、剪贴板与 nativeImage
- 结合 electron-vue 做一个完整的舆情监控系统并打包上线
- 2019 年的写法放到今天该怎么改(安全模型变了,这块单独讲)
先说一句放在最前面的提醒:这篇笔记写于 2019 年初,那会儿 Electron 还允许渲染进程直接 require('electron')、直接读文件,remote 模块也还在核心里。现在的官方安全基线已经变成 contextIsolation: true + nodeIntegration: false,能力统一由 preload 脚本通过 contextBridge 往外暴露,remote 也早就从核心移出去了。原文的写法我一个字没删,因为它能帮你理解这套模型是怎么演进过来的;但每个涉及安全边界的地方,我都另起了一小段写现在该怎么做。要直接抄进新项目的话,请照着新写法来。
# 一、前言
NW.js和Electron都可以用前端的知识来开发桌面应用。NW.js和Electron起初是同一 个作者开发。后来种种原因分为两个产品。一个命名为NW.js(英特尔公司提供技术支持)、 另一命名为Electron(Github 公司提供技术支持)。NW.js和Electron可以用Nodejs中几乎所有的模块。NW.js和Electron不仅可以把html写的web页面打包成跨平台可以安装到电脑上面的软件,也可以通过javascript访问操作 系统原生的UI和Api(控制窗口、添加菜单项目、托盘应用菜单、读写文件、访问剪贴板)。
github的atom编辑器、微软的vscode编辑器,包括阿里内部的一些 软件也是用electron开发的
你每天在用的 VS Code 就是 Electron 写的。这一点对我当时的说服力比任何文档都强,一个前端能写出来的东西,可以是编辑器这种量级的产品,那我做个后台客户端总归没问题。
顺着上面聊,NW.js 和 Electron 的分家其实是个挺有意思的历史。它们的入口哲学不一样:NW.js 是以页面为入口,直接指一个 html 文件就能跑;Electron 是以脚本为入口,先起一个 Node 进程(主进程),再由这个进程去开窗口装页面。这个差别看着小,但它决定了后面整篇笔记的所有内容,因为「主进程和渲染进程分工」这件事就是从这儿来的。
下面这几个问题是我当时给自己列的自查清单,搞清楚了再动手。
1. Electron 是由谁开发的?
Electron是由Github开发
2. Electron 是什么?
Electron是一个用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库
3. Electron 把 HTML,CSS 和 JavaScript 组合的程序构建为跨平台桌面应用程序的原理 是什么?
原理为
Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用来实现这一目的。
4. Electron 何时出现的,为什么会出现?
Electron于2013年作为构建Atom的框架而被开发出来。这两个项目在2014春季开源。 (Atom:为 Github 上可编程的文本编辑器)
一些历史:
2013年4月Atom Shell项目启动 。2014年5月Atom Shell被开源 。2015年4月Atom Shell被重命名为Electron2016年5月Electron发布了v1.0.0版本
5. Electron 当前流行程度?
目前
Electron已成为开源开发者、初创企业和老牌公司常用的开发工具。
6. Electron 当前由那些人在维护支持?
Electron当前由Github上的一支团队和一群活跃的贡献者维护。有些贡献者是独立开发者,有些则在用Electron构建应用的大型公司里工作。
7. Electron 新版本多久发布一次?
Electron的版本发布相当频繁。每当Chromium、Node.js有重要的bug修复,新API或是版本更新时Electron会发布新版本。
- 一般
Chromium发行新的稳定版后的一到两周之内,Electron中Chromium的版本会对其进行更新,具体时间根据升级所需的工作量而定。 一般Node.js发行新的稳定版一个月后,Electron中Node.js的版本会对其进行更新,具 体时间根据升级所需的工作量而定。
8. Electron 的核心理念是什么?
Electron的核心理念是:保持Electron的体积小和可持续性开发。 如:为了保持Electron的小巧 (文件体积) 和可持续性开发 (以防依赖库和API的泛滥) ,Electron限制了所使用的核心项目的数量。 比如Electron只用了Chromium的渲染库而不是其全部组件。这使得升级Chromium更加容易,但也意味着Electron缺少了Google Chrome里的一些浏览器相关的特性。 添加到Electron的新功能应该主要是原生API。 如果可以的话,一个功能应该尽可能的成 为一个Node.js模块。
9. Electron 当前的最新版本为多少?
Electron当前的最新版本为4.0.1(当前时间为2019年1月6号)
这个 4.0.1 是我写这篇笔记那天的版本,现在早就翻了好几倍了,具体多少以官方 releases 页面为准。我不写死数字,因为 Electron 的发版节奏是跟着 Chromium 走的,你今天看到的数字过两周就不对了。真正需要记住的是它的支持策略:只有最近几个 major 版本会拿到安全补丁,老版本用着用着就等于把一个不再打补丁的 Chromium 装进了用户电脑。所以选版本这件事,别挑「我熟悉的那个」,挑还在支持窗口里的那个。
回到我们要解决的问题。搞明白 Electron 是「Chromium 渲染 + Node 运行时」这一个合体,后面所有 API 的位置就都好理解了:跟界面有关的走 Chromium 那一半,跟系统有关的走 Node 那一半,两半之间靠进程通信连起来。
# 二、环境搭建
搭环境这块有四条路,从「一条命令跑起来」到「一个文件一个文件手搓」都有。我建议第一次学的时候走手搓那条,因为只有自己写一遍 main.js,你才知道那个窗口是谁创建的、什么时候创建的。等你熟了,做真实项目再用脚手架。
1. 安装 electron
npm install -g electron
全局装是为了能在任意目录直接敲 electron .,方便试手。真实项目里我更推荐装成项目的 devDependencies,理由很实在:Electron 的版本号决定了你的 Chromium 和 Node 版本,全局装的话每个项目共用同一个运行时,一旦你同时维护两个不同版本的项目就会互相打架,而且报出来的错和版本八竿子打不着,很难往这个方向想。
2. 克隆一个仓库、快速启动一个项目
# 克隆示例项目的仓库
git clone https://github.com/electron/electron-quick-start
# 进入这个仓库
cd electron-quick-start
# 安装依赖并运行
npm install && npm start
electron-quick-start 是官方维护的最小样板,main.js 加 index.html 加 package.json 三个文件,看完不超过五分钟。想快速确认自己机器上的环境没问题,跑它最快。
3. 手动搭建一个 electron 项目
- 新建一个项目目录 例如:
electrondemo01 - 在
electrondemo01目录下面新建三个文件:index.html、main.js、package.json index.html里面用css进行布局(以前怎么写现在还是怎么写)- 在
main.js中写如下代码
这段代码是整个 Electron 应用的起点,它就干三件事:等 Electron 初始化完成、创建一个 800x600 的窗口把 index.html 装进去、在窗口关掉时把引用置空。package.json 里的 main 字段要指向这个文件,Electron 启动时先跑的就是它。
var electron = require('electron'); // electron 对象的引用
const app = electron.app; // 控制应用生命周期
const BrowserWindow = electron.BrowserWindow; // BrowserWindow 类的引用
let mainWindow = null;
// 监听应用准备完成的事件
app.on('ready', function () {
// 创建窗口
mainWindow = new BrowserWindow({ width: 800, height: 600 });
mainWindow.loadFile('index.html');
mainWindow.on('closed', function () {
mainWindow = null;
});
});
// 监听所有窗口关闭的事件
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
有两个点特别容易被跳过去,但它们都是有原因的。
let mainWindow = null 为什么要放在函数外面?因为 BrowserWindow 对象一旦被 JavaScript 垃圾回收,对应的原生窗口就跟着关了。如果你写成 app.on('ready', function () { let win = new BrowserWindow(...) }),窗口有时候会莫名其妙自己消失。放到模块作用域顶层,就是为了留一个全局引用把它拴住。