基于 electron-builder 实现 Electron 项目的 打包 与 远程更新

使用 Vue + Electron + electron-builder 搭建一个简单的 Electron 项目。本示例只对 Windows 打包进行了部分配置,更多配置可以查看 electron-builder 官网。如果遇到 依赖安装失败 或者 打包失败 的问题,建议多尝试几次。

Electron 依赖 安装失败原因:

1. Node 版本过低

可以在 Electron 官网 版本发布页 查看对应的 Node 版本。如果没有安装过 Node ,建议直接安装 nvm(WindowsLinux/MacOS),使用 nvm 下载 和 切换 Node 版本。

2. npm 没有使用国内镜像源

# 查看当前 npm 源
npm config get registry
# 设置阿里云镜像源
npm config set registry https://registry.npmmirror.com

1. 创建 Vue 项目

npm create vue@latest

2. 安装 Electron 依赖

npm install --save-dev electron

3. 安装 electron-builder 依赖

npm install --save-dev electron-builder

4. 安装 electron-updater 依赖

npm install electron-updater

5. 配置 package.json

/package.json

5.1 dependencies 与 devDependencies 由 npm 自动生成,不需要改动。

5.2 实际代码中需要把注释删掉。

{
  "name": "shadiao",
  "version": "0.0.0",
  "description": "沙雕",
  "type": "module",
  // 主进程
  "main": "./electron/main.js",
  "scripts": {
    "dev": "vite",
    // 启动 Electron
    "start": "electron .",
    // 打包
    "build": "vite build && electron-builder",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
    "format": "prettier --write src/"
  },
  "dependencies": {
    "electron-updater": "^6.1.8",
    "pinia": "^2.1.7",
    "vue": "^3.4.21",
    "vue-router": "^4.3.0"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.8.0",
    "@vitejs/plugin-vue": "^5.0.4",
    "@vue/eslint-config-prettier": "^9.0.0",
    "electron": "^30.0.0",
    "electron-builder": "^24.13.3",
    "eslint": "^8.57.0",
    "eslint-plugin-vue": "^9.23.0",
    "prettier": "^3.2.5",
    "vite": "^5.2.8",
    "vite-plugin-vue-devtools": "^7.0.25"
  },
  // 作者信息
  "author": {
    "name": "shadiao",
    "email": "shadiao@shadiao.com"
  },
  // 软件协议
  "license": "MIT",
  // 打包配置
  "build": {
    // 应用名称
    "productName": "沙雕",
    // 应用唯一标识
    "appId": "com.shadiao.shadiao",
    "directories": {
      // 打包文件存放位置
      "output": "build"
    },
    // 将源代码打包成 asar 包
    "asar": true,
    // windows 配置
    "win": {
      // 应用图标
      "icon": "./public/shadiao.ico",
      // 安装包名称
      "artifactName": "${productName}-${platform}-${arch}-${version}.${ext}",
      "target": [
        {
          // 打包为 nsis
          "target": "nsis",
          // 适用 x64 架构
          "arch": [
            "x64"
          ]
        }
      ]
    },
    "nsis": {
      // 是否一键安装
      "oneClick": false,
      // 允许请求提升权限
      "allowElevation": true,
      // 允许自定义安装目录
      "allowToChangeInstallationDirectory": true,
      // 创建桌面快捷方式
      "createDesktopShortcut": true
    },
    "publish": {
      // 发布类型,generic 表示私有服务,也可以使用 github
      "provider": "generic",
      // 服务 url,填写自己服务器的 url(http/https + :// + 域名/ip + : + 端口号 (默认端口号为 80))
      "url": "http://www.wurenjingjie.com"
    }
  }
}

6. 配置 vite.config.js

/vite.config.js

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueDevTools from 'vite-plugin-vue-devtools'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    VueDevTools(),
  ],
  // 防止打包后找不到渲染页面导致白屏
  base: './',
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

7. 在根目录创建 electron 目录

/electron

8. main.js - Electron 主进程

/electron/main.js

import { app, BrowserWindow } from 'electron'
import path from 'path'
import { fileURLToPath } from 'url'

import { window } from './window.js'
import { tray } from './tray.js'
import { updater } from './updater.js'

