Electron + Vue3 开发桌面应用


相关文档

Vite: https://vitejs.cn
Vue : https://cn.vuejs.org
Electron: https://www.electronjs.org/
electron-builder: https://github.com/electron-userland/electron-builder
concurrently: https://github.com/open-cli-tools/concurrently

示例环境

电脑: MacBook Pro (13-inch, M1, 2020)
系统: macOS 11.6
Node 版本: v16.14.0
npm 版本: v7.1.2
Electron 版本: v20.1.0
Vue 版本: v3.2.37


一、搭建项目

1、创建 Vue 项目

参考:https://cn.vuejs.org/guide/quick-start.html

确保你安装了最新版本的 Node.js,然后在命令行中运行以下命令:

npm init vue@latest 

这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示,本项目只做示例,所以只选了 Vue Router

✔ Project name: … electron_vue3_test
✔ Add TypeScript? … No
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add Cypress for both Unit and End-to-End testing? … No
✔ Add ESLint for code quality? … No
✔ Add Prettier for code formatting? … No

在项目被创建后,通过以下步骤安装依赖并启动开发服务器:

cd <your-project-name>
npm install
npm run dev

执行后终端显示如下:

  VITE v3.1.2  ready in 333 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

访问 http://localhost:5173/ 显示页面如下所示,Vue 项目创建成功

在这里插入图片描述

如果想要修改端口号,修改 vite.config.js 文件的 server.port 选项为你想要的端口号,这里我改成了 3004

export default defineConfig({
  // ...
  server: {
    port: 3004
  }
});

2、安装 Electron

参考:https://www.electronjs.org/zh/docs/latest/tutorial/installation

执行以下命令安装 Electron

npm install electron --save-dev

如果安装过程中卡住,可尝试设置以下环境变量

ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"

mac 修改环境变量的方法:
终端 中输入 vim ~/.zshrc 打开文件
i 进入编辑模式
添加一行 export ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
esc 退出编辑模式
输入 :wq 保存并退出
终端 输入 source ~/.zshrc 使刚才的修改生效

3、运行 Electron

参考:https://www.electronjs.org/zh/docs/latest/tutorial/quick-start

在根目录创建 electron 文件夹,新建 index.html 文件,添加如下代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'"
    />
    <meta
      http-equiv="X-Content-Security-Policy"
      content="default-src 'self'; script-src 'self'"
    />
    <title>Hello from Electron renderer!</title>
  </head>
  <body>
    <h1>Hello from Electron renderer!</h1>
    <p>👋</p>
  </body>
</html>

electron 目录下新建文件 main.js,并添加如下代码:

const { app, BrowserWindow } = require('electron')
const path = require("path")

const createWindow = () => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
  })
  
  // 使用 loadFile 加载 electron/index.html 文件
  mainWindow.loadFile(path.join(__dirname, "./index.html"));
}

// 在应用准备就绪时调用函数
app.whenReady().then(() => {
  createWindow()
})

修改 package.json 文件,指定 electron/main.jsElectron 的入口文件,并添加 electron:dev 命令以开发模式运行 Electron

{
  "main": "electron/main.js",
  "scripts": {
    "electron:dev": "electron ."
  }
}

然后打开终端,执行 npm run electron:dev ,显示窗口如下所示,Electron 运行成功

在这里插入图片描述

4、Vue 和 Electron 结合

删除 electron/index.html 文件,修改 electron/main.js 文件代码如下:

const { app, BrowserWindow } = require('electron')
// const path = require("path")

const createWindow = () => {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
  })

  // 主要改了这里
  // mainWindow.loadFile(path.join(__dirname, "./index.html"));
  // 使用 loadURL 加载 http://localhost:3004 ,也就是我们刚才创建的 Vue 项目地址
  // 3004 改为你 Vue 项目的端口号
  mainWindow.loadURL("http://localhost:3004/");
}

app.whenReady().then(() => {
  createWindow()
})

有时开发需要,会使用 nginx 代理为线上地址,则将 loadURL 的参数改为代理的地址,并在 electron/main.js 中添加如下代码:

参考:https://stackoverflow.com/questions/49637026/mainwindow-loadurlhttps-localhost3000-show-white-screen-on-electron-app/49673272

// 证书的链接验证失败时,触发该事件
app.on(
  "certificate-error",
  function (event, webContents, url, error, certificate, callback) {
    event.preventDefault();
    callback(true);
  }
);

