用JS开发桌面应用总结|博客系列
# 导读
使用Electron开发客户端程序已经有一段时间了,整体感觉还是非常不错的,其中也遇到了一些坑点,本文旨在从【运行原理】到【实际应用】对Electron进行一次系统性的总结。【多图,长文预警~】
# 一、桌面应用程序

桌面应用程序,又称为 GUI 程序(Graphical User Interface),但是和 GUI 程序也有一些区别。桌面应用程序 将 GUI 程序从GUI 具体为“桌面”,使冷冰冰的像块木头一样的电脑概念更具有 人性化,更生动和富有活力。
我们电脑上使用的各种客户端程序都属于桌面应用程序,近年来WEB和移动端的兴起让桌面程序渐渐暗淡,但是在某些日常功能或者行业应用中桌面应用程序仍然是必不可少的。
传统的桌面应用开发方式,一般是下面两种:
# 1.1 原生开发
直接将语言编译成可执行文件,直接调用系统API,完成UI绘制等。这类开发技术,有着较高的运行效率,但一般来说,开发速度较慢,技术要求较高,例如:
- 使用
C++ / MFC开发Windows应用 - 使用
Objective-C开发MAC应用
# 1.2 托管平台
一开始就有本地开发和UI开发。一次编译后,得到中间文件,通过平台或虚机完成二次加载编译或解释运行。运行效率低于原生编译,但平台优化后,其效率也是比较可观的。就开发速度方面,比原生编译技术要快一些。例如:
- 使用
C# / .NET Framework(只能开发Windows应用) Java / Swing
不过,上面两种对前端开发人员太不友好了,基本是前端人员不会设计的领域,但是在这个【大前端😅】的时代,前端开发者正在想方设法涉足各个领域,使用WEB技术开发客户端的方式横空出世。
# 1.3 WEB开发
使用WEB技术进行开发,利用浏览器引擎完成UI渲染,利用Node.js实现服务器端JS编程并可以调用系统API,可以把它想像成一个套了一个客户端外壳的WEB应用。
在界面上,WEB的强大生态为UI带来了无限可能,并且开发、维护成本相对较低,有WEB开发经验的前端开发者很容易上手进行开发。
本文就来着重介绍使用WEB技术开发客户端程序的技术之一【electron】
# 二、Electron

Electron是由Github开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库。 Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用来实现这一目的。
# 2.1 使用Electron开发的理由:
- 1.使用具有强大生态的
Web技术进行开发,开发成本低,可扩展性强,更炫酷的UI - 2.跨平台,一套代码可打包为
Windows、Linux、Mac三套软件,且编译快速 - 3.可直接在现有
Web应用上进行扩展,提供浏览器不具备的能力 - 4.你是一个前端👨💻~
当然,我们也要认清它的缺点:性能比原生桌面应用要低,最终打包后的安装包和其他文件都比较大。
# 2.2 开发体验
兼容性
虽然你还在用WEB技术进行开发,但是你不用再考虑兼容性问题了,你只需要关心你当前使用Electron的版本对应Chrome的版本,一般情况下它已经足够新来让你使用最新的API和语法了,你还可以手动升级Chrome版本。同样的,你也不用考虑不同浏览器带了的样式和代码兼容问题。
Node环境
这可能是很多前端开发者曾经梦想过的功能,在WEB界面中使用Node.js提供的强大API,这意味着你在WEB页面直接可以操作文件,调用系统API,甚至操作数据库。当然,除了完整的 Node API,你还可以使用额外的几十万个npm模块。
跨域
你可以直接使用Node提供的request模块进行网络请求,这意味着你无需再被跨域所困扰。
强大的扩展性
借助node-ffi,为应用程序提供强大的扩展性(后面的章节会详细介绍)。
# 2.3 谁在用Electron

现在市面上已经有非常多的应用在使用electron进行开发了,包括我们熟悉的VS Code客户端、GitHub客户端、Atom客户端等等。印象很深的,去年迅雷在发布迅雷X10.1时的文案:
从迅雷X 10.1版本开始,我们采用Electron软件框架完全重写了迅雷主界面。使用新框架的迅雷X可以完美支持2K、4K等高清显示屏,界面中的文字渲染也更加清晰锐利。从技术层面来说,新框架的界面绘制、事件处理等方面比老框架更加灵活高效,因此界面的流畅度也显著优于老框架的迅雷。至于具体提升有多大?您一试便知。
你可以打开VS Code,点击【帮助】【切换开发人员工具】来VS Code客户端的界面。