let win = null

// 创建窗口
const createWindow = () => {
  win = new BrowserWindow({
    // 窗口宽度
    width: 1050,
    // 窗口高度
    height: 800,
    // 最小窗口宽度
    minWidth: 800,
    // 最小窗口高度
    minHeight: 750,
    // 窗口居中
    center: true,
    // 窗口边框
    // frame: false,
    // 不显示窗口
    show: false,
    // 页面配置
    webPreferences: {
      // 预加载脚本配置
      preload: path.join(path.dirname(fileURLToPath(import.meta.url)), './preload.js')
    }
  })

  // 优雅地显示窗口
  win.once('ready-to-show', () => {
    win.show()
  })

  // 根据 app 是否打包来判断 开发环境 和 生产环境
  if (!app.isPackaged) {
    // 开发环境
    win.loadURL('http://localhost:5173')
  } else {
    // 生产环境
    win.loadFile(path.join(path.dirname(fileURLToPath(import.meta.url)), '../dist/index.html'))
  }

  // 打开开发者工具
  // win.webContents.openDevTools()

  // 禁用菜单
  win.setMenu(null)

  window(win)
  tray(win)
  updater(win)
}

// app 准备完毕
app.whenReady().then(() => {
  // 请求单例锁,防止同时打开多个程序
  if (!app.requestSingleInstanceLock()) {
    app.quit()
    return
  }

  createWindow()

  // 第二个程序实例被创建
  // event - 第二个程序实例触发的事件
  // argv - 第二个程序实例命令行参数的数组
  // workingDirectory - 第二个程序实例的工作目录
  // additionalData - 第二个程序实例发送过来的额外的 JSON 对象
  app.on('second-instance', (event, argv, workingDirectory, additionalData) => {
    console.log(additionalData)

    // 窗口是否对用户可见
    if (win.isVisible()) {
      // 聚焦窗口
      win.focus()
    } else {
      // 显示窗口
      win.show()
    }
  })

  // 在 maxOS 上,当单击停靠图标并且没有其他窗口打开时,通常会在应用程序中重新创建一个窗口
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
})

9. window.js - Electron 窗口配置

/electron/window.js

import { ipcMain } from 'electron'

export function window(win) {
  // 最小化窗口
  ipcMain.on('min-window', () => {
    win.minimize()
  })

  // 最大化窗口
  ipcMain.on('max-window', () => {
    win.maximize()
  })

  // 恢复窗口大小
  ipcMain.on('restore-window', () => {
    win.unmaximize()
  })

  // 窗口最大化
  win.on('maximize', () => {
    win.webContents.send('window-max')
  })

  // 窗口恢复大小
  win.on('unmaximize', () => {
    win.webContents.send('window-restore')
  })

  // 关闭窗口
  ipcMain.on('close-window', () => {
    win.close()
  })

  // 改变 win.close() 默认事件
  win.on('close', (event) => {
    event.preventDefault()
    win.hide()
  })
}

10. tray.js - Electron 托盘配置

/electron/tray.js

import { app, Tray, Menu } from 'electron'
import path from 'path'
import { fileURLToPath } from 'url'

export function tray(win) {
  let systemTray = null

  // 初始化系统托盘
  if (!app.isPackaged) {
    // 开发环境
    systemTray = new Tray(path.join(path.dirname(fileURLToPath(import.meta.url)), '../public/shadiao.ico'))
  } else {
    // 生产环境
    systemTray = new Tray(path.join(path.dirname(fileURLToPath(import.meta.url)), '../dist/shadiao.ico'))
  }

  // 初始化托盘菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '打开沙雕',
      click: () => {
        // 窗口是否对用户可见
        if (win.isVisible()) {
          // 聚焦窗口
          win.focus()
        } else {
          // 显示窗口
          win.show()
        }
      }
    },
    {
      label: '退出沙雕',
      click: () => {
        // 由于 win.close() 默认事件被修改,使用 win.destroy() 强制关闭窗口
        win.destroy()
        // 退出程序
        app.quit()
      }
    }
  ])

  // 设置托盘菜单
  systemTray.setContextMenu(contextMenu)

  // 设置鼠标移入托盘显示文字
  systemTray.setToolTip('沙雕')

  // 单击托盘事件
  systemTray.on('click', () => {
    // 窗口是否对用户可见
    if (win.isVisible()) {
      // 最小化窗口
      win.minimize()
    } else {
      // 显示窗口
      win.show()
    }
  })
}