打开终端,执行 npm run dev 启动 Vue 项目,再新建一个终端,执行 npm run electron:dev 启动 Electron ,即可成功启动,显示如下:

在这里插入图片描述

这样每次启动项目需要分别执行两个命令,有些麻烦,我们使用 concurrently把它们合并成一个命令。

参考:https://github.com/open-cli-tools/concurrently

执行以下命令,安装 concurrently

 npm install concurrently --save-dev

修改 package.json 文件中的 electron:dev 命令,同时执行 viteelectron . 两个命令,用引号将单独的命令括起来,并使用 \ 转义引号,代码如下:

// package.json
{
  "scripts": {
    "electron:dev": "concurrently vite \"electron .\""
  }
}

重新执行 npm run electron:dev ,即可成功显示。

5、其它配置

以下是开发环境下,我的一些其它配置,可供参考:

// electron/main.js
const { app, BrowserWindow, Menu, screen } = require("electron");
const path = require("path");

// 是否是生产环境
const isPackaged = app.isPackaged;

// 禁止显示默认菜单
Menu.setApplicationMenu(null);

// 主窗口
let mainWindow;

const createWindow = () => {
  // 创建浏览器窗口
  mainWindow = new BrowserWindow({
    // 默认窗口标题,如果由loadURL()加载的HTML文件中含有标签<title>,此属性将被忽略。
    title: "Electron + Vue3",
    // width: 800,
    // height: 600,
    // 设置窗口尺寸为屏幕工作区尺寸
    width: screen.getPrimaryDisplay().workAreaSize.width,
    height: screen.getPrimaryDisplay().workAreaSize.height,
    // 设置最小尺寸
    minWidth: 800,
    minHeight: 600,
    // 窗口图标。 在 Windows 上推荐使用 ICO 图标来获得最佳的视觉效果, 默认使用可执行文件的图标.
    // 在根目录中新建 build 文件夹存放图标等文件
    icon: path.resolve(__dirname, "../build/icon.ico"),
  });

  // 开发环境下,打开开发者工具。
  if (!isPackaged) {
    mainWindow.webContents.openDevTools();
  }

  // 使用 loadURL 加载 http://localhost:3004 ,也就是我们刚才创建的 Vue 项目地址
  // 3004 改为你 Vue 项目的端口号
  // mainWindow.loadURL("http://localhost:3004/");
  // 如果使用了 nginx 代理,url 改为代理地址
  mainWindow.loadURL("https://example.com/");
};