# 3.1 Chromium
Chromium 是Google为发展Chrome浏览器而启动的开源项目,Chromium 相当于Chrome的工程版或称实验版,新功能会率先在Chromium上实现,待验证后才会应用在Chrome 上,故Chrome的功能会相对落后但较稳定。
Chromium为Electron提供强大的UI能力,可以在不考虑兼容性的情况下开发界面。
# 3.2 Node.js
Node.js是一个让JavaScript运行在服务端的开发平台,Node 使用事件驱动,非阻塞I/O 模型而得以轻量和高效。
单单靠Chromium是不能具备直接操作原生GUI能力的,Electron内集成了Nodejs,这让其在开发界面的同时也有了操作系统底层API的能力,Nodejs 中常用的 Path、fs、Crypto 等模块在 Electron 可以直接使用。
# 3.3 系统API
为了提供原生系统的GUI支持,Electron内置了原生应用程序接口,对调用一些系统功能,如调用系统通知、打开系统文件夹提供支持。
在开发模式上,Electron在调用系统API和绘制界面上是分离开发的,下面我们来看看Electron关于进程如何划分。
# 3.4 主进程
Electron区分了两种进程:主进程和渲染进程,两者各自负责自己的职能。

Electron 运行package.json的 main 脚本的进程被称为主进程。一个 Electron 应用总是有且只有一个主进程。
职责:
- 创建渲染进程(可多个)
- 控制了应用生命周期(启动、退出
APP以及对APP做一些事件监听) - 调用系统底层功能、调用原生资源
可调用的API:
Node.js APIElectron提供的主进程API(包括一些系统功能和Electron附加功能)
# 3.5 渲染进程
由于 Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到。 每个 Electron 中的 web页面运行在它自己的渲染进程中。
主进程使用 BrowserWindow 实例创建页面。 每个 BrowserWindow 实例都在自己的渲染进程里运行页面。 当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。
你可以把渲染进程想像成一个浏览器窗口,它能存在多个并且相互独立,不过和浏览器不同的是,它能调用Node API。
职责:
- 用
HTML和CSS渲染界面 - 用
JavaScript做一些界面交互
可调用的API:
DOM APINode.js APIElectron提供的渲染进程API
# 四、Electron基础
# 4.1 Electron API
在上面的章节我们提到,渲染进和主进程分别可调用的Electron API。所有Electron的API都被指派给一种进程类型。 许多API只能被用于主进程中,有些API又只能被用于渲染进程,又有一些主进程和渲染进程中都可以使用。
你可以通过如下方式获取Electron API
const { BrowserWindow, ... } = require('electron')
下面是一些常用的Electron API:

在后面的章节我们会选择其中常用的模块进行详细介绍。
# 4.2 使用 Node.js 的 API

你可以同时在Electron的主进程和渲染进程使用Node.js API,)所有在Node.js可以使用的API,在Electron中同样可以使用。
import {shell} from 'electron';
import os from 'os';
document.getElementById('btn').addEventListener('click', () => {
shell.showItemInFolder(os.homedir());
})
有一个非常重要的提示: 原生Node.js模块 (即指,需要编译源码过后才能被使用的模块) 需要在编译后才能和Electron一起使用。
# 4.3 进程通信
主进程和渲染进程虽然拥有不同的职责,然是他们也需要相互协作,互相通讯。
例如:在
web页面管理原生GUI资源是很危险的,会很容易泄露资源。所以在web页面,不允许直接调用原生GUI相关的API。渲染进程如果想要进行原生的GUI操作,就必须和主进程通讯,请求主进程来完成这些操作。

# 4.4 渲染进程向主进程通信
ipcRenderer 是一个 EventEmitter 的实例。 你可以使用它提供的一些方法从渲染进程发送同步或异步的消息到主进程。 也可以接收主进程回复的消息。
在渲染进程引入ipcRenderer:
import { ipcRenderer } from 'electron';
异步发送:
通过 channel 发送同步消息到主进程,可以携带任意参数。
在内部,参数会被序列化为
JSON,因此参数对象上的函数和原型链不会被发送。
ipcRenderer.send('sync-render', '我是来自渲染进程的异步消息');
同步发送:
const msg = ipcRenderer.sendSync('async-render', '我是来自渲染进程的同步消息');
注意: 发送同步消息将会阻塞整个渲染进程,直到收到主进程的响应。
主进程监听消息:
ipcMain模块是EventEmitter类的一个实例。 当在主进程中使用时,它处理从渲染器进程(网页)发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。
ipcMain.on:监听 channel,当接收到新的消息时 listener 会以 listener(event, args...) 的形式被调用。
ipcMain.on('sync-render', (event, data) => {
console.log(data);
});
# 4.5 主进程向渲染进程通信
https://imweb.io/topic/5b13a663d4c96b9b1b4c4e9c
在主进程中可以通过BrowserWindow的webContents向渲染进程发送消息,所以,在发送消息前你必须先找到对应渲染进程的BrowserWindow对象。:
const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.webContents.send('main-msg', `ConardLi]`)
根据消息来源发送:
在ipcMain接受消息的回调函数中,通过第一个参数event的属性sender可以拿到消息来源渲染进程的webContents对象,我们可以直接用此对象回应消息。

