Electron 快速上手
0x01 概述
(1)基本介绍
- Electron 官网:https://www.electronjs.org
- Electron 是一个使用 HTML、CSS 和 JavaScript 构建跨平台的桌面应用程序的框架
- Electron = Chromium + NodeJS + Native API
- Chromium:负责页面渲染处理
- Node.js:负责后台逻辑处理
- Native API:修改系统注册表的项
- Electron 可以结合 React、Vue、Less 等可以转换为 HTML、CSS、JS 的框架与编程语言
(2)流程模型
- 核心是主进程 Main,是 JS 文件
- 其中是 NodeJS 环境,可以调用所有 NodeJS 提供的 API
- 主进程主要目的是管理渲染进程
- 可以管理多个渲染进程
- 可以调用 Native API
- 渲染进程继承了 Chromium,负责根据 HTML、CSS、JS 渲染窗口
- 可以与主进程双向通信
- Native API 可以根据系统的不同自动使用合适的系统接口,从而实现跨平台
(3)开发环境
- Node:v20.11.1
- npm:v9.8.1
- 编辑工具:Visual Studio Code
0x02 第一个工程
(1)创建工程
-
新建文件夹 project,在新目录下开启终端
-
使用命令
npm init -y
创建 NodeJS 开发环境,在 package.json 中:- 需要保证
author
项和description
不能为空字符串 - 在
scripts
中添加新命令:"start": "electron ."
建议采用默认的 CommonJS 模块
{ "name": "project", "version": "1.0.0", "description": "Electron Application", "main": "main.js", "scripts": { "start": "electron ." }, "keywords": [ "Electron" ], "author": "SRIGT", "license": "MIT", "devDependencies": { "electron": "^31.1.0" } }
- 需要保证
-
使用命令
npm install -D electron
安装开发依赖 Electron -
(可选)配置热更新
-
使用命令
npm install -D nodemon
安装开发依赖 nodemon -
修改 package.json
// ... "scripts": { "start": "nodemon --exec electron ." }, // ...
-
创建 nodemon.json
{ "ignore": [ "node_modules", "dist" ], "restartable": "r", "watch": ["*.*"], "ext": "html,css,js" }
-
-
在工程目录下新建 main.js
// 1. 引入应用与浏览器窗口 const { app, BrowserWindow } = require("electron"); // 2. 当 App 准备好后执行回调函数 app.on("ready", () => { // 3. 创建浏览器窗口对象并使用变量存储 const window = new BrowserWindow({ width: 800, // 窗口宽度 height: 600, // 窗口高度 autoHideMenuBar: true, // 隐藏默认菜单栏 }); // 4. 窗口加载页面 window.loadURL("https://www.cnblogs.com/SRIGT"); });
BrowserWindow
更多属性参考:https://www.electronjs.org/zh/docs/latest/api/base-window
-
使用命令
npm run start
启动工程
(2)本地页面
-
在工程根目录创建 pages 目录,其中包括本地页面的 HTML 和 CSS
-
在 pages 中创建 index.html、index.css
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>本地页面</title> <link rel="stylesheet" href="./index.css"> </head> <body> <h1>这是一个在 Electron 中展示的本地页面</h1> </body> </html>
body { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; background-color: #f0f8ff; }
-
修改 main.js
const { app, BrowserWindow } = require("electron"); app.on("ready", () => { const window = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, }); // 加载本地(页面)文件 window.loadFile("./pages/index.html"); });
-
使用命令
npm run start
启动工程 -
使用快捷键
Ctrl+Shift+i
可以在 Electron 应用中开启控制台- 之后可以通过代码禁用控制台
-
在 Console 页中,控制台发出关于 CSP(内容安全策略)的警告,需要在页面的头部配置以下内容解决:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';" />
(3)适应系统
-
主要在于 Windows 系统与 Mac 系统对窗口与应用的关系处理
- 在 Windows 系统中,最后一个窗口关闭后,应用也会自动关闭
- 在 Mac 系统中,最后一个窗口关闭后,应用不会自动关闭
-
修改 main.js
const { app, BrowserWindow } = require("electron"); function createWindow() { const window = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, }); window.loadFile("./pages/index.html"); } app.on("ready", () => { createWindow(); // 当应用被激活时,如果没有打开窗口,则创建窗口 app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); // 当所有窗口关闭时,如果不是 Mac 系统,则退出应用 app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } });
0x03 进程
(1)渲染进程
-
每个 Electron 应用的主进程的控制程序是唯一的(如 main.js)
-
每个
BrowserWindow
对象实例都对应着一个或多个独立的渲染进程控制程序-
在 pages 目录下创建 render.js
const button = document.querySelector("button"); button.addEventListener("click", () => { alert("触发点击事件"); });
-
修改 pages\index.html
<!-- ... --> <body> <h1>这是一个在 Electron 中展示的本地页面</h1> <button>点击按钮</button> <script type="text/javascript" src="./render.js"></script> </body> <!-- ... -->
-
此时 render.js 就是渲染进程的控制程序
-
(2)预加载脚本
预加载脚本在渲染进程上运行,但可以访问一部分 NodeJS 的 API
-
在工程根目录下创建 preload.js 作为预加载脚本
console.log("执行预加载脚本...");
-
修改 main.js
const { app, BrowserWindow } = require("electron"); const path = require("path"); function createWindow() { const window = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, webPreferences: { preload: path.resolve(__dirname, "./preload.js"), }, }); window.loadFile("./pages/index.html"); } // ...
- 如果报错 ReferenceError: __dirname is not defined,则将
__dirname
改为process.cwd()
- 如果报错 ReferenceError: __dirname is not defined,则将
-
修改 render.js
console.log("渲染进程进行中..."); // ...
-
使用命令
npm run start
启动工程-
此时,根据 Visual Studio Code 的终端输出以及窗口的控制台输出可以发现,脚本的执行顺序为:
graph LR 主进程-->预加载脚本-->渲染进程 -
此时仅实现了主进程与预加载脚本的关联,还需要完成渲染进程与预加载脚本的关联
-
-
修改 preload.js
const { contextBridge } = require("electron"); contextBridge.exposeInMainWorld("key", { value: "Hello World", });
-
修改 render.js
const button = document.querySelector("button"); button.addEventListener("click", () => { alert(window.key.value); });
-
使用命令
npm run start
启动工程
(3)进程通信
- 进程通信主要指主进程与渲染进程之间通信,通过“中间人”——预加载脚本实现进程通信
a. 渲染进程向主进程单向通信
-
方法
graph LR 渲染进程--"ipcRender.send"-->d((数据))--"ipcMain.on"-->主进程 -
常用于在 Web 中调用主进程的 API
-
举例:
-
修改 pages\index.html
<!-- ... --> <body> <input type="text" /> <button>提交</button> <script type="text/javascript" src="./render.js"></script> </body> <!-- ... -->
-
修改 pages\render.js
const input = document.querySelector("input"); const button = document.querySelector("button"); button.addEventListener("click", () => { // 调用 api.save 方法 api.save(input.value); });
-
修改 preload.js
const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("api", { save: (text) => { // 发送数据到主进程 ipcRenderer.send("save", text); }, });
-
修改 main.js
const { app, BrowserWindow, ipcMain } = require("electron"); const path = require("path"); function createWindow() { const window = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, webPreferences: { preload: path.resolve(process.cwd(), "./preload.js"), }, }); window.loadFile("./pages/index.html"); // 接收来自 preload.js 的数据 ipcMain.on("save", (event, data) => { console.log(data); }); }
-
b. 渲染进程向主进程双向通信
-
方法
flowchart LR 渲染进程<--"ipcRender.invoke"-->d((数据))<--"ipcMain.handle"-->主进程 -
常用于:从渲染进程调用主进程方法并等待结果
-
举例:
-
修改 pages\index.html
<!-- ... --> <body> <button>获取</button> <script type="text/javascript" src="./render.js"></script> </body> <!-- ... -->
-
修改 pages\render.js
const button = document.querySelector("button"); button.addEventListener("click", async () => { let data = await api.read(); document.body.innerHTML += `<p>${data}</p>`; });
-
修改 preload.js
const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("api", { read: () => { return ipcRenderer.invoke("read", "hello"); }, });
-
修改 main.js
const { app, BrowserWindow, ipcMain } = require("electron"); const path = require("path"); function createWindow() { const window = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, webPreferences: { preload: path.resolve(process.cwd(), "./preload.js"), }, }); window.loadFile("./pages/index.html"); ipcMain.handle("read", (event, data) => { console.log(data); return "Hello World!"; }) }
-
c. 主进程向渲染进程通信
-
方法
graph LR 主进程--"BrowserWindow().webContents.send"-->d((数据))--"ipcRender.on"-->渲染进程 -
常用于:主进程主动向渲染进程发送数据
-
举例:
-
修改 pages\index.html
<!-- ... --> <body> <script type="text/javascript" src="./render.js"></script> </body> <!-- ... -->
-
修改 pages\render.js
window.onload = () => { api.getMsg((event, msg) => { alert(msg); }); };
-
修改 preload.js
const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("api", { getMsg: (callback) => { return ipcRenderer.on("msg", callback); }, });
-
修改 main.js
const { app, BrowserWindow } = require("electron"); const path = require("path"); function createWindow() { const window = new BrowserWindow({ width: 800, height: 600, autoHideMenuBar: true, webPreferences: { preload: path.resolve(process.cwd(), "./preload.js"), }, }); window.loadFile("./pages/index.html"); setTimeout(() => { window.webContents.send("msg", "Hello World!"); }, 2000); }
-
d. 渲染进程之间通信
-
方法
graph LR A[渲染进程 A]--ipcRender.send-->d1((数据))--ipcMain.on-->主进程--"BrowserWindow().webContents.send"-->d2((数据))--ipcRender.on-->B[渲染进程 B]
0x04 打包
-
使用命令
npm install -D electron-builder
安装开发依赖 electron-builder -
修改 package.json
{ "name": "project", "version": "1.0.0", "description": "Electron Application", "main": "main.js", "scripts": { "start": "nodemon --exec electron .", "build": "electron-builder" }, "build": { "appId": "com.example.demo", "win": { "icon": "./icon.ico", "target": [ { "target": "nsis", "arch": ["x64"] } ] }, "nsis": { "oneClick": false, "perMachine": true, "allowToChangeInstallationDirectory": true } }, "keywords": [ "Electron" ], "author": "SRIGT", "license": "MIT", "devDependencies": { "electron": "^31.1.0", "electron-builder": "^25.0.0-alpha.9", "nodemon": "^3.1.4" } }
- 添加打包命令:
"scripts": { "build": "electron-builder" }
- 添加打包配置:
"build": {}
- 应用 id:
"appId": "com.example.demo"
- 适应 Windows 系统:
"win": {}
- 图标:
"icon": "./icon.svg"
- 安装程序格式与位数:
"target": [{ "target": "nsis", "arch": ["x64"] }]
- 图标:
- 配置 nsis 格式:
- 禁用一键安装:
"oneClick": false
- 每台机器安装一次:
"perMachine": true
- 允许自定义安装目录:
"allowToChangeInstallationDirectory": true
- 禁用一键安装:
- 应用 id:
- 添加打包命令:
-
使用命令
npm run build
执行打包- 如果因为下载依赖包报错,则可以根据报错提供的 URL 下载对应的包,并解压到 C:\Users[username]\AppData\Local\electron-builder\Cache 目录下