阅读 1223

使用 Electron 开发桌面应用

image.png

介绍

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
复制代码
包名版本作用
electron^9.2.1electron库
electron-builder^22.8.0打包库
electron-updater^4.1.2更新库
electron-is-dev2.0.0环境库

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配置项,添加打包相关配置。
主要有以下几个配置:

属性说明
appId应用id,一般默认为应用安装路径
windows下可以在 PowerShell中输入Get-StartApps查看应用id
compression应用打包压缩类型
nsis应用打包安装与卸载的相关配置
files应用打包所包含的文件范围
directories应用打包地址和输出地址
publish应用发布配置,目前主要作为应用更新地址来使用
protocols自定义协议,可根据协议名唤醒app
winwindows打包配置,生成exe文件
"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) {
  // myapp://?a=1&b=2
  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
  }
  */ 
}
复制代码

8. 文档参考

  1. 自动更新
  2. 自定义协议唤醒Electron应用
  3. electron主进程与渲染进程

9. 完整项目代码

github.com/zhuxingmin/…


南京三百云信息科技有限公司(车300)成立于2014年3月27日,是一家扎根于南京的移动互联网企业,目前坐落于南京、北京。经过7年积累,累计估值次数已达52亿次,获得了国内外多家优质投资机构青睐如红杉资本、上汽产业基金等。
三百云是国内优秀的以人工智能为依托、以汽车交易定价和汽车金融风控的标准化为核心产品的独立第三方的汽车交易与金融SaaS服务提供商。

欢迎加入三百云,一起见证汽车行业蓬勃发展,期待与您携手同行!
Java开发、Java实习、PHP实习、测试、测开、产品经理、大数据、算法实习,热招中...
官网:www.sanbaiyun.com/
投递简历:hr@che300.com,请注明来自掘金😁

文章分类
前端