electron

electron更新

# 1. 思路 1

问题:electron 中集成 nestjs 无法将 nestjs 的核心依赖从 dependencies 移除

# 1.1. 打包 asar 只保留核心模块的依赖

# 1.1.1. package.json 优化

“dependencies”中只保留核心模块的依赖,其他移入到"devDependencies"中,以减少打包体积,如:

  "dependencies": {
    "better-sqlite3": "^11.8.1",
    "better-sqlite3-multiple-ciphers": "^11.8.1",
    "typeorm": "^0.3.21",
    "reflect-metadata": "^0.2.2",
    "@nestjs/core": "^11.1.5",
    "@nestjs/common": "^11.1.5",
    "@nestjs/platform-express": "^11.1.5",
    "rxjs": "^7.8.2"
  },

非 nestjs 框架,只保留前面 4 个依赖即可。

# 1.1.2. electron-builder 配置

files:
  - '!**/.vscode/*'
  - '!src/*'
  - '!electron.vite.config.{js,ts,mjs,cjs}'
  - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
  - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
  - '!store/*'
  - '!*.md'
  - '!log/*'
  - '!dist/**'
  - '!locales/**'
  - '!dist/win-unpacked'
  - '!data/*'
  - '!node_modules/.vite/**'
  - '!node_modules/@opentiny/**' # 不打包tiny的组件
  - '!node_modules/@swc/**' # 编译ts的插件
  - '!node_modules/better-sqlite3'
  - '!node_modules/better-sqlite3-multiple-ciphers'
  - '!node_modules/hyt-exam-native/*'
  - '!node_modules/typeorm/*'
  - '!packages'
  - '!resources/*'
extraResources:
  - from: 'resources'
    to: 'resources'
  - from: 'node_modules/better-sqlite3'
    to: 'node_modules/better-sqlite3'
  - from: 'node_modules/better-sqlite3-multiple-ciphers'
    to: 'node_modules/better-sqlite3-multiple-ciphers'
  - from: 'packages/hyt-exam-native'
    to: 'node_modules/hyt-exam-native'
  - from: 'node_modules/typeorm'
    to: 'node_modules/typeorm'

# 1.1.3. electron.vite.config.ts 配置

import { initModulePathEval } from './build/scripts/module-path.js'
 main: {
    plugins: [externalizeDepsPlugin()],
    build: {
      chunkSizeWarningLimit: 2000,
      assetsInlineLimit: 1024 * 2, // 2KB 以下图片转 Base64;设置大了转成base64后会增加包大小
      // 在生产环境移除console.log
      terserOptions: {
        compress: {
          drop_console: false,
          pure_funcs: ['console.log', 'console.info'],
          drop_debugger: true
        }
      },
      rollupOptions: {
        input: {
          index: path.resolve(__dirname, 'src/main/index.ts')
        },
        output: {
          banner: initModulePathEval  // 打包时注入模块路径-核心
        }
      }
    }
  },

# 1.1.4. module-path.js 配置

这一步,目前我也没看懂为啥这么做。 在根目录的 build 文件夹下创建 scripts 文件夹,并在该文件夹下创建 module-path.js 文件,内容如下:

export const initModulePathEval = `
  // ================== early init start ==================
  (function(){
      const path = require('path')
      const resourcesPath = process.resourcesPath
      const resourceModules = path.join(resourcesPath, 'node_modules')
      const asarModules = path.join(resourcesPath, 'app.asar', 'node_modules')
      const extraPaths = [resourceModules, asarModules]
      const isWindows = process.platform === 'win32'
      // 把 extraResources 和 asar 的 node_modules 添加到搜索路径
      process.env.NODE_PATH = extraPaths.join(isWindows ? ';' : ':')
      require('module').Module._initPaths()
  })();
  // ================== early init end ==================
`

# 1.1.5. package-lock.json 要提交

package-lock.json 不能忽略提交,不然只能在一台电脑打包更新,其他电脑打包更新会报错。

# 1.1.6. 新开子进程执行更新

import {  spawn } from 'node:child_process'
return new Promise((resolve) => {
  const child = spawn(exePath, ['--uv', v], {
    detached: true,
    stdio: 'ignore',
    windowsHide: true
  }) as any
  child.unref()
  timer = setTimeout(() => {
    child.kill() // 2s后杀死子进程
  }, 2000)
  app.quit() //立即退出程序,避免asar被占用
  child.on('error', (error) => {
    // 更新失败
    return resolve(0)
  })
  resolve(1)  // 更新成功
})

# 1.2. Windows 的.bat 脚本

@echo off
:: echo 开始执行文件操作...

:: 检查history文件夹是否存在,不存在则创建
if not exist "history" (
    mkdir "history"
    echo 创建history文件夹成功
)

:: 检查app.asar是否存在,如果存在则移动并重命名
if exist "app.asar" (
    move "app.asar" "history\4.25.16.asar" >nul
)

:: 检查pack.asar是否存在,如果存在则重命名为app.asar
if exist "pack.asar" (
    ren "pack.asar" "app.asar"
)

# 1.3. electron 更新待测试

自研“退出-替换-重启”小脚本

const { app } = require('electron')
const path = require('path')
const fs = require('fs-extra')
const cp = require('child_process')

const ASAR = path.join(__dirname, '../app.asar')
const TMP = path.join(__dirname, '../app_new.asar')

// 1. 下载完新 asar 后把它重命名为 app_new.asar
// 2. 启动一个 detached 的“替换器”
function applyUpdate() {
  const bat = path.join(__dirname, 'updater.exe') // 也可以直接写 .bat
  // updater.exe 内容见下
  cp.spawn(bat, [process.pid, TMP, ASAR], {
    detached: true,
    stdio: 'ignore'
  }).unref()
  app.quit() // 主进程立即退出,释放句柄
}

// updater.exe(C++ / Go / PyInstaller 均可)逻辑:
//   ① 等待父进程 PID 消失;
//   ② MoveFileEx(app_new.asar, app.asar, MOVEFILE_REPLACE_EXISTING);
//   ③ ShellExecute 原 exe 路径,重启 App。

签名丢失:只要替换了 asar,Windows 重新计算哈希,Mac 会破环签名,上架商店版本必须走全量安装器; 不要把新文件命名为 app.asar.tmp 后原地改名——改名瞬间同样会被锁;

全部排除方式

# 2. 思路 2

asar 最轻量化,最优解

electron-builder.yml 中配置如下:

# 以下不打包进asar
asarUnpack:
  - node_modules/** #【重要】所有node_modules目录不打包进asar
  - resources/** #resources目录不打包进asar
  - node_modules/better-sqlite3
  - node_modules/better-sqlite3-multiple-ciphers
上次更新: