查看原文
其他

微软不会放弃Electron:Electron 快速入门及IPC 通信

Editor's Note

最近,有媒体推文说“微软要放弃Electron了”,很多不明真相的群众被带偏了节奏:我刚学了Electron就要过时了吗?求真相!那么我们还要不要学Electron呢?


The following article is from 腾讯IMWeb前端团队 Author hanzhen

最近,有媒体推文说“微软要放弃Electron了”,很多不明真相的群众被带偏了节奏:我刚学了Electron就要过时了吗?求真相!那么我们还要不要学Electron呢?

实际上:只有微软旗下的Teams产品打算把Electron框架换成WebView2而已。而Electron的应用可不仅仅是在Teams上,微软的VSCode、GitHubDesktop,其他包括Facebook、MongoDB、twitch、Slack、迅雷、字节跳动、阿里、拼多多、京东大企业都在用这个框架。把微软Teams换成微软,妥妥的标题党,博眼球无误了!


solar 是基于互动课件编辑器 Cocos ICE 进行二次定制和个性化开发的课件制作系统,其底层是 Cocos Creator。而 Cocos Creator 是基于Electron进行开发的,所以学习了一些关于 Electron IPC 通信的相关知识,在这里做一个总结。

文章的开始,先让我们来了解下 Electron 是什么。

1. 什么是 Electron?

Electron 官网只有一句简单的话: 使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。简单点讲,就是有了 Electron,我们就可以用前端技术来写 web 页面,它可以转化为一个桌面应用。
除此之前,Electron 还有其他的一些特性:

  • 基于 Chromium 和 Node.js

  • 兼容 Mac、Windows 和 Linux,可以构建出三个平台的应用程序

2. Electron 能做啥?

Electron 基于 Chromium 和 Node.js,类似一个小型的 Chrome 的浏览器,Electron 可以将你写的 web 页面(html 文件)本地化,然后打包成一个桌面应用程序。它同时还是跨平台的,提供了许多功能与原生系统进行交互。

由于是基于 Chromium 的,所以写 Electron,从此与前端兼容性无缘(真香)。Node.js版本也是固定的,无需考虑版本兼容问题(除非升级大版本)。

所以作为前端开发人员来说,想开发一款桌面端应该,Electron 是再适合不过了。

Electron 官网还举了一些使用 Electron 进行开发的应用,大名鼎鼎的 VSCode 就是基于Electron

3. Electron 快速上手

学啥不得先来个 hello world 呢?

3.1. 初始化工程

创建 Electron 工程方式与前端项目别无二致,创建一个目录,然后用 npm 初始化:

mkdir hello-electron && cd hello-electron
npm init -y

生成之后的 package.json 应该长这样。

{
"name": "hello-electron",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

3.2. 安装依赖

npm install --save-dev electron

安装过程中,electron 模块会去 Github 下载 预编译二进制文件,然而下载速度大家都懂的,可能会出现下载失败的情况。这里可以使用 taobao 的镜像源来下载。

npm config set electron_mirror http://npm.taobao.org/mirrors/electron/
npm config set electron_custom_dir "8.1.1"

为了更方便的启动我们的程序,可以新增一条命令。

{
"scripts": {
"start": "electron ."
}
}

接下来,就让我们愉快地编码吧。

3.3. 创建 HTML

在 Electron 中,每个窗口都可以加载本地或者远程 URL,这里我们先创建一个本地的 HTML 文件。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using Electron <span id="electron-version"></span>
</body>
</html>

这里你可能会注意到, span 标签里面是空文本,后面我们会动态插入 Electron 的版本。

3.4. 创建入口文件

类似于 Node.js 启动服务,Electron 启动也需要一个入口文件,这里我们创建 index.js 文件。在这个入口文件里,需要去加载上面创建的 HTML 文件,那么如何加载呢? Electron 提供了两个模块:

  • app 模块,它控制应用程序的事件生命周期。

  • BrowserWindow 模块,它创建和管理应用程序 窗口。

入口文件是 Node.js 环境,所以可以通过 CommonJS 模块规范来导入 Electron 的模块。同时添加一个 createWindow() 方法来将 index.html 加载进一个新的 BrowserWindow 实例。

// index.js
const { app, BrowserWindow } = require('electron');

function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600
})

win.loadFile('index.html')
}

那么在什么时候调用 createWindow 方法来打开窗口呢?在 Electron 中,只有在 app 模块的 ready 事件被激发后才能创建浏览器窗口。可以通过使用 app.whenReady() API 来监听此事件。

// index.js
app.whenReady().then(() => {
createWindow()
})

这样一来就可以通过以下命令打开 Electron 应用程序了!

# 这里会自动去找package.json的main字段对应的文件运行
# 当然 你也可以将命令放进 script 里面
npx electron .

运行完打开的应用程序如下图所示。

3.5. 管理窗口的声明周期