// 在应用准备就绪时调用函数
app.whenReady().then(() => {
  createWindow();

  app.on("activate", () => {
    // 通常在 macOS 上,当点击 dock 中的应用程序图标时,如果没有其他
    // 打开的窗口,那么程序会重新创建一个窗口。
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// 除了 macOS 外,当所有窗口都被关闭的时候退出程序。 因此,通常对程序和它们在任务栏上的图标来说,应当保持活跃状态,直到用户使用 Cmd + Q 退出。
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});

// 如果开发环境使用了 nginx 代理,禁止证书报错
if (!isPackaged) {
  // 证书的链接验证失败时,触发该事件
  app.on(
    "certificate-error",
    function (event, webContents, url, error, certificate, callback) {
      event.preventDefault();
      callback(true);
    }
  );
}

6、其它问题

(1)热更新失效、控制台报错

我使用了 nginx 代理,开发时出现了热更新失效的问题,并且控制台报了如下错误

WebSocket connection to 'wss://example.com/' failed: 
WebSocket connection to 'wss://localhost:3004/' failed: 
[vite] failed to connect to websocket.
your current setup:
  (browser) example.com/ <--[HTTP]--> localhost:3004/ (server)
  (browser) example.com:/ <--[WebSocket (failing)]--> localhost:3004/ (server)
Check out your Vite / network configuration and https://vitejs.dev/config/server-options.html#server-hmr 

已经通过在 nginx 的配置文件中添加如下代码解决:

参考:https://blog.csdn.net/weixin_39621860/article/details/111490058

server {
    # ...
    location / {
        # ...
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

二、打包

1、安装 electron-builder

参考:https://www.electron.build/#installation

打开终端,执行一下命令安装:
使用 yarn (官方推荐):

yarn add electron-builder --dev

或是 npm :

npm install electron-builder --save-dev

2、打包配置

(1)修改公共基础路径

修改 vite.config.js 中的 base 选项为 “./”,如下所示:

export default defineConfig({
  base: "./"
  // ...
});

(2)修改 Electron 配置

修改 electron/main.js 文件中 mainWindow.loadURL 的参数为 vue 打包后的文件路径

// electron/main.js
const path = require("path");
const createWindow = () => {
  //...
  mainWindow.loadURL(`file://${path.join(__dirname, "../dist/index.html")}`);
};

(3)打包配置

package.json 中添加如下配置,可根据需求调整:

参考:
https://www.electron.build/#quick-setup-guide
https://docs.npmjs.com/cli/v9/configuring-npm/package-json?v=true

{
  "name": "electron_vue3_test",
  "description": "Electron + Vue3 开发桌面应用",
  "author": "喵喵喵",
  "version": "1.0.0",
  "scripts": {
    // ...
    "electron:build": "npm run build && electron-builder",
    "postinstall": "electron-builder install-app-deps"
  },
  // ...
  "build":{
    "appId": "your.id",
    "productName": "Electron + Vue3 开发桌面应用",
    "copyright": "Copyright © year ${author}",
    "directories": {
      "output": "app_client"
    },
    "mac": {
      "category": "public.app-category.music",
      "icon": "build/icon.icns",
      "target": [
        {
          "target": "dmg",
          "arch": [
            "x64"
          ]
        },
        {
          "target": "zip",
          "arch": [
            "x64"
          ]
        }
      ]
    },
    "win": {
      "icon": "build/icon.ico",
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64",
            "ia32"
          ]
        }
      ]
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "installerIcon": "build/icon.ico",
      "uninstallerIcon": "build/icon.ico"
    }
  }
}

(3)打包

打开终端,执行 npm run electron:build 打包,打包后的应用程序在 app_client 目录下,.dmg 文件为 mac 的安装包,如下所示:
在这里插入图片描述

3、其它问题

(1)web和客户端同时部署

如果要一套代码部署在web端和客户端,还需要再做些处理。

在根目录下新增以下环境变量文件:
.env.development 文件,web 端开发环境配置,文件内容如下:

VITE_PLATFORM = 'web'

.env.production 文件,web 端生产环境配置,文件内容如下:

VITE_PLATFORM = 'web'

.env.electron_production 文件,客户端生产环境配置,文件内容如下:

VITE_PLATFORM = 'electron'

修改 package.json 文件 scripts 下的 electron:build 命令为 vite build --mode electron_production && electron-builder

修改 vite.config.js 文件如下:

import { defineConfig, loadEnv } from "vite";

export default ({ mode }) => {
  const isWeb = loadEnv(mode, process.cwd()).VITE_PLATFORM === "web";

  const config = {
    base: isWeb ? "/" : "./",
    // ...
  };
  return defineConfig(config);
};

如果图片、css等资源文件需上传cdn,vite.config.js 文件还要增加如下代码:

// ...
export default ({ mode }) => {
  const isWeb = loadEnv(mode, process.cwd()).VITE_PLATFORM === "web";
  const config = {
  // ...
  }
  if (isWeb) {
    config.experimental = {
      renderBuiltUrl(filename: string, { type }: { type: "public" | "asset" }) {
        if (type === "asset") {
          return "https://example.com/" + filename;
        }
      },
    };
  }
  return defineConfig(config);
};

(2)刷新页面后报错问题

我的方法是监听加载失败和导航事件,使用 loadURL 重新加载页面,并禁止刷新页面的快捷键。

electron/main.js 中判断如果是生产环境则做处理:

// electron/main.js
const { app, BrowserWindow } = require("electron");

const isPackaged = app.isPackaged;
let mainWindow;

const createWindow = () => {
  mainWindow = new BrowserWindow({
  // ...
  });

  // 加载文件
  function load() {
    mainWindow.loadURL(
      isPackaged
        ? `file://${path.join(__dirname, "../dist/index.html")}`
        : "https://example.com/"
    );
  }

  if (isPackaged) {
    // 生产环境下,load 的是 html 文件,要做特殊处理。
    // 加载失败之后触发
    mainWindow.webContents.on("did-fail-load", () => {
      load();
    });

    // 当用户或页面想要导航时触发。
    // 它可能发生在 window.location 对象改变或用户点击页面上的链接时,可能会发生这种情况。
    // 当使用如 webContents.loadURL 和 webContents.back APIs 以编程方式导航时,将不会触发此事件。
    // 页面内导航也不会触发,例如点击锚点或更新 window.location.hash。 可使用 did-navigate-in-page 事件。
    mainWindow.webContents.on("will-navigate", (event, url) => {
      event.preventDefault();
      load();
    });
    
    // 禁止使用快捷键刷新
    mainWindow.webContents.on("before-input-event", (event, input) => {
      mainWindow.webContents.setIgnoreMenuShortcuts(
        input.key.toLowerCase() === "f5" ||
        (input.control && input.key.toLowerCase() === "r") ||
        (input.meta && input.key.toLowerCase() === "r")
      );
    });
  }

  load();
};

(3)不带协议的 url 被解析为 file 协议问题

项目接口中有很多不带协议名的 url 链接,应解析为 https 协议,但打包后被解析为 file 协议,在 onBeforeRequest 方法中对 url 做处理:

// electron/main.js
const { app, BrowserWindow } = require("electron");

const isPackaged = app.isPackaged;
let mainWindow;

const createWindow = () => {
  mainWindow = new BrowserWindow({
  // ...
  });

  if (isPackaged) {
    // 将 file://example.com/xxx 的 文件改为 https 协议
    session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
      if (/^file:\/\/example\.com\//.test(details.url)) {
        callback({ redirectURL: details.url.replace(/^file/, "https") });
      } else {
        callback(details);
      }
    });
  }
};

