注册

使用 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配置项,添加打包相关配置。

主要有以下几个配置:6128c2e4c4d866b0924511241ce8bff0.png


"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




  1. 首先在目录下新建main.js文件,并在package.json文件中,修改应用入口字段main的值为main.js
  2. 通过electron提供的BrowserWindow,创建一个窗口实例mainWindow
  3. 通过mainWindow实例方法loadURL, 加载静态资源
  4. 静态资源分两种加载方式:开发和生产;需要通过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");
});


应用更新的主要步骤




  1. 在主进程中,通过api获取远程服务器上是否有更新包
  2. 对比更新包的版本号来确定是否更新
  3. 对比结果如需更新,则开始下载更新包并返回当前下载进度
  4. 下载完成后,开发者可选择自动提示还是手动提示或者不提醒(应用在下次启动时会自动更新)


上文演示了在页面上(渲染进程),是如何与主进程进行通信,让主进程去检查更新。

在实际使用中,如果我们需要用到后台的能力或者原生功能时,主进程与渲染进程的交互必不可少。

那么他们有哪些交互方式呢?



在看下面的代码片段之前,可以先了解一下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

0 个评论

要回复文章请先登录注册