虽然现在可以打开一个浏览器窗口,但还需要一些额外的模板代码使其看起来更像是各平台原生的。应用程序窗口在每个 OS 下有不同的行为,Electron 将在 app 中实现这些约定的责任交给开发者们。

可以使用 process.platform 属性来为不同的操作系统做处理。

3.5.1. 关闭所有窗口时退出应用(Windows & Linux)

在 Windows 和 Linux 上,关闭所有窗口通常会完全退出一个应用程序。 app 模块可以监听所有窗口关闭的事件 window-all-closed,在事件回调里可以调用 app.quit() 退出应用。

// index.js
app.on('window-all-closed', function () {
// darwin 为 macOS
if (process.platform !== 'darwin') app.quit()
})

3.5.2. 没有窗口打开则打开一个新窗口(macOS)

用过 macOS 的人应该都知道,一个应用没有窗口打开的时候,也是可以继续运行的,这时如果打开应用程序,就会打开新的窗口。 app 模块可以监听应用激活事件 activate,在事件回调里可以判断当前窗口数量来确定需不需要打开一个新的窗口。因为窗口无法在 ready 事件前创建,你应当在你的应用初始化后仅监听 activate 事件。通过在您现有的 whenReady() 回调中附上您的事件监听器来完成这个操作。

// index.js
app.whenReady().then(() => {
createWindow()

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

3.6. 预加载脚本

前面讲到我们会在 HTML 文件中插入 Electron 的版本号。然而,在 index.js 主进程中,是不能编辑 DOM 的,因为它无法访问到渲染进程 document 上下文,它们存在于完全不同的进程中。

这时候,预加载脚本就可以派上用场了。预加载脚本在渲染进程加载之前加载,并有权访问两个渲染进程全局 (例如 window 和 document) 和 Node.js 环境。

3.6.1. 创建预加载脚本

创建一个名为 preload.js 的新脚本如下:

window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector);
if (element) element.innerText = text;
}

replaceText('electron-version', process.versions.electron);
})

我们需要在初始化 BrowserWindow 实例的时候,传入该预加载脚本。

// 在文件头部引入 Node.js 中的 path 模块
const path = require('path')

// 修改现有的 createWindow() 函数
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

win.loadFile('index.html')
}
// ...

然后重新启动程序,就可以看到 Electron 的版本了。

4. Electron 的流程模型

前面讲到了主进程、渲染进程等概念性知识,初学者可能会对此比较迷惑,不过,进行 Electron,对这一块内容的掌握是至关重要的,后面的 IPC 进程通信,也与此有关。实际上,Electron 继承了来自 Chromium 的多进程架构,作为前端工程师,对于浏览器进程架构有所了解,也是非常有必要的。

4.1. 主进程

每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点,比如上面的 index.js。主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。主进程一般包括以下三大块:

  • 窗口管理:使用 BrowserWindow 模块创建和管理应用窗口。类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。

  • 应用生命周期:主进程可以使用 Electron 提供的 app 模块来控制应用程序的生命周期。

  • 原生 API:Electron 有着多种控制原生桌面功能的模块,例如菜单、对话框以及托盘图标。

4.2. 渲染进程

每个打开的 BrowserWindow 都会生成一个单独的渲染进程。渲染进程负责渲染网页实际的内容。因此,渲染进程中运行的代码,几乎跟我们编写的 Web 代码别无二致。除此之外,渲染进程也无法直接访问 require 或其他 Node.js API。

注意:实际上渲染进程可以生成一个完整的 Node.js 环境以便于开发。在过去这是默认的,但如今此功能考虑到安全问题已经被禁用。

4.3. 预加载脚本

前面上手的时候已经讲过预加载脚本了,预加载(preload)脚本会在渲染进程网页内容开始加载之前执行,并且可以访问 Node.js API。由于预加载脚本与渲染器共享同一个全局 Window 接口,因此它通过在 window 全局中暴露任意您的网络内容可以随后使用的 API 来增强渲染器。

不过我们不能在预加载脚本中直接给 window 挂载变量,因为  是默认的。

window.myAPI = { desktop: true }
console.log(window.myAPI) // => undefined

Electron 这样做是为了将预加载脚本与渲染进程的主要运行环境隔离开来的,以避免泄漏任何具特权的 API 到网页内容代码中。(比如有些人会把 ipcRenderer.send 的方法暴露给 web 端,这将允许网站发送任意的 IPC 消息)

我们也可以关闭 contextIsolation不过不建议这么做

new BrowserWindow({
// ...
webPreferences: {
// ...
contextIsolation: false
}
})

最好使用 contextBridge 模块来安全地实现交互:

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
console.log(window.myAPI)// => { desktop: true }

5. Electron IPC 通信

Electron 有主进程和渲染进程,之间会有许多通信,这样就涉及到了进程间通信(IPC,InterProcess Communication)

