Electron 开发之旅:从入门到实践
前言
接续上一篇 blog,这篇的内容主要和 Electron 有关。
课设不是特别想做下去了,实际核心代码大概只有 3,4 百行左右,比较水......
或许会把 Docker 的部署也做一做(权当是练习了)。
日程
现在是晚上 7 点 40 分,希望 9 点之前能把应用打包好,这样今天还能匀点时间用来复习。
困困困困困困困困困困困困困困困困困困
Electron 的水比我想象的深,搞不懂所以用 bat 了
过 0 点了,都不睡觉是吧,我和你们爆了😡😭😡😭
学习内容
《省流》
- Electron 安装与入门
- 预加载脚本与进程通信
- 打包应用
- bat 脚本启动前后端
1. Electron 安装与入门
先放个官方的安装教程:安装指导 | Electron
基本上参照官方的教程安装即可。因为 Electron 的安装过程分为两部分:
- 下载 npm 包 (镜像)
- 下载 Electron 二进制文件(GitHub 下载)
所以国服玩家最好设置一下国内的镜像源:
bash
# 设置淘宝镜像源
npm config set registry https://registry.npmmirror.com
# 从国内镜像下载二进制包
npm install electron --save-dev --electron-mirror=https://npmmirror.com/mirrors/electron/
接着是入门程序:
官方 api 文档:app | Electron
javascript
// main.js
const { app, BrowserWindow } = require('electron')
function createWindow() {
const win = new BrowserWindow({ // 创建窗口对象
width: 800,
height: 600,
autoHideMenuBar: true, // 隐藏菜单栏
})
win.loadURL('https://www.github.com') // 加载网页
}
app.on('ready', () => { // 启动钩子
createWindow()
})
如果要启动本地的网页资源:
javascript
win.loadFile('./pages/index.html')
通常还需要设置内容安全策略来控制资源的访问:
html
<!-- index.html -->
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
关于 CSP 的详细说明请参考:内容安全策略(CSP) - HTTP | MDN
窗口默认行为:
javascript
// windows 习惯:关闭所有窗口后退出程序
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
// Mac 习惯
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
配置自动重启:
这个能保证在编写代码后 electron 应用能自动更新
bash
# 安装 nodemon
npm i nodemon -D
# 配置 nodemon.json
{
"ignore": [
"node_modules",
"dist"
],
"restartable": "r",
"watch": ["*.*"],
"ext": "html,js,css",
"delay": 10000
}
# 配置 package.json
"start": "nodemon --exec electron .",
2. 预加载脚本与进程通信
首先要了解 Electron 的流程模型:
由一个入口 主进程(main.js)和多个渲染进程(render.js [即 index.html] 的 js 脚本)组成
预加载脚本(preload.js),作为沟通两者的桥梁
javascript
// preload.js
const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', { // 形成全局上下文键值对
version: process.version,
})
在 main.js 中声明
javascript
const path = require('path')
function createWindow() {
const win = new BrowserWindow({
...
webPreferences: {
preload: path.resolve(__dirname, "./preload.js")
}
})
}
在 render.js 中调用
javascript
const btn1 = document.getElementById('btn1')
btn1.onclick = () => {
alert(myAPI.version)
}
进程通信(IPC)是 Electron 中的一个重要概念,实现进程之间的通信
1)单向通信
在 main.js 中导入 ipc 模块:
javascript
const { ipcMain } = require('electron')
function createWindow() {
.....
ipcMain.on('file-save', writeFile)
}
function writeFile(_, data) {
fs.writeFileSync(
path.join(__dirname, 'output.txt'),
data
)
}
在 preload.js 中设置信道方法:
javascript
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
version: process.version,
saveFile: (data) => {
ipcRenderer.send('file-save', data)
}
})
在 render.js 中触发方法,将信息 单向 向主进程通信
javascript
btn2.onclick = () => {
myAPI.saveFile(input.value)
}
2)双向通信
实现原理和单向类似,只是方法细节上有所区别
javascript
// main.js
ipcMain.handle('file-read', readFile)
....
function readFile() {
return fs.readFileSync(
path.join(__dirname, 'output.txt')
).toString()
}
// preload.js
readFile: () => {
return ipcRenderer.invoke('file-read')
}
// render.js
btn3.onclick = async () => {
let data = await myAPI.readFile()
alert(data)
}
!需要注意一个 axios 上的细节:
Electron 的进程间通信是异步的
invoke() 设计就是返回 Promise 来处理这种异步性
Promise 链延续:
- 如果不 return,Promise 链会在 preload 层中断
- 加了 return 才能让 Promise 链延续到渲染进程
实际调用示例对比
能正常工作的版本:
javascript
// preload.js
contextBridge.exposeInMainWorld('myAPI', {
readFile: () => {
return ipcRenderer.invoke('file-read') // 有 return
}
})
// 渲染进程
const data = await myAPI.readFile() // 能获取到数据
不能工作的版本:
javascript
// preload.js
contextBridge.exposeInMainWorld('myAPI', {
readFile: () => {
ipcRenderer.invoke('file-read') // 无 return
}
})
// 渲染进程
const data = await myAPI.readFile() // data 将是 undefined
建议:永远 return IPC 调用的结果:无论是 invoke() 还是 send() 的响应
3. 打包应用
这里用到的打包方法是 electron-builder,配置麻烦,但是自由度比较高
bash
# 安装 electron-builder
npm install electron-builder -D
完整 package.json
json
"build": {
// 应用程序的唯一标识符(反向域名格式)
"appId": "com.kacat.testapp",
// Windows 平台特定配置
"win": {
// 应用程序图标的路径(建议使用 256x256 像素的.ico文件)
"icon": "./logo.ico",
// 构建目标配置
"target": [
{
// 使用 NSIS 制作安装程序(.exe)
"target": "nsis",
// 目标架构(仅 x64 位)
"arch": ["x64"]
}
]
},
// NSIS 安装程序配置
"nsis": {
// 是否一键安装(false 表示显示安装向导)
"oneClick": false,
// 是否为所有用户安装
"perMachine": true,
// 是否允许用户更改安装目录
"allowToChangeInstallationDirectory": true
},
// Electron 下载镜像配置(使用国内淘宝镜像加速下载)
"electronDownload": {
"mirror": "https://npmmirror.com/mirrors/electron/"
}
}
bash
# 执行打包
npm run build
4. bat 脚本启动前后端
bash
@echo off
chcp 65001 > nul
title kat-watermarking
set JAR_PATH=kat-watermarking.jar
set NGINX_PATH=nginx
set NGINX_EXEC=nginx.exe
set NGINX_URL=http://localhost:10086
set JDK_DIR=jdk-17.0.2
set JAVA_CMD="%JDK_DIR%\bin\java"
if errorlevel 1 (
echo 错误: bat 配置有误,请检查
pause
exit /b 1
)
echo 正在启动 Java 应用程序...
start "JavaApp" /min cmd /c "%JAVA_CMD% -jar %JAR_PATH% && pause"
if errorlevel 1 (
echo 错误: 启动 Java 应用程序失败
pause
exit /b 1
)
echo 正在启动 Nginx 服务器...
tasklist /fi "imagename eq nginx.exe" | find /i "nginx.exe" > nul
if not errorlevel 1 (
echo 检测到已有 Nginx 正在运行,正在终止旧进程...
taskkill /f /im nginx.exe > nul 2>&1
timeout /t 1 /nobreak > nul
)
cd "%NGINX_PATH%"
start "" "%NGINX_EXEC%"
if errorlevel 1 (
echo 错误: 启动 Nginx 服务器失败
pause
exit /b 1
)
cd ..
timeout /t 2 /nobreak > nul
echo 正在打开 Nginx 网页...
start "" "%NGINX_URL%"
if errorlevel 1 (
echo 警告: 无法打开浏览器,请手动访问 %NGINX_URL%
)
echo 所有服务已启动完成!
pause
taskkill /f /im nginx.exe > nul 2>&1
exit /b 0
其实没有什么技术含量,挑一下有用的讲讲吧:
@echo off
关闭命令本身的回显,只显示输出结果(即 echo 指明的字段)chcp 65001
设置控制台字符集为 utf-8> nul
重定向输出到 null(即不显示输出)if errorlevel 1 ()
检查上一个命令的退出代码, >=1 为执行错误tasklist /fi "字段 运算符 值"
列出进程列表并条件过滤|
管道符,用于将一个命令的输出传递给另一个命令作为输入find /i "字符串"
查找字符串,不区分大小写taskkill /f /im nginx.exe
/f
强制终止/im
按进程名称终止timeout /t 1 /nobreak
暂停脚本执行 1 秒钟/nobreak
禁止用户按键跳过等待
结语
事实证明困距离不得不睡觉有很长一段距离。
如果能习惯这个感觉还能正常工作,🤤