什么是 electron
Electron 是一种基于 Node.js 和 Chromium 开发的开源框架,用于构建跨平台桌面应用程序。
Electron 提供了许多跨平台的功能,例如窗口管理、菜单、对话框和托盘图标等。它还可以轻松地与 Node.js 模块进行集成,以便开发人员可以使用已有的 Node.js 生态系统中的库和工具。
这使得 Electron 非常适合开发具有丰富用户界面和本地操作系统集成的桌面应用程序。
由于 Electron 使用 Chromium 作为其呈现引擎,因此应用程序可以获得高性能和现代 Web 技术的支持。
同时,由于其跨平台特性,开发人员可以使用一致的开发工具和技术来构建适用于 Windows、macOS 和 Linux 等多个操作系统的应用程序。
简单点说,就是 Electron 提供一套方法,将 nodejs 和 Chromium 打包成一套跨平台桌面端应用。
为什么要选择 Electron
我们来看一下有哪些技术可以开发桌面应用:
编程语言 / 技术框架 | 可提供的能力 | 代表作 |
---|---|---|
C#/WPF | 开发 Windows 应用 | Visual Studio |
Swift | 开发 macOS 应用 | CodeEdit |
C++/QT | 跨平台应用 | YY 语音 |
C++/CEF | 跨平台应用 | 钉钉 |
Java/Swing | 跨平台应用 | JetBrains系列软件 |
JavaScript/Electron | 跨平台应用 | Visual Studio Code |
Rust/Tauri | 跨平台应用 | xplorer |
Dart/Flutter | 跨平台应用 | rustdesk |
作为一名开发者,学习某项技术需要看投资回报率高不高,像 Windows 平台上的 C# 和 macOS 平台上的 Swift,个人觉得技术路线会比较窄,未来更多的场景是采用跨平台的技术来实现。
所以如果有 C++ 的功底,QT 和 CEF 是不错的选择,而对于前端来说,最好的选择无外乎下面三个:
- Electron:纯 JavaScript 技术栈,生态非常成熟,前端同学快速上手,无学习成本
- Tauri:打出来的包非常小,需要一定的 Rust 基础
- Flutter:一套代码通吃 web、iOS、Android、macOS、Windows、Linux 六大平台
可以看到,上述三个选择的优势都非常明显,但是其他两个对于前端来说都有一定的学习成本,需要学习 rust 和 dart 语言,而 Electron 对于前端来说简直就是零成本,所以从学习成本来说 Electron 是最适合的。
从流行度上来说,相当多的桌面应用使用的就是 Electron 开发。这其中不乏大名鼎鼎、如雷贯耳的应用,例如 Postman、Notion、Skype 等,而且我敢打赌,各位的电脑上一定安装过用 Electron 开发的应用,如果你用的是 Mac 电脑,请在命令行运行下面的命令来检测本地采用 Electron 技术开发的桌面软件:
for app in /Applications/*; do;[ -d $app/Contents/Frameworks/Electron\ Framework.framework ] && echo $app; done
我本地检测出来的应用有:
$ for app in /Applications/*; do;[ -d $app/Contents/Frameworks/Electron\ Framework.framework ] && echo $app; done
/Applications/Baidu***disk_mac.app
/Applications/QQ.app
/Applications/Scratch 3.app
/Applications/Visual Studio Code.app
/Applications/Xmind.app
/Applications/aDrive.app
/Applications/lx-music-desktop.app
最后我们来看一下 Electron 比 Web 的优势在哪里:
- 资源本地化和离线持久化:对于传统 Web 应用来说,性能卡点往往不在于解析和渲染,而在于网络请求,把资源本地化之后,可以做到应用启动后页面秒开,极大程度上减少了白屏和用户等待的时间。另外,传统 Web 的数据都是通过调用接口实时保存到服务端,无法做到离线持久化,而 Electron 可以将用户配置、缓存数据全部写入本地文件,在离线状态下依然能够正常使用软件,当恢复在线时同步到云端,例如笔记类应用。
- 系统底层 API 调用:Web 环境本质上是浏览器提供的一个沙箱,出于安全考虑限制了很多底层能力,例如文件操作等,而在 Electron 里面,你可以调用任何操作系统提供的 API,包括 node.js 帮开发者封装好的 fs、child_process、os 等。
- 不受限的网络通信:每一个前端开发都会遇到跨域问题,这其实是 Web 浏览器为了保证不同域下的数据安全,人为制造出来的限制,而在 Electron 环境下,你不用考虑是否跨域,所有 http 和 https 请求都畅通无阻,而且你还能对这些请求进行拦截和重定向,甚至修改任意 header 字段和响应结果。不仅如此,更底层的网络能力,例如 tcp 和 udp 也是支持的,你完全可以做一个类似于 QQ 一样的聊天软件。
- 可定制化的窗口:传统 Web 只能给用户提供一个 tab,单调乏味,Electron 可以创建各式各样的无边框窗口,甚至可以设置成圆形或三角形的形状,例如你可能用过苹果自带的 Spotlight 或者 Alfred,只提供了一个搜索入口,快捷键唤起全局置顶的输入框,用完即走,从视觉和交互体验上都完爆 Web,除此之外,桌面应用还可以设置托盘菜单,做出类似于 QQ 消息一闪一闪的效果。
所以各位前端们还等什么,快跟着我一起学起来!
安装 Electron
为什么要特地讲一下安装呢,因为这里有一个坑
当执行 npm install electron -D
会一直卡在一个地方无法正常安装
解决办法 使用 ***pm:
npm install -g ***pm --registry=https://registry.npmmirror.***
***pm install electron -D
桌面CSDN实战
纸上得来终觉浅,我们接下来先通过手写一个 Electron Demo 带大家入门 Electron。
我们首先来快速创建一个 Electron 项目:
$ mkdir my-electron
$ cd my-electron
$ npm init -y
$ npm install electron electron-packager --dev
接下来我们创建一个 src 目录,里面再分别创建两个子目录 main 和 renderer。
在 Electron 中有主进程和渲染进程两个重要的概念,我们在主进程里面用 Node.js 代码调用 Electron 封装好的 API 来创建窗口,管理应用整个生命周期,而在渲染进程里面加载传统的 Web 界面。
因此 main 目录用于存放跟主进程相关的代码,renderer 目录用于存放跟渲染进程相关的代码。
整个桌面应用的入口在主进程里面,接下来在 main 目录中创建 index.js 作为入口。
为了让 Electron 知晓该入口,我们需要在 package.json 中做以下指定:
"main": "src/main/index.js",
"scripts": {
"start": "electron ."
},
至此一个基础的 Electron 项目就搭建完毕,接下来我们可以来写我们的页面了。
在 src/main/index.js 入口文件中,我们来加载一个 CSDN :
//引入两个模块:app 和 BrowserWindow
//app 模块,控制整个应用程序的事件生命周期。
//BrowserWindow 模块,它创建和管理程序的窗口。
const { app, BrowserWindow } = require('electron')
//在 Electron 中,只有在 app 模块的 ready 事件被激发后才能创建浏览器窗口
app.whenReady().then(() => {
//创建一个窗口
createWindow()
})
function createWindow() {
const mainWindow = new BrowserWindow({ width: 800, height: 600 })
//窗口加载 URL
mainWindow.loadURL('https://www.csdn.***/')
}
然后在终端输入 npm start
启动项目,你会看到一个加载 CSDN 官网的窗口弹出来了
一般的企业桌面应用都会定义自己的专属协议,我们可能都遇到过在网页的百度网盘点击下载,会自动打开我们电脑里的百度网盘软件。这是怎么做到的呢?就是通过自定义协议。
所谓自定义协议,其实就是给应用起个独一无二的名称,然后注册到操作系统里面,凡是通过这个协议名就能唤起这个软件了,在 Electron 中注册协议只需要一行代码:
app.setAsDefaultProtocolClient('electron-desktop')
注册之后,当在浏览器中输入 electron-desktop://
之后,会发现弹出跳转提示,点击同意就能启动并跳转到桌面应用了,通过这种协议唤起应用被称为 scheme 唤起,而且在唤起的时候还可以带上一些参数,例如:
electron-desktop://width=800&height=600
自定义协议之后,可以用 scheme 唤起桌面应用,这是非常重要的能力
这里面最关键的是需要拿到协议唤起参数,否则唤起 QQ 之后不知道要跟谁聊天,唤起百度网盘之后不知道要下载哪款资料。
scheme 唤起的行为是操作系统默认支持的,操作系统也提供了 API 来监听唤起事件并拿到唤起参数。
在 Mac 和 Windows 上获取协议唤起参数是不一样的,这是由于平台策略不同导致的,这里讲解一下 Mac。
在 Mac 上面通过监听 open-url 事件,可以拿到唤起的 scheme 参数:
app.on('open-url', (event, url) => {
console.log(url) // 打印 electron-desktop://width=800&height=600
})
url 里面就是 scheme 唤起的完整地址字符串,除了开头的 electron-desktop://
前缀之外,后面的内容是完全交给用户自定义的,例如:
electron-desktop://hello-CSDN
electron-desktop://1+1=2
这些都可以唤起,上面之所以用 width=800&height=600
完全是因为模仿 http 地址栏的 query 参数的格式,有现成的 API 方便解析参数而已。
下面我们把 open-url 的回调获取到的 scheme 参数解析出来放到全局变量 urlParams 里面:
const { app, BrowserWindow } = require('electron')
const protocol = 'electron-desktop'
app.setAsDefaultProtocolClient(protocol)
let urlParams = {}
app.on('open-url', (event, url) => {
const scheme = `${protocol}://`
const urlParams = new URLSearchParams(url.slice(scheme.length))
urlParams = Object.fromEntries(urlParams.entries())
})
app.whenReady().then(() => {
createWindow()
})
function createWindow() {
const mainWindow = new BrowserWindow({ width: 800, height: 600 })
mainWindow.loadURL('https://www.csdn.***/')
}
协议唤起在 Mac 平台上有两点需要注意:
- open-url 要在 ready 事件之前注册,因为有些场景是需要拿到参数之后再决定如何创建窗口的,如果放在 ready 回调里面,createWindow 可能会拿不到该参数了。
- 在应用支持多实例场景下,如果程序未启动,会立即启动应用,在 open-url 中获取到唤起参数。如果存在正在运行的实例(可能有多个),会激活(其中一个)已经运行的程序,而不会开启新的实例,被激活的实例可以通过 open-url 回调获取唤起参数。
接下来我们来完成这个桌面 CSDN demo:
- 打开桌面应用后立即进入 CSDN 首页
- 支持用
csdn://
这个 scheme 唤起应用 - 支持用
csdn://width=500&height=300
这个 scheme 指定窗口大小
const { app, BrowserWindow } = require('electron')
let mainWindow
const protocol = 'csdn'
const scheme = `${protocol}://`
app.setAsDefaultProtocolClient(protocol)
let urlParams = {}
handleSchemeWakeup(process.argv)
app.on('open-url', (event, url) => handleSchemeWakeup(url))
app.whenReady().then(() => {
createWindow()
})
// 创建 electron 新窗口
function createWindow() {
const width = parseInt(urlParams.width) || 800
const height = parseInt(urlParams.height) || 600
if (mainWindow) {
mainWindow.setSize(width, height)
} else {
mainWindow = new BrowserWindow({ width, height })
mainWindow.loadURL('https://www.csdn.***/')
}
}
// 处理自定义协议 scheme 唤起
function handleSchemeWakeup(argv) {
const url = [].concat(argv).find((v) => v.startsWith(scheme))
if (!url) return
// url 之间的 search 转换
const searchParams = new URLSearchParams(url.slice(scheme.length))
urlParams = Object.fromEntries(searchParams.entries())
if (app.isReady()) createWindow()
}
到此为止,我们的桌面 CSDN 实战就完成了。
我们把传统的 Web 页面通过用 Electron 加载出来的方式叫做「套壳桌面应用」,这也是将网站做成桌面软件最快速的方式。
在开发完成后,需要将应用程序打包成可执行文件,可以使用 electron-builder 进行打包。
在命令行中运行以下命令进行安装:
npm install electron-builder --save-dev
安装完成后,可以在项目的 package.json 文件中添加以下脚本:
"scripts": { "start": "electron .", "build": "electron-builder" }
然后,在命令行中运行 npm run build 命令即可打包 Electron 应用程序。
相信你通过上面这个的应用已经了解了 Electron 的简单开发和打包流程,
接下来我们来学习一些 Electron 的基础理论知识和进阶用法。
Electron 基础配置
const { app, BrowserWindow } = require('electron')
let win
// 监听electron 加载完毕的时候的创建窗口等等
app.on('ready', function () {
// 创建一个窗口 设置属性
win = new BrowserWindow({
//fullscreen: true //全屏
//frame: false, //让桌面应用没有边框,这样菜单栏也会消失
resizable: false, //不允许用户改变窗口大小
width: 800, //设置窗口宽高
height: 600,
icon: iconPath, //应用运行时的标题栏图标
minWidth: 300, // 最小宽度
minHeight: 500, // 最小高度
maxWidth: 300, // 最大宽度
maxHeight: 600, // 最大高度
// 进行对首选项的设置
webPreferences:{
backgroundThrottling: false, //设置应用在后台正常运行
nodeIntegration:true, //设置能在页面使用nodejs的API
contextIsolation: false, //关闭警告信息
//preload: path.join(__dirname, './preload.js')
}
})
// 这里让主进程加载一个index.html
win.loadFile('index.html')
// 设置为最顶层
//win.setAlwaysOnTop(true)
//win.loadURL(`www.baidu.***`) 可以让主进程打开文件或者一个链接
// 监听窗口关闭事件
win.on('closed',()=>{
//释放win
win = null
})
})
// 监听所有的窗口都关闭了
app.on('window-all-closed', () => {
console.log('窗口全部都关闭了')
})
Electron 进程
Electron 应用程序有两种类型的进程:主进程和渲染进程。
主进程负责管理应用程序的生命周期和所有窗口,而渲染进程负责显示窗口内容。
通常,主进程和渲染进程是通过 ipcMain 和 ipcRenderer 模块进行通信。
使用主进程和渲染进程,你可以更好地管理应用程序和窗口,并且可以在不同的进程中处理不同的任务。
主进程
Electron 运行 package.json 的 main 脚本的进程被称为主进程 (只有一个)
主进程特点:
- 主进程连接着操作系统和渲染进程,可以把它看做页面和计算机沟通的桥梁
- 进程间通信、窗口管理
- 全局通用服务
- 一些只能或适合在主进程做的事情,例如浏览器下载、全局快捷键处理、托盘、session
- 维护一些必要的全局状态
渲染进程
渲染进程就是我们所熟悉前端环境了,只是载体改变了,从浏览器变成了 window.
注:出于安全考虑,渲染进程是不能直接访问本地资源的,因此都需要在主进程完成。
渲染进程特点:
- Electron 使用了 Chromium 来展示 web 页面,所以 Chromium 的多进程架构也被使用到
- 每个 web 页面运行在它自己的渲染进程中。每个渲染进程都是相互独立的,并且只关心他们自己的网页
- 使用 BrowserWindow 类开启一个渲染进程并将这个实例运行在该进程中,当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止
- 渲染进程中不能调用原生资源,但是渲染进程中同样包含 Node.js 环境,所以可以引入 Node.js
主进程与渲染进程的区别
- 主进程使用 BrowserWindow 实例创建网页
- 每个 BrowserWindow 实例都在自己的渲染进程里运行着一个网页。当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止
- 主进程管理所有页面和与之对应的渲染进程
- 由于在网页里管理原生 GUI 资源是非常危险而且容易造成资源泄露,所以在网页面调用 GUI 相关的 API 是不被允许的。如果你想在网页里使用 GUI 操作,其对应的渲染进程必须与主进程进行通讯,请求主进程进行相关的 GUI 操作
把它们想象成这样 Chrome 的每个标签页及其页面,就好比 Electron 中的一个单独渲染进程。
即使关闭所有标签页,Chrome 依然存在。这好比 Electron 的主进程,能打开新的窗口或关闭这个应用。
主进程与渲染进程的通信
主线程 到 渲染线程 通过 webContents.send
来发送 —>ipcRenderer.on
来监听
渲染线程 到 主线程 需要通过 ipcRenderer.send
发送 —> ipcMain.on
来监听
Electron 跨平台问题
跨平台问题是在开发 Electron 应用程序时需要考虑的一个重要问题。
由于不同平台有不同的 UI 和 API,因此需要使用不同的代码来处理跨平台问题。
以下是一些常见的跨平台问题和解决方法:
- 文件路径:在不同的操作系统上,文件路径的格式可能不同。为了解决这个问题,可以使用 Node.js 提供的 path 模块来处理文件路径。
- 窗口大小和位置:在不同的操作系统上,窗口的大小和位置可能有所不同。为了解决这个问题,可以使用 Electron 提供的 screen 模块来获取屏幕大小和分辨率,并使用相对位置来设置窗口的大小和位置。
- 快捷键:在不同的操作系统上,快捷键的组合键可能不同。为了解决这个问题,可以使用 Electron 提供的 globalShortcut 模块来注册快捷键,该模块可以自动适应不同的操作系统。
可以使用快捷键 Ctrl+Shift+I(Windows 和 Linux)或 Cmd+Shift+I(macOS)打开开发者工具。
Electron 部署
打包应用程序
在将应用程序部署到生产环境之前,需要将其打包为可执行文件。
以下是一些常用的工具,可以帮助我们将 Electron 应用程序打包为可执行文件:
- electron-builder:一个基于 Electron 的打包器,支持将应用程序打包为各种格式,如 Windows、macOS 和 Linux。
- electron-packager:另一个流行的打包器,也支持多种格式。
以下介绍一下 Electron Builder 的使用步骤和注意事项:
首先需要使用 npm 安装 Electron Builder:
npm install electron-builder --save-dev
然后我们要配置 package.json 文件,我们需要添加一些字段来配置应用程序的打包和发布。
- build 字段:用于配置应用程序的构建选项。
- directories 字段:用于配置应用程序的源代码和构建输出目录。
- repository 字段:用于配置应用程序的源代码仓库地址。
以下是一个 package.json 文件的示例:
{
"name":"my-electron-app",
"version":"1.0.0",
"description":"my Electron App",
"main":"main.js",
"scripts":{
"start":"electron .",
"build":"electron-builder"
},
"repository":{
"type":"git",
"url":"https://github.***/username/my-electron-app.git"
},
"build":{
"appId":"***.example.my-electron-app",
"productName":"my Electron App",
"directories":{
"output":"dist"
}
}
}
在 build 字段中,我们需要添加一些构建选项来指定应用程序的行为,以下是一些常用的构建选项:
- appId:应用程序的 ID。
- productName:应用程序的名称。
- files:要打包的文件和文件夹。
- directories:源代码和构建输出目录。
- asar:是否将应用程序打包成 ASAR 文件。
- mac、win、linux:用于配置每个平台的构建选项。
- dmg、nsis、deb:用于配置每个平台的安装包选项。
以下是一个常见的 build 字段的示例:
{
"build":{
"appId":"***.example.my-electron-app",
"productName":"My Electron App",
"directories":{
"buildResources": "build", //指定打包需要的静态资源,默认是build
"output": "dist", //打包生成的目录,默认是dist
},
"files":[
"main.js",
"package.json",
"index.html",
"assets/**/*"
],
"asar":true,
"mac":{
"target":"dmg",
"icon":"assets/icon.i***s"
"category": "public.app-category.utilities" //应用程序安装到哪个分类下
},
"win":{
"target":"nsis",
"icon":"assets/icon.ico"
},
"linux":{
"target":"deb",
"icon":"assets/icon.png"
},
"dmg": {
"background": "build/background.jfif", //安装窗口背景图
"icon": "build/icons/icon.i***s", //安装图标
"iconSize": 100, //图标的尺寸
"contents": [ //安装图标在安装窗口中的坐标信息
{
"x": 380,
"y": 180,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 180,
"type": "file"
}
],
"window": { //安装窗口的大小
"width": 540,
"height": 380
}
}
}
}
在 package.json 文件中,可以使用以下命令来打包应用程序:
npm run build
执行此命令后,Electron Builder 将使用我们在 build 字段中配置的选项来构建应用程序,并将构建输出文件保存到 directories.output 中指定的目录。
在打包生成的文件夹中,会有一个 app.asar
,它是 Electron 应用程序的主业务文件压缩包,要知道项目中哪些文件被 pack 到安装包,可以通过解压 app.asar
进行查看。
解压完 app.asar
后,里面除了项目的所有文件,还有一个 node_modules
。
对于 node_modules
,并不是所有 node_modules
中的内容都会被打包进安装包,只有 package.json
中 dependencies
字段中的依赖会被打包,devDependencies
字段中的依赖则不会。
这是唯一规则,跟项目实际是否使用依赖没有关系。
所以,为了减小安装包体积,建议在渲染进程中使用的外部包,都安装在 devDependencies
中,然后使用 webpack 将外部包的代码和业务代码打包到一起。
发布应用程序
将应用程序打包为可执行文件之后,我们就可以将其发布到各个平台的应用商店或者自己的网站上。
electron-updater:用于自动化发布应用程序的工具
更新应用程序的安装包应该存放在互联网的某台服务器上,每次打开应用的时候,进行自动检测,根据当前应用程序的 version
和线上版本进行匹配,当发现有新的 version
的时候,就自动下载,下载完成后,询问用户是否安装新版本。
还有一种情况就是 web 资源和 “app 壳子” 分离,web 资源放在服务器,每次都通过网络动态加载,像我们上面的桌面 CSDN 实战一样:
mainWindow.loadURL('https://www.csdn.***/')
在业务需要频繁更新的场景中,可以使用这种方式,快速无障碍地实现更新。
在这种情况下,我们可以按照上述方式打包和更新 “壳子”,也就是主进程相关;
而页面资源的打包和普通的前端项目打包无异,这里不再赘述。
Electron 跨端原理
Electron
的跨端原理并不难理解,我们在这里简单介绍一下,相信大家能很容易理解。
它通过集成浏览器内核,使用前端技术来实现不同平台下的渲染,并结合了 Chromium
、Node.js
和用于调用系统本地功能的 API
三大板块。
-
Chromium
为Electron
提供强大的UI
渲染能力,由于Chromium
本身跨平台,因此无需考虑代码的兼容性。最重要的是,可以使用前端三板斧进行Electron
开发。 -
Chromium
并不具备原生GUI
的操作能力,因此Electron
内部集成Node.js
,编写UI
的同时也能够调用操作系统的底层API
,例如 path、fs、crypto 等模块。 -
Native API
为Electron
提供原生系统的GUI
支持,借此Electron
可以调用原生应用程序接口。
总结起来,Chromium
负责页面 UI
渲染,Node.js
负责业务逻辑,Native API
则提供原生能力和跨平台。
总结
Electron
确实是构建跨平台桌面应用程序的利器,建议大家学习的时候多看看官方的文档,因为Electron
版本迭代太快了,不及时看官方文档真的还是有不少坑的。
简单做个小总结,本文介绍了一些 Electron 的基础知识,包括 Electron 的定义、优势、基础配置、进程、部署、跨端原理,并完成了一个桌面 CSDN 应用实战案例。
通过掌握上述这些知识,你已经可以试着开发属于你自己的 Electron 应用程序啦,快去试试吧。