在 Electron 中,主线程和渲染进程之间进行通信,只要是用到以下两个模块:

  •  :ipcMain 是一个 EventEmitter 的实例。当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。从渲染器进程发送的消息将被发送到该模块。

  •  :ipcRenderer 是一个 EventEmitter 的实例。你可以使用它提供的一些方法从渲染进程 (web 页面) 发送同步或异步的消息到主进程。也可以接收主进程回复的消息。

5.1. 渲染进程给主线程发送消息,主线程回复

5.1.1. 普通脚本监听

普通脚本引入 electron 的 ipcRenderer 模块,实现发送消息。

在 HTML 文件添加 renderer.js 脚本

const { ipcRenderer } = require('electron')

ipcRenderer.on('main-message-reply', (event, arg) => {
console.log(arg);
});
ipcRenderer.send('message-from-renderer', '渲染进程发送消息过来了');

在 index.js 入口文件引入 ipcMain 模块,并修改 BrowserWindow 的实例化参数,开启渲染进程的 Node.js 环境。

const { ipcMain } = require('electron')

function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
// 这里开启后 渲染进程就可以用 NodeJS 环境
// 可以引如 Electron 相关模块
nodeIntegration: true,
contextIsolation: false,
},
});
mainWindow.loadFile('index.html');
}

ipcMain.on('message-from-renderer', (event, arg) => {
console.log(arg);
// 接收到消息后可以回复
event.reply('main-message-reply', '主进程回复了')
})

启动应用,可以在命令行看到渲染进程发过来的消息了。

 然后渲染进程收到主线程的回复。

5.1.2. 预加载脚本暴露接口

在预加载脚本中,可以暴露一些全局的接口给到渲染进程,然后渲染进程调用,从而达到通信的目的。这种方式类似于微信 SDK,不用侵入到前端脚本去监听事件,较为安全。

// preload.js
const { contextBridge, ipcRenderer } = require('electron')

// 这里暴露一个全局myAPI变量
contextBridge.exposeInMainWorld('myAPI', {
getMessage(args) {
ipcRenderer.send('message-from-proload', args);
consoloe.log('前端调用了:', args)
}
})

renderer.js 直接调用暴露出来的接口。

// renderer.js
window
.myAPI.getMessage('postMessage');

index.js 主进程监听预加载脚本发送过来的信息。

ipcMain.on('message-from-proload', (event, arg) => {
console.log(arg);
// 接收到消息后可以回复
event.reply('main-message-reply', '主进程回复了')
})

5.2. 主线程给渲染进程发送消息

将 renderer.js 改为如下代码,监听主线程发送过来的消息。

const { ipcRenderer } = require("electron");

ipcRenderer.on("message", (event, arg) => {
console.log("主进程主动推消息了:", arg);
});

主线程往渲染进程发送消息,需要用到 。 webContents 是一个 EventEmitter,负责渲染和控制网页,是 BrowserWindow 对象的一个属性。修改一下 index.js 文件。

function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false,
},
});

const contents = mainWindow.webContents;
mainWindow.loadFile('index.html');
contents.openDevTools(); //打开调试工具

contents.on("did-finish-load", () => {
//页面加载完成触发的回调函数
contents.send("main-message-reply", "我看到你加载完了,给你发个信息");
});
}

运行应用,就可以在渲染进程中打开看到消息了。

以上的通信方式均为异步,不过 Electron 也提供了同步的通信方式,但是同步的方式会阻塞代码的执行,最好都使用异步通信。同步用法在这里不多作介绍。

ipcMain 和 ipcRenderer 模块还有一些其他的通信 API,不过大抵都是类似的通信方式,需要了解的同学可以自行去查阅文档。

6. 推荐学习资料

《Electron实战》以实战为导向,讲解了如何用Electron结合现代前端技术来开发桌面应用。不仅全面介绍了Electron入门需要掌握的功能和原理,而且还针对Electron开发中的重点和难点进行了重点讲解,旨在帮助读者实现快速进阶。本书遵循渐进式的原则逐步传递知识给读者,书中以Electron知识为主线并对现代前端知识进行了有序的整合,对易发问题从深层原理的角度进行讲解,对普适需求以实践的方式进行讲解,同时还介绍了Electron生态内的大量优秀组件和项目。





扫码关注【华章计算机】视频号

每天来听华章哥讲书




更多精彩回顾



书讯 | 11月书讯(上)| 拿下这些新书,赢在起跑线书讯 | 11月书讯(下) | 拿下这些新书,赢在起跑线
资讯 | 为什么 Rust 是编程的未来?书单 | 8本书助你零基础转行数据分析岗干货 | SpringBoot 实战:加载和读取资源文件内容收藏 | 看漫画来告诉你:什么是 “元宇宙” ?上新 | 【新书速递】产品经理应该知道的72件事赠书 | 【第80期】浅谈如何成为技术一号位?点击阅读全文购买

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存