(4)存取 cookie 问题

网站有些功能用到了 cookie ,但打包后的应用程序读取的 html 文件为 file 协议,存取 cookie 会有问题。

第一问题是请求接口时获取 cookie 和接收服务端响应时设置 cookie 的问题,我解决的办法是在 onBeforeSendHeadersonHeadersReceived 方法中对 cookie 做处理,在网站域名下存取 cookie

electron 目录下新建 utils.js 文件,内容如下:

const { session } = require("electron");

// 将响应头中的 set-cookie 转成 set() 方法需要的 details 对象
function getCookieDetails(cookie) {
  const details = {
    url: "https://example.com/",
  };
  cookie = cookie.split("; ");
  cookie.forEach((item, index) => {
    item = item.split("=");
    if (index === 0) {
      details.name = item[0];
      details.value = item[1];
    } else if (["domain", "path"].includes(item[0])) {
      details[item[0]] = item[1];
    } else if (item[0] == "expires") {
      details.expirationDate = new Date(item[1]).getTime() / 1000;
    } else if (item[0] == "HttpOnly") {
      details.httpOnly = true;
    }
    // HttpOnly 没有 item[1]  value 设为 true
  });
  return details;
}

module.exports = {
  // 是否是本站接口
  isWeapi: /^https:\/\/example\.com\/api\//,
  // 设置 cookies
  setCookies: function (cookies) {
    const setCookiePromise = [];

    cookies.forEach((cookie) => {
      const details = getCookieDetails(cookie);
      setCookiePromise.push(session.defaultSession.cookies.set(details));
    });

    return Promise.all(setCookiePromise);
  },

  // 获取 cookies,传给后端
  getCookies: async function () {
    const cookies = await session.defaultSession.cookies.get({
      domain: ".example.com"
    });
    const cookiesArr = [];
    cookies.forEach((cookie) => {
      cookiesArr.push(`${cookie.name}=${cookie.value}`);
    });
    return cookiesArr.join("; ");
  },
};

electron/main.js 中添加如下代码:

const { app, BrowserWindow, session } = require("electron");
const { isWeapi, setCookies, getCookies } = require("./utils.js");

const isPackaged = app.isPackaged;
let mainWindow;

const createWindow = () => {
  mainWindow = new BrowserWindow({
  // ...
  });

  if (isPackaged) {
    // 请求接口时,如果是本站接口,则获取 .example.com 中的 cookie,添加到请求头中
    session.defaultSession.webRequest.onBeforeSendHeaders(
      async (details, callback) => {
        if (isWeapi.test(details.url)) {
          const cookies = await getCookies();
          if (cookies) details.requestHeaders.cookie = cookies;
        }
        callback({ requestHeaders: details.requestHeaders });
      }
    );

    // 接受到服务端响应时,如果是本站接口,则获取响应头中的 set-cookie,添加到 .example.com 的 cookie 中
    session.defaultSession.webRequest.onHeadersReceived(
      async (details, callback) => {
        if (
          isWeapi.test(details.url) &&
          "responseHeaders" in details &&
          "set-cookie" in details.responseHeaders
        ) {
          await setCookies(details.responseHeaders["set-cookie"]);
        }
        callback({ responseHeaders: details.responseHeaders });
      }
    );
  }
};

