使用 Electron 开发桌面应用
介绍
Electron,官方简介:使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。
出于个人爱好,接触到了Electron,并开始尝试开发一些本地小工具。
以下是对开发过程做的一个经验总结,便于回顾和交流。
使用
下面来构建一个简单的electron应用。
应用源码地址:github.com/zhuxingmin/…
1. 项目初始化
项目基于
create-react-app@3.3.0
搭建,执行命令生成项目
// 全局安装 create-react-app
npm install -g create-react-app
// 执行命令生成项目
create-react-app electronApp
// 安装依赖并启动项目
yarn && yarn start
此时启动的只是一个react应用,下一步安装
electron
electron-updater
electron-builder
electron-is-dev
等库
yarn add electron electron-updater electron-builder electron-is-dev
2. 配置package.json
安装完项目依赖后,在
package.json
中添加electron应用相关配置。
"version": "0.0.1" // 设置应用版本号
"productName": "appName" // 设置应用名称
"main": "main.js" // 设置应用入口文件
"homepage": "." // 设置应用根路径
在
scripts
中添加应用命令,启动以及打包。
"estart": "electron ." // 启动
"package-win": "electron-builder" // 打包 (此处以windows平台为例,故命名为package-win)
新增
build
配置项,添加打包相关配置。
主要有以下几个配置:
"build": {
// 自定义appId 一般以安装路径作为id windows下可以在 PowerShell中输入Get-StartApps查看应用id
"appId": "org.develar.zhuxingmin",
// 打包压缩 "store" | "normal"| "maximum"
"compression": "store",
// nsis安装配置
"nsis": {
"oneClick": false, // 一键安装
"allowToChangeInstallationDirectory": true, // 允许修改安装目录
// 下面这些配置不常用
"guid": "haha", // 注册表名字
"perMachine": true, // 是否开启安装时权限限制(此电脑或当前用户)
"allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
"installerIcon": "xxx.ico", // 安装图标
"uninstallerIcon": "xxx.ico", //卸载图标
"installerHeaderIcon": "xxx.ico", // 安装时头部图标
"createDesktopShortcut": true, // 创建桌面图标
"createStartMenuShortcut": true, // 创建开始菜单图标
"shortcutName": "lalala" // 图标名称
},
// 应用打包所包含文件
"files": [
"build/**/*",
"main.js",
"source/*",
"service/*",
"static/*",
"commands/*"
],
// 应用打包地址和输出地址
"directories": {
"app": "./",
"output": "dist"
},
// 发布配置 用于配合自动更新
"publish": [
{
// "generic" | "github"
"provider": "generic", // 静态资源服务器
"url": "http://你的服务器目录/latest.yml"
}
],
// 自定义协议 用于唤醒应用
"protocols": [
{
"name": "myProtocol",
"schemes": [
"myProtocol"
]
}
],
// windows打包配置
"win": {
"icon": "build/fav.ico",
// 运行权限
// "requireAdministrator" | "获取管理员权"
// "highestAvailable" | "最高可用权限"
"requestedExecutionLevel": "highestAvailable",
"target": [
{
"target": "nsis"
}
]
},
},
3. 编写入口文件 main.js
众所周知,基于react脚手架搭建的项目,入口文件为
index.js
,因此在上面配置完成后,我们想要启动electron应用,需要修改项目入口为main.js
- 首先在目录下新建
main.js
文件,并在package.json
文件中,修改应用入口字段main
的值为main.js
- 通过
electron
提供的BrowserWindow
,创建一个窗口实例mainWindow
- 通过
mainWindow
实例方法loadURL
, 加载静态资源 - 静态资源分两种加载方式:开发和生产;需要通过
electron-is-dev
判断当前环境;若是开发环境,可以开启调试入口,通过http://localhost:3000/
加载本地资源(react项目启动默认地址);若是生产环境,则要关闭调试入口,并通过本地路径找到项目入口文件index.html
大体代码如下
const { BrowserWindow } = require("electron");
const url = require("url");
const isDev = require('electron-is-dev');
mainWindow = new BrowserWindow({
width: 1200, // 初始宽度
height: 800, // 初始高度
minWidth: 1200,
minHeight: 675,
autoHideMenuBar: true, // 隐藏应用自带菜单栏
titleBarStyle: false, // 隐藏应用自带标题栏
resizable: true, // 允许窗口拉伸
frame: false, // 隐藏边框
transparent: true, // 背景透明
backgroundColor: "none", // 无背景色
show: false, // 默认不显示
hasShadow: false, // 应用无阴影
modal: true, // 该窗口是否为禁用父窗口的子窗口
webPreferences: {
devTools: isDev, // 是否开启调试功能
nodeIntegration: true, // 默认集成node环境
},
});
const config = dev
? "http://localhost:3000/"
: url.format({
pathname: path.join(__dirname, "./build/index.html"),
protocol: "file:",
slashes: true,
});
mainWindow.loadURL(config);
4. 项目启动
项目前置操作完成,运行上面配置的命令来启动electron应用
// 启动react应用,此时应用运行在"http://localhost:3000/"
yarn start
// 再启动electron应用,electron应用会在入口文件`main.js`中通过 mainWindow.loadURL(config) 来加载react应用
yarn estart
文件目录
至此,一个简单的electron应用已经启动,效果图如下(这是示例项目的截图)。
作为一个客户端应用,它的更新与我们的网页开发相比要显得稍微复杂一些,具体将会通过下面一个应用更新的例子来说明。
5. 应用更新
electron客户端的更新与网页不同,它需要先下载更新包到本地,然后通过覆盖源文件来达到更新效果。
首先第一步,安装依赖
yarn add electron-updater electron-builder
复制代码
应用通过
electron-updater
提供的api,去上文配置的服务器地址寻找并对比latest.yml
文件,如果版本号有更新,则开始下载资源,并返回下载进度相关信息。下载完成后可以自动也可以手动提示用户,应用有更新,请重启以完成更新 (更新是可以做到无感的,下载完更新包之后,可以不提示,下次启动客户端时会自动更新)
// 主进程
const { autoUpdater } = require("electron-updater");
const updateUrl = "应用所在的远程服务器目录"
const message = {
error: "检查更新出错",
checking: "正在检查更新……",
updateAva: "检测到新版本,正在下载……",
updateNotAva: "现在使用的就是最新版本,不用更新",
};
autoUpdater.setFeedURL(updateUrl);
autoUpdater.on("error", (error) => {
sendUpdateMessage("error", message.error);
});
autoUpdater.on("checking-for-update", () => {
sendUpdateMessage("checking-for-update", message.checking);
});
autoUpdater.on("update-available", (info) => {
sendUpdateMessage("update-available", message.updateAva);
});
autoUpdater.on("update-not-available", (info) => {
sendUpdateMessage("update-not-available", message.updateNotAva);
});
// 更新下载进度事件
autoUpdater.on("download-progress", (progressObj) => {
mainWindow.webContents.send("downloadProgress", progressObj);
});
autoUpdater.on("update-downloaded", function (
event,
releaseNotes,
releaseName,
releaseDate,
updateUrl,
quitAndUpdate
) {
ipcMain.on("isUpdateNow", (e, arg) => {
// 接收渲染进程的确认消息 退出应用并更新
autoUpdater.quitAndInstall();
});
//询问是否立即更新
mainWindow.webContents.send("isUpdateNow");
});
ipcMain.on("checkForUpdate", () => {
//检查是否有更新
autoUpdater.checkForUpdates();
});
function sendUpdateMessage(type, text) {
// 将更新的消息事件通知到渲染进程
mainWindow.webContents.send("message", { text, type });
}
// 渲染进程
const { ipcRenderer } = window.require("electron");
// 发送检查更新的请求
ipcRenderer.send("checkForUpdate");
// 设置检查更新的监听频道
// 监听检查更新事件
ipcRenderer.on("message", (event, data) => {
console.log(data)
});
// 监听下载进度
ipcRenderer.on("downloadProgress", (event, data) => {
console.log("downloadProgress: ", data);
});
// 监听是否可以开始更新
ipcRenderer.on("isUpdateNow", (event, data) => {
// 用户点击确定更新后,回传给主进程
ipcRenderer.send("isUpdateNow");
});
应用更新的主要步骤
- 在主进程中,通过api获取远程服务器上是否有更新包
- 对比更新包的版本号来确定是否更新
- 对比结果如需更新,则开始下载更新包并返回当前下载进度
- 下载完成后,开发者可选择自动提示还是手动提示或者不提醒(应用在下次启动时会自动更新)
上文演示了在页面上(渲染进程),是如何与主进程进行通信,让主进程去检查更新。
在实际使用中,如果我们需要用到后台的能力或者原生功能时,主进程与渲染进程的交互必不可少。
那么他们有哪些交互方式呢?
在看下面的代码片段之前,可以先了解一下electron主进程与渲染进程
简单来说就是,通过main.js
来执行的都属于主进程,其余皆为渲染进程。
6. 主进程与渲染进程间的常用交互方式
// 主进程中使用
const { ipcMain } = require("electron");
// 渲染进程中使用
const { ipcRenderer } = window.require("electron");
方式一
渲染进程 发送请求并监听回调频道
ipcRenderer.send(channel, someRequestParams);
ipcRenderer.on(`${channel}-reply`, (event, result)=>{
// 接收到主进程返回的result
})
主进程 监听请求并返回结果
ipcMain.on(channel, (event, someRequestParams) => {
// 根据someRequestParams,经过操作后得到result
event.reply(`${channel}-reply`, result)
})
方式二
渲染进程
const result = await ipcRenderer.invoke(channel, someRequestParams);
主进程:
ipcMain.handle(channel, (event, someRequestParams) => {
// 根据someRequestParams,经过操作后得到result
return result
});
方式三
以上两种方式均为渲染进程通知主进程, 第三种是主进程通知渲染进程
主进程
/*
* 使用`BrowserWindow`初始化的实例`mainWindow`
*/
mainWindow.webContents.send(channel, something)
渲染进程
ipcRenderer.on(channel, (event, something) => {
// do something
})
上文的应用更新用的就是方式一
。
还有其它通讯方式postMessage, sendTo
等,可以根据具体场景决定使用何种方式。
7. 应用唤醒(与其他应用联动)
electron应用除了双击图标运行之外,还可以通过协议链接启动(浏览器地址栏或者命令行)。这使得我们可以在网页或者其他应用中,以链接的形式唤醒该应用。链接可以携带参数 例:zhuxingmin://?a=1&b=2&c=3
‘自定义协议名:zhuxingmin’ ‘参数:a=1&b=2&c=3’。
我们可以通过参数,来使应用跳转到某一页或者让应用做一些功能性动作等等。
const path = require('path');
const { app } = require('electron');
// 获取单实例锁
const gotTheLock = app.requestSingleInstanceLock();
// 如果获取失败,证明已有实例在运行,直接退出
if (!gotTheLock) {
app.quit();
}
const args = [];
// 如果是开发环境,需要脚本的绝对路径加入参数中
if (!app.isPackaged) {
args.push(path.resolve(process.argv[1]));
}
// 加一个 `--` 以确保后面的参数不被 Electron 处理
args.push('--');
const PROTOCOL = 'zhuxingmin';
// 设置自定义协议
app.setAsDefaultProtocolClient(PROTOCOL, process.execPath, args);
// 如果打开协议时,没有其他实例,则当前实例当做主实例,处理参数
handleArgv(process.argv);
// 其他实例启动时,主实例会通过 second-instance 事件接收其他实例的启动参数 `argv`
app.on('second-instance', (event, argv) => {
if (process.platform === 'win32') {
// Windows 下通过协议URL启动时,URL会作为参数,所以需要在这个事件里处理
handleArgv(argv);
}
});
// macOS 下通过协议URL启动时,主实例会通过 open-url 事件接收这个 URL
app.on('open-url', (event, urlStr) => {
handleUrl(urlStr);
});
function handleArgv(argv) {
const prefix = `${PROTOCOL}:`;
const offset = app.isPackaged ? 1 : 2;
const url = argv.find((arg, i) => i >= offset && arg.startsWith(prefix));
if (url) handleUrl(url);
}
function handleUrl(urlStr) {
//
let paramArr = urlStr.split("?")[1].split("&");
const params = {};
paramArr.forEach((item) => {
if (item) {
const [key, value] = item.split("=");
params[key] = value;
}
});
/**
{
a: 1,
b: 2
}
*/
}
链接:https://juejin.cn/post/6995077640566603789