Electron安装
安装问题
npm或者yarn安装electron就算是配置了淘宝源还是会出现超时。所以我的解决方案是安装cnpm,使用cnpm去安装。
全局安装cnpm
npm i cnpm -G
复制代码
新建项目
cnpm init // 一路Enter然后到最后一步输入yes
// 安装dev相关依赖
cnpm i electron -D //安装electron
cnpm i electron-builder -D // 用来打包客户端安装包 -- 需要下一步下一步安装来完成点击打开
cnpm i electron-packager -D // 用来打包客户端可执行文件 -- 直接点击打包📦后的可执行文件即可运行
// 安装生产相关依赖
cnpm i electron-log // 用于调试时的log输出,dev环境会直接在终端打印日志同时会在项目跟目录的logs文件夹生成log
cnpm i electron-updater //用户项目自动更新
cnpm i express // 因为使用的是history路由模式所以我们使用node来启动前端项目
cnpm i http-proxy-middleware // 用于代理前端项目访问服务器接口
复制代码
相关依赖的版本如下
生产
"electron-log": "^4.4.8",
"electron-updater": "^5.0.5",
"express": "^4.18.1",
"http-proxy-middleware": "^2.0.6"
复制代码
开发
"electron": "^19.0.6",
"electron-builder": "^23.1.0",
"electron-packager": "^15.5.1"
复制代码
项目架构详解
├── build // 用于存放前端打包后的文件
├── desk // 用于存放打包后的exe安装文件或者dmg
├── logs // 用于存放项目调试log文件
├── main.js // electron的主进程文件
├── media // 项目的多媒体文件诸如.mp3 .mp4 .ico .icns文件
├── node_modules // 项目依赖
├── package.json // 配置文件
├── preload.js
├── renderer.js
└── server // 需要打包进项目的后端可执行文件
复制代码
关于preload.js 和 renders.js的详解
话说,在传统的electron程序中,大量的逻辑是写在renderer.js文件中的。但是,后来随着electron的版本发展,逐渐出来了一种呼声:就是要将node能力从renderer.js中分离出来。让renderer.js回归传统js的功能。这个时候,出现的新概念就是preload.js。
本文的测试环境:electron@13.0.1,win10。本文探讨preload.js在browserWindow中的应用,当然,preload.js在webview中也有使用到。但是暂时不在本文的讨论范围内。本文主要命题是:preload.js的作用范围,以及如何区分当前作用的页面。
原文链接
项目启动
首先配置package.json文件的main字段为项目中的main.js
配置script字段添加如下
"start": "chcp 65001 && electron .", // chcp 65001是为了解决Windows平台在启动后答应的log中文乱码问题
"macpack": "electron-builder build --mac", // 用于打包dmg安装包
"winpack": "electron-builder build --win" // 用于打包exe安装包
复制代码
在electron启动前端项目
首先需要将打包📦后的前端代码放到项目build文件夹下,注意是放到build文件夹根目录而不是将诸如dist(vue打包后)或者build(react打包后)文件直接拷贝到项目的build文件夹。build文件夹下的文件目录如果是react就应该如下
├── asset-manifest.json
├── favicon.ico
├── files
├── index.html
├── manifest.json
├── robots.txt
└── static
复制代码
开始编写main.js
直接贴出代码如下
const { app, BrowserWindow, Menu, dialog } = require('electron');
const path = require('path');
const isDev = !app.isPackaged;
const cp = require('child_process');
const { createProxyMiddleware } = require('http-proxy-middleware');
const express = require('express');
const application = express();
const START_PORT = 50001;
const DOMAIN = 'http://xxx';
const enviroment = process.platform == 'darwin' ? 'mac' : 'win';
const log = require('electron-log');
// 获取项目资源目录注意区分打包前和打包后的区别
const appPath = app.isPackaged
? path.dirname(app.getPath('exe')) // 打包后
: app.getAppPath(); // 打包前
const { autoUpdater } = require('electron-updater');
if (isDev) {
// 判断如果是dev环境就将log存储在项目根目录的logs文件夹
log.transports.file.resolvePath = () =>
path.join(__dirname, `logs/${new Date().toLocaleDateString()}.log`);
}
// 设置log日志的格式可以去electron-log官方文档查看更多格式化
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}';
let localServer; // node服务的实例,这里定义是为了后面方便在关闭窗口的时候杀掉它
function createWindow() {
// 主进程开启一个尺寸为1920*1000的窗口
const mainWindow = new BrowserWindow({
width: 1920,
height: 1000,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
// 生命一个meu
const menu = [
{
label: '帮助',
submenu: [
{
label: '控制台',
click: () => {
mainWindow.webContents.openDevTools({ mode: 'bottom' });
},
},
{
label: '检查更新',
click: () => {
autoUpdater.checkForUpdates();
},
},
{
label: '关于',
click: () => {
dialog.showMessageBoxSync({
title: '关于',
message: `${app.getName()}V${app.getVersion()}`,
type: 'info',
icon: path.resolve(
__dirname,
'media/images/logo.png'
),
buttons: ['好的'],
});
},
},
],
},
];
// 启动一个node服务也就是用node部署打包后的文件
proxys().then((res) => {
const m = Menu.buildFromTemplate(menu);
// 设置顶部菜单
Menu.setApplicationMenu(m);
// 窗口显示我们部属的前端项目
mainWindow.loadURL(`http://127.0.0.1:${START_PORT}`);
// 判断如果是dev环境将devTool打开
isDev && mainWindow.webContents.openDevTools();
});
// 启动后端服务
startServer();
}
function checkUpdate() {
if (enviroment === 'win') {
// 本地模拟更新的端口
autoUpdater.setFeedURL('http://127.0.0.1:9005/win32');
} else {
// mac系統更新
}
autoUpdater.checkForUpdates();
//监听'error'事件
autoUpdater.on('error', (err) => {
logMsg(`autoUpdater错误${err}`);
});
//监听'update-available'事件,发现有新版本时触发
autoUpdater.on('update-available', () => {
logMsg('发现更新-----------------------------');
});
autoUpdater.on('update-not-available', () => {
dialog
.showMessageBox({
type: 'info',
title: '应用更新',
message: '未发现新版本'
})
})
//监听'update-downloaded'事件,新版本下载完成时触发
autoUpdater.on('update-downloaded', () => {
// 如果有更新提示用户并后台下载安装
dialog
.showMessageBox({
type: 'info',
title: '应用更新',
message: '发现新版本,是否更新?',
buttons: ['是', '否'],
})
.then((buttonIndex) => {
if (buttonIndex.response == 0) {
//选择是,则退出程序,安装新版本
autoUpdater.quitAndInstall();
app.quit();
}
});
});
}
function logMsg(msg) {
log.info(msg);
}
function startServer() {
// 启动后台打包后的可执行文件
logMsg('开始执行-----------------------------');
let shellCode;
if (enviroment === 'win') {
logMsg(`程序安装目录: ${appPath}`);
// serverPath = path.resolve(__dirname, 'server/python');
const serverPathSplit = appPath.split(':');
shellCode = `${serverPathSplit[0]}: && cd ${serverPathSplit[1]}${
isDev ? '' : '\\resources'
}\\server\\python && ${enviroment === 'win' ? 'main.exe' : 'test'}`;
logMsg(`即将执行脚本:${shellCode}`);
}
// 子进程运行后端可执行文件
cp.exec(shellCode, (error, stdout, stderr) => {
if (error) {
logMsg(`脚本执行错误: ${error}`);
return;
}
logMsg('执行成功');
logMsg(`stdout: ${stdout}`);
log.error(`stderr: ${stderr}`);
});
logMsg('结束执行-----------------------------');
}
function proxys() {
return new Promise((resolve, reject) => {
application.use(
createProxyMiddleware('/api', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/v1', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/icons', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/apks', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/zip', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/img_avatar', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/screenshot', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/data', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/android', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/ipa_icons', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/ipas', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/admin', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/ws', {
target: DOMAIN,
changeOrigin: true,
secure: false,
})
);
application.use(
createProxyMiddleware('/desktop', {
target: 'http://127.0.0.1:29096',
changeOrigin: true,
secure: false,
})
);
// 这一步是用户前端项目是history路由比如写的相关配置
application.use(express.static(path.resolve(__dirname, 'build')));
application.get('*', function (request, response) {
response.sendFile(path.resolve(__dirname, 'build', 'index.html'));
});
localServer = application.listen(START_PORT, () => {
resolve();
});
});
}
app.whenReady().then(() => {
createWindow();
// 判断窗口ready之后检测更新
checkUpdate();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', function () {
// 关闭窗口之后需要杀掉node启动的服务
localServer.close();
logMsg(`node服务已中止---------------------`);
if (enviroment === 'win') {
cp.exec(`taskkill /f /t /im main.exe`, (error, stdout, stderr) => {
if (error) {
logMsg(`杀死进程执行错误: ${error}`);
return;
}
logMsg(`stdout: ${stdout}`);
log.error(`stderr: ${stderr}`);
logMsg('后台服务程序已被杀死---------------------');
});
}
if (process.platform !== 'darwin') app.quit();
});
复制代码
关于package.json的编写
由于使用的是electron-builder故可以去到该插件官网查看相关字段的文档。由于业务要求我们只需要打包.exe所以以下是关于打包exe应用的相关配置。
以下我用到的字段我尽量注释写出来
"build": {
"appId": "9928c2b60725cde286468f0696df8b30",
"productName": "打包后的应用名称",
"icon": "./media/images/logo.png", // 打包后的应用logo
"asar": true, // 是否使用asar加密源码
"nsis": {
"oneClick": false, // 是否一键安装
"allowElevation": true,
"allowToChangeInstallationDirectory": true, // 是否可以自定义安装目录
"installerIcon": "./media/images/app.ico", // 安装时候的icon图标,注意图标格式是.con
"uninstallerIcon": "./media/images/app.ico", // 卸载时候的icon图标
"installerHeaderIcon": "./media/images/app.ico", // 安装时候的头icon
"createDesktopShortcut": true, // 是否创建桌面快捷方式
"createStartMenuShortcut": true,
"shortcutName": "星源",
"include": "script/installer.nsh" // 安装完成执行的nsh脚本
},
"directories": {
"output": "desk/win" // 打包完成输出的目录如果该目录不存在会帮你在当前项目创建
},
"files": [ // 需要打包的文件
"main.js",
"build",
"preload.js",
"media",
"script",
"package.json",
"server"
],
"extraResources": [ // 不需要打包的额外资源,比我我这里就存放了后端的可执行.exe文件
{
"from": "server",
"to": "server"
}
],
"publish": { // 自动更新--这里后面会讲到
"provider": "generic",
"url": "http://127.0.0.1:9005/"
},
"win": {
"icon": "media/images/logo.png",
"target": [
{
"target": "nsis",
"arch": [
"ia32"
]
}
]
}
}
复制代码
关于自动更新
如何编写自动更新的配置
先说明使用到的依赖是electron-updater点击查看官方文档
上文中main.js文件中的如下代码块的作用就是用来自动更新的, 如下代码注释都写了出来
function checkUpdate() {
if (enviroment === 'win') {
// 本地模拟更新的端口
autoUpdater.setFeedURL('http://127.0.0.1:9005/win32');
} else {
// mac系統更新
}
autoUpdater.checkForUpdates();
//监听'error'事件
autoUpdater.on('error', (err) => {
logMsg(`autoUpdater错误${err}`);
});
//监听'update-available'事件,发现有新版本时触发
autoUpdater.on('update-available', () => {
logMsg('发现更新-----------------------------');
});
autoUpdater.on('update-not-available', () => {
dialog
.showMessageBox({
type: 'info',
title: '应用更新',
message: '未发现新版本'
})
})
//监听'update-downloaded'事件,新版本下载完成时触发
autoUpdater.on('update-downloaded', () => {
// 如果有更新提示用户并后台下载安装
dialog
.showMessageBox({
type: 'info',
title: '应用更新',
message: '发现新版本,是否更新?',
buttons: ['是', '否'],
})
.then((buttonIndex) => {
if (buttonIndex.response == 0) {
//选择是,则退出程序,安装新版本
autoUpdater.quitAndInstall();
app.quit();
}
});
});
}
复制代码
搭建本地发布平台
我自己搭建的一个本地更新服务使用node写的 仓库项目地址
该代码的使用如下
首先在项目根目录创建static文件夹,理论上该目录下📁内容如下
├── builder-debug.yml
├── builder-effective-config.yaml
├── latest.yml
├── win-ia32-unpacked
├── �\230\237�\220�\214�\235��\211\210\ Setup\ 1.0.0.exe
└── �\230\237�\220�\214�\235��\211\210\ Setup\ 1.0.0.exe.blockmap
复制代码
将打包后的exe以及一堆相关配置文件丢到该目录
启动项目npm run start
Electron项目的package.json配置如下
"publish": { // 自动更新--这里后面会讲到
"provider": "generic",
"url": "http://127.0.0.1:9005/"
},
复制代码
遇到的问题
前端项目dev环境启动可以正常看,但是打包之后一直报css/js路径加载问题。打包后的代码的路径指定
// 这一步是用户前端项目是history路由比如写的相关配置
application.use(express.static(path.resolve(__dirname, 'build'))); // 这里一定要使用path来resole到当前打包目录的根目录要不然会出现资源加载问题
application.get('*', function (request, response) {
response.sendFile(path.resolve(__dirname, 'build', 'index.html'));
});
复制代码
打包出来会出现有些包找不到。解决方案是如果你确定你在打包后需要用到的包,在使用cnpm安装的时候不要加-D后缀,即使该包变成项目依赖而非开发环境依赖。因为打包会打包dependencies而不会打包devDependencies
打包的时候会出现打包出错,记得认真查看终端错误日志。我遇到的就是icon的格式不对。注意以下三个字段的文件格式是ico而非png
"installerIcon": "./media/images/app.ico",
"uninstallerIcon": "./media/images/app.ico",
"installerHeaderIcon": "./media/images/app.ico",
复制代码
启动后台给到的可执行文件实现不联网本地数据入库。
"extraResources": [
{
"from": "server",
"to": "server"
}
],
复制代码
如上我将后端给到的可执行文件放在项目的server目录,然后使用extraResources字段将打包后的文件放到了server目录。
在本地和打包后的路径会有很大出入。使用app.isPackaged判断是否是打包后。如下来获取该目录正确地址来执行后端打包后的可执行文件。
const appPath = app.isPackaged
? path.dirname(app.getPath('exe')) // 打包后
: app.getAppPath(); // 打包前