第二个问题是 document.cookie 获取不到,我在项目中使用的是 js-cookie 这个插件,我的解决方案是封装一个 Cookie对象,根据当前的协议类型判断使用哪种方法,如果是 file 协议,具体代码如下:

// TODO: 待更新

(5)限制只能打开一个窗口

使用 requestSingleInstanceLock 方法判断是否已有实例在运行,并监听 second-instance ,当运行第二个实例时,聚焦到主窗口,在 electron/main.js 中添加如下代码:

// 限制只能打开一个窗口
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();
} else {
  app.on("second-instance", () => {
    // 当运行第二个实例时,将会聚焦到 mainWindow 这个窗口
    if (mainWindow) {
      if (mainWindow.isMinimized()) mainWindow.restore();
      mainWindow.focus();
      // mainWindow.show();
    }
  });
  
  app.whenReady().then(() => {
  // ...
  });
}

(6)关闭窗口时提示

监听 close 事件,使用 dialog 弹出提示,在 electron/main.js 中添加如下代码:

// electron/main.js
const { app, BrowserWindow, dialog } = require("electron");

const isPackaged = app.isPackaged;
let mainWindow;

const createWindow = () => {
  mainWindow = new BrowserWindow({
  // ...
  });
  
  // 在窗口要关闭的时候触发
  mainWindow.on("close", (e) => {
    e.preventDefault();
    dialog
      .showMessageBox(mainWindow, {
        type: "info",
        title: "退出提示",
        defaultId: 0,
        cancelId: 1,
        message: "确定要退出吗?",
        buttons: ["退出", "取消"],
      })
      .then((res) => {
        if (res.response === 0) {
          // e.preventDefault();
          // mainWindow.destroy();
          app.exit(0);
        }
      });
  });
};

三、代码签名

参考:https://www.electron.build/code-signing

(1)windows

windows 的证书需要找有资质的商家购买,在此不做说明,只说一下签名配置。

package.jsonbuild 中添加如下代码,设置使用的签名算法,我用的是 sha256:

{
  "build": {
    "win": {
      "signingHashAlgorithms": [
        "sha256"
      ]
    }
  }
}

设置环境变量,CSC_LINK(证书地址) 和 CSC_KEY_PASSWORD(证书密码)。运行 npm run electron:build 即可打包签名的客户端。

(2)mac

参考:Notarizing your Electron application

mac 代码签名需要申请 Apple Developer Program ,具体流程可参考苹果开发者公司账号申请全流程以及出现的问题,有问题就在客服页面打电话咨询客服。

Apple Developer Program 申请完成后,要生成签名证书,参考Mac Electron 应用的签名(signature)和公证(notarization)

证书生成后,需要修改一下配置,使用 Electron Notarize 公证应用程序,打开终端,执行以下命令安装 Electron Notarize:

 npm install @electron/notarize --save-dev

浏览器打开 https://appleid.apple.com/account/manage,生成 Apple 专用密码。

打开终端,输入如下命令,其中 <USERNAME> 替换为你的 Apple Id,<PASSWORD> 替换为刚才生成的专用密码:

security add-generic-password -a "<USERNAME>" -w "<PASSWORD>" -s "AC_PASSWORD"

build 目录下新建 notarize.js 文件,并添加如下代码:

const { notarize } = require("@electron/notarize");

exports.default = async function packageTask(context) {
  const { electronPlatformName, appOutDir } = context;
  if (electronPlatformName !== "darwin") {
    return;
  }

  const appName = context.packager.appInfo.productFilename;

  return await notarize({
    appBundleId: "your.id",
    appPath: `${appOutDir}/${appName}.app`,
    appleId: "email@example.com",
    appleIdPassword: `@keychain:AC_PASSWORD`,
  });
};

build 目录下新建 entitlements.mac.plist 文件,并添加如下代码:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.debugger</key>
    <true/>
  </dict>
</plist>

package.jsonbuild 中添加如下代码:

{
  "build": {
    "afterSign": "build/notarize.js",
    "mac": {
      "hardenedRuntime": true,
      "gatekeeperAssess": false,
      "entitlements": "build/entitlements.mac.plist",
      "entitlementsInherit": "build/entitlements.mac.plist",
    }
  }
}

设置环境变量,CSC_LINK(证书地址) 和 CSC_KEY_PASSWORD(证书密码)。运行 npm run electron:build 即可打包签名的客户端。


四、自动更新

参考:Auto Update

打开终端,执行以下命令安装 electron-updater:

npm install electron-updater

package.jsonbuild 中添加如下代码,其中 url 为客户端更新地址:

{
  "build": {
    "publish": {
      "provider": "generic",
      "url": "https://example.com/app_client/",
      "channel": "latest"
    },
  }
}

注:mac 必须进行代码签名、target 同时启用 dmg 和 zip,才能自动更新

electron/main.js 文件中添加如下代码:

const { app, BrowserWindow, dialog } = require("electron");
const { autoUpdater } = require("electron-updater");

const isPackaged = app.isPackaged;
let mainWindow;

const createWindow = () => {
  mainWindow = new BrowserWindow({
  // ...
  });

  // 自动更新
  function handleUpdate() {
    // 更新地址
    const updateURL = "https://js.houzi8.com/app_client/";
    // 设置是否自动下载,默认是true,当点击检测到新版本时,会自动下载安装包,所以设置为false
    autoUpdater.autoDownload = false;
    // 如果安装包下载好了,那么当应用退出后是否自动安装更新
    autoUpdater.autoInstallOnAppQuit = true;
    // 是否接受开发版,测试版之类的版本号
    autoUpdater.allowPrerelease = false;
    // 是否可以回退版本,比如从开发版降到旧的稳定版
    autoUpdater.allowDowngrade = false;
    autoUpdater.setFeedURL(updateURL);
    autoUpdater.on("error", function (error) {
      // 检查更新出错
    });
    autoUpdater.on("checking-for-update", function () {
      // 检查中
    });
    autoUpdater.on("update-not-available", function (info) {
      // 已经是最新版
    });
    autoUpdater.on("update-available", function (info) {
      // 检测到新版本
      dialog
        .showMessageBox(mainWindow, {
          type: "info",
          title: "更新提示",
          defaultId: 0,
          cancelId: 1,
          message: "检测到新版本,是否立即更新?",
          buttons: ["确定", "取消"],
        })
        .then((res) => {
          if (res.response === 0) {
            // 执行下载安装包
            autoUpdater.downloadUpdate();
          }
        });
    });
    autoUpdater.on("download-progress", function (progress) {
      // 下载进度
    });
    autoUpdater.on("update-downloaded", function (info) {
      // 新安装包下载完成
      dialog
        .showMessageBox(mainWindow, {
          type: "info",
          title: "更新提示",
          defaultId: 0,
          cancelId: 1,
          message: "新版本下载完成,是否立即安装?",
          buttons: ["确定", "取消"],
        })
        .then((res) => {
          if (res.response === 0) {
            // 退出应用并安装更新
            autoUpdater.quitAndInstall();
            mainWindow.destroy();
          }
        });
    });

    // 执行自动更新检查
    autoUpdater.checkForUpdates();
  }

  if (isPackaged) {
    handleUpdate();
  }
};

配置完成后,执行 npm run electron:build 打包,将打包生成的文件上传到 https://example.com/app_client/ 即可。
mac 下需上传的文件有:latest-mac.yml、Electron + Vue3 开发桌面应用-1.0.0-mac.zip、Electron + Vue3 开发桌面应用-1.0.0.dmg
windows 下需上传的文件有:latest.yml、Electron + Vue3 开发桌面应用 Setup 1.0.0.exe


参考文献:

Vite
Vue
Electron
electron-builder
concurrently
electron-vite-vue3-ts开发环境搭建
mainWindow.loadURL(“https://localhost:3000/”) show white screen on Electron app
vue热更新失效_vue-cli3热更新失效
package.json 文档
Notarizing your Electron application
Apple Developer Program
苹果开发者公司账号申请全流程以及出现的问题
Mac Electron 应用的签名(signature)和公证(notarization)
Electron Notarize