11. updater.js - Electron 更新配置

/electron/updater.js

import { ipcMain } from 'electron'
import electronUpdater from 'electron-updater'

const { autoUpdater } = electronUpdater

export function updater(win) {
  // 关闭自动更新
  autoUpdater.autoDownload = false

  // 有新版本
  autoUpdater.on('update-available', (info) => {
    win.webContents.send('update-available', info)
  })

  // 无新版本
  autoUpdater.on('update-not-available', (info) => {
    win.webContents.send('update-not-available', info)
  })

  // 更新出错
  autoUpdater.on('error', (error) => {
    win.webContents.send('update-error', error)
  })

  // 下载进度
  autoUpdater.on('download-progress', (info) => {
    win.webContents.send('download-progress', {
      speed: info.bytesPerSecond,  // 网速
      percent: info.percent,  // 百分比
      transferred: info.transferred,  // 已传大小
      total: info.total  // 总大小
    })
  })
  
  // 下载完成
  autoUpdater.on('update-downloaded', () => {
    win.webContents.send('update-downloaded')
  })

  // 检测是否有新版本
  ipcMain.on('check-update', () => {
    autoUpdater.checkForUpdates()
  })

  // 下载更新包
  ipcMain.on('download', () => {
    autoUpdater.downloadUpdate()
  })

  // 关闭程序,安装更新包
  ipcMain.on('quit-and-install', () => {
    autoUpdater.quitAndInstall()
  })
}

12. preload.js - Electron 预加载脚本

/electron/preload.js

/* eslint-disable no-undef */
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  // 最小化窗口
  minWindow: () => {
    ipcRenderer.send('min-window')
  },
  // 最大化窗口
  maxWindow: () => {
    ipcRenderer.send('max-window')
  },
  // 恢复窗口大小
  restoreWindow: () => {
    ipcRenderer.send('restore-window')
  },
  // 窗口最大化
  windowMax: (event) => {
    ipcRenderer.on('window-max', event)
  },
  // 窗口恢复大小
  windowRestore: (event) => {
    ipcRenderer.on('window-restore', event)
  },
  // 关闭窗口
  closeWindow: () => {
    ipcRenderer.send('close-window')
  },
  // 检测是否有新版本
  checkUpdate: () => {
    ipcRenderer.send('check-update')
  },
  // 有新版本
  updateAvailable: (event, info) => {
    ipcRenderer.on('update-available', event, info)
  },
  // 无新版本
  updateNotAvailable: (event, info) => {
    ipcRenderer.on('update-not-available', event, info)
  },
  // 更新出错
  updateError: (event, error) => {
    ipcRenderer.on('update-error', event, error)
  },
  // 下载更新包
  download: () => {
    ipcRenderer.send('download')
  },
  // 下载进度
  downloadProgress: (event) => {
    ipcRenderer.on('download-progress', event)
  },
  // 下载完成
  updateDownloaded: (event) => {
    ipcRenderer.on('update-downloaded', event)
  },
  // 关闭程序,安装更新包
  quitAndInstall: () => {
    ipcRenderer.send('quit-and-install')
  }
})

13. 图标

/public/shadiao.ico

https://gitee.com/wu-ren-jing-jie/shadiao/blob/master/public/shadiao.ico

14. 运行

npm run dev
npm run start

15. 打包

npm run build

16. 远程更新

16.1 打包时修改 package.json 中的 version 字段,使其高于原来版本。

16.2 将打包后 build 文件夹生成的 包名.exe 和 latest.yml 放到 package.json 中指定 url 的 服务器 目录下。

Tips

1. Vue Router 中使用 createWebHashHistory。

2. Vue 组件中,使用 window.electronAPI.xxx() 调用 preload.js 中的通信函数。

代码示例

https://gitee.com/wu-ren-jing-jie/shadiao

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值