Electron-updater + Electron-builder + IIS + NSIS + Blockmap 完整增量更新方案

Electron-updater + Electron-builder + IIS + NSIS + Blockmap 完整增量更新方案

将这些工具结合使用可以实现高效、可靠的增量更新机制,其中 Blockmap 技术能显著减小更新包体积,提升用户体验。以下是完整实现方案:

方案概述

  • Electron-builder:负责打包应用并生成支持 Blockmap 的安装包和增量更新包
  • Electron-updater:客户端更新引擎,利用 Blockmap 实现高效增量更新
  • IIS:托管更新文件(包括安装包、Blockmap 和版本信息)
  • NSIS:生成 Windows 安装程序,支持自定义安装流程
  • Blockmap:二进制块映射技术,用于精确计算和传输文件差异

1. 环境准备

安装依赖

bash 复制代码
# 核心依赖
npm install electron-updater --save

# 开发依赖
npm install electron --save-dev
npm install electron-builder --save-dev

2. 项目配置

package.json 完整配置

JSON 复制代码
{
  "name": "electron-advanced-updater",
  "version": "1.0.0",
  "main": "src/main.js",
  "scripts": {
    "start": "electron .",
    "build": "electron-builder --win",
    "build:dir": "electron-builder --win --dir",
    "release": "electron-builder --win --publish always"
  },
  "build": {
    "productName": "ElectronAdvancedUpdater",
    "appId": "com.example.electronadvancedupdater",
    "copyright": "Copyright © 2023 Your Company",
    "directories": {
      "output": "dist",
      "buildResources": "build"
    },
    "generateBlockmap": true,  // 强制生成blockmap文件
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": [
            "x64",
            "ia32"
          ]
        }
      ],
      "publish": [
        {
          "provider": "generic",
          "url": "http://your-iis-server/update/"
        }
      ],
      "blockmap": {
        "compression": "deflate"  // blockmap压缩算法
      }
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "installerIcon": "build/icon.ico",
      "uninstallerIcon": "build/icon.ico",
      "installerHeaderIcon": "build/icon.ico",
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "Electron Advanced Updater",
      "include": "build/installer.nsh"  // 自定义NSIS脚本
    },
    "publish": {
      "provider": "generic",
      "url": "http://your-iis-server/update/"
    },
    "files": [
      "src/**/*",
      "node_modules/**/*",
      "package.json"
    ],
    "extraMetadata": {
      "main": "src/main.js"
    }
  },
  "devDependencies": {
    "electron": "^26.0.0",
    "electron-builder": "^24.6.0"
  },
  "dependencies": {
    "electron-updater": "^6.1.4"
  }
}
    

3. 代码实现

主进程更新逻辑 (src/main.js)

javascript 复制代码
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
const path = require('path');
const log = require('electron-log');

// 配置日志
log.transports.file.level = 'info';
autoUpdater.logger = log;
console.log = log.log; // 重定向console日志到文件

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1024,
    height: 768,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  });

  mainWindow.loadFile(path.join(__dirname, '../public/index.html'));
  
  // 开发环境下打开开发者工具
  if (process.env.NODE_ENV === 'development') {
    mainWindow.webContents.openDevTools();
  }
  
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

// 发送状态到渲染进程
function sendStatusToWindow(status, progress = null) {
  if (mainWindow && mainWindow.webContents) {
    mainWindow.webContents.send('update-status', {
      status,
      progress
    });
  }
}

// 配置并检查更新
function configureAndCheckUpdates() {
  // 配置更新源
  autoUpdater.setFeedURL({
    provider: 'generic',
    url: 'http://your-iis-server/update/'
  });

  // 更新事件监听
  autoUpdater.on('checking-for-update', () => {
    sendStatusToWindow('正在检查更新...');
  });

  autoUpdater.on('update-available', (info) => {
    sendStatusToWindow(`发现新版本 v${info.version},准备下载...`);
  });

  autoUpdater.on('update-not-available', (info) => {
    sendStatusToWindow('当前已是最新版本');
  });

  autoUpdater.on('error', (err) => {
    sendStatusToWindow(`更新错误: ${err.message}`);
    log.error('更新错误:', err);
  });

  autoUpdater.on('download-progress', (progressObj) => {
    const progress = Math.round(progressObj.percent);
    sendStatusToWindow(`正在下载: ${progress}%`, progress);
  });

  autoUpdater.on('update-downloaded', (info) => {
    sendStatusToWindow('更新已下载完成,准备安装');
    
    // 询问用户是否立即安装
    dialog.showMessageBox({
      type: 'info',
      title: '更新准备就绪',
      message: `新版本 v${info.version} 已下载完成,是否立即重启应用以应用更新?`,
      buttons: ['立即重启', '稍后重启']
    }).then((result) => {
      if (result.response === 0) {
        autoUpdater.quitAndInstall(false, true);
      }
    });
  });

  // 检查更新
  autoUpdater.checkForUpdates().catch(err => {
    log.error('检查更新失败:', err);
  });
}

// 监听渲染进程请求
ipcMain.on('check-for-updates', () => {
  configureAndCheckUpdates();
});

ipcMain.on('get-current-version', (event) => {
  event.returnValue = app.getVersion();
});

// 应用生命周期管理
app.on('ready', () => {
  createWindow();
  
  // 启动后延迟检查更新,避免影响启动性能
  setTimeout(() => {
    configureAndCheckUpdates();
  }, 3000);
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});
    

预加载脚本 (src/preload.js)

javascript 复制代码
{
  const { contextBridge, ipcRenderer } = require('electron');

  contextBridge.exposeInMainWorld('electronUpdater', {
    checkForUpdates: () => ipcRenderer.send('check-for-updates'),
    getCurrentVersion: () => ipcRenderer.sendSync('get-current-version'),
    onUpdateStatus: (callback) => {
      ipcRenderer.on('update-status', (event, status) => callback(status));
    }
  });
}
    

渲染进程页面 (public/index.html)

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Electron 高级更新示例</title>
  <style>
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      margin: 20px;
      color: #333;
    }
    .update-container {
      max-width: 600px;
      margin: 50px auto;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    .version-info {
      margin-bottom: 20px;
      color: #666;
    }
    .update-status {
      padding: 15px;
      border-radius: 4px;
      background-color: #f5f5f5;
      min-height: 40px;
      margin: 15px 0;
    }
    .progress-container {
      height: 8px;
      background-color: #eee;
      border-radius: 4px;
      overflow: hidden;
      margin: 10px 0;
      display: none;
    }
    .progress-bar {
      height: 100%;
      background-color: #0078d7;
      width: 0%;
      transition: width 0.3s ease;
    }
    button {
      background-color: #0078d7;
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
    }
    button:hover {
      background-color: #005a9e;
    }
  </style>
</head>
<body>
  <div class="update-container">
    <h1>Electron 高级更新示例</h1>
    <div class="version-info">
      当前版本: <span id="current-version"></span>
    </div>
    <button id="check-update-btn">检查更新</button>
    <div class="update-status" id="update-status">准备就绪</div>
    <div class="progress-container" id="progress-container">
      <div class="progress-bar" id="progress-bar"></div>
    </div>
  </div>

  <script>
    // 显示当前版本
    document.getElementById('current-version').textContent = window.electronUpdater.getCurrentVersion();
    
    // 检查更新按钮
    document.getElementById('check-update-btn').addEventListener('click', () => {
      document.getElementById('check-update-btn').disabled = true;
      document.getElementById('update-status').textContent = '正在发起更新检查...';
      window.electronUpdater.checkForUpdates();
    });
    
    // 监听更新状态
    window.electronUpdater.onUpdateStatus(({ status, progress }) => {
      document.getElementById('update-status').textContent = status;
      
      // 处理进度显示
      const progressContainer = document.getElementById('progress-container');
      const progressBar = document.getElementById('progress-bar');
      
      if (progress !== null) {
        progressContainer.style.display = 'block';
        progressBar.style.width = `${progress}%`;
        
        // 下载完成后重新启用按钮
        if (progress === 100) {
          document.getElementById('check-update-btn').disabled = false;
        }
      } else {
        // 没有进度信息时隐藏进度条
        progressContainer.style.display = 'none';
        document.getElementById('check-update-btn').disabled = false;
      }
    });
  </script>
</body>
</html>
    

自定义 NSIS 脚本 (build/installer.nsh)

bash 复制代码
; 引入必要的库
!include "MUI2.nsh"
!include "LogicLib.nsh"
!include "FileFunc.nsh"
!include "electron-builder.nsh"

; 配置安装程序
Name "Electron Advanced Updater"
OutFile "ElectronAdvancedUpdater-Setup.exe"
InstallDir "$PROGRAMFILES\ElectronAdvancedUpdater"

; 定义安装界面
!define MUI_ABORTWARNING
!define MUI_ICON "${BUILD_RESOURCES_DIR}\icon.ico"
!define MUI_UNICON "${BUILD_RESOURCES_DIR}\icon.ico"

; 欢迎页面
!insertmacro MUI_PAGE_WELCOME
; 安装目录选择页面
!insertmacro MUI_PAGE_DIRECTORY
; 安装进度页面
!insertmacro MUI_PAGE_INSTFILES
; 完成页面
!define MUI_FINISHPAGE_RUN "$INSTDIR\ElectronAdvancedUpdater.exe"
!insertmacro MUI_PAGE_FINISH

; 卸载页面
!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH

; 语言设置
!insertmacro MUI_LANGUAGE "SimpChinese"
!insertmacro MUI_LANGUAGE "English"

; 安装前检查
Function .onInit
  ; 检查应用是否正在运行
  ${If} ${RunningX64}
    StrCpy $R0 "ElectronAdvancedUpdater.exe"
  ${Else}
    StrCpy $R0 "ElectronAdvancedUpdater.exe"
  ${EndIf}
  
  nsProcess::FindProcess "$R0"
  Pop $R1
  ${If} $R1 == 0
    MessageBox MB_ICONEXCLAMATION "应用程序正在运行,请先关闭再继续安装。" /SD IDOK
    Abort
  ${EndIf}
  
  ; 调用electron-builder的初始化函数
  !insertmacro electron-builder::onInit
FunctionEnd

; 安装完成后的操作
Function .onInstSuccess
  ; 可以在这里添加注册系统事件、创建额外快捷方式等操作
FunctionEnd

; 主安装逻辑
Section "MainSection" SEC01
  ; 包含electron-builder的安装逻辑
  !insertmacro electron-builder::mainSection
SectionEnd

; 卸载前的清理操作
Section "Uninstall"
  ; 停止所有相关进程
  ${nsProcess::KillProcess} "ElectronAdvancedUpdater.exe" ""
  Pop $0
  
  ; 调用electron-builder的卸载逻辑
  !insertmacro electron-builder::uninstallSection
  
  ; 删除残留目录
  RMDir /r "$INSTDIR"
SectionEnd

; 安装完成页面设置
!define MUI_FINISHPAGE_TITLE "安装完成"
!define MUI_FINISHPAGE_DESCRIPTION "Electron Advanced Updater 已成功安装。"
    

4. IIS 服务器配置

目录结构

在 IIS 网站根目录下创建如下结构:

复制代码
/update/
  /win32-x64/
  
    ElectronAdvancedUpdater-1.0.0 Setup.exe.blockmap
    ElectronAdvancedUpdater-1.1.0 Setup.exe
    ElectronAdvancedUpdater-1.1.0 Setup.exe.blockmap
    latest.yml
  /win32-ia32/
    ; 同上,32位版本文件

MIME 类型配置

在 IIS 管理器中为网站添加以下 MIME 类型:

扩展名 MIME 类型 说明
.nupkg application/zip 增量更新包
.yml text/yaml 版本信息文件
.blockmap application/octet-stream Blockmap元数据
.exe application/exe 安装程序

权限配置

确保 update 目录授予 IIS_IUSRS 组读取权限,允许匿名访问。

5. 构建与发布流程

首次发布 (v1.0.0)

  1. 确保 package.json 中版本号为 1.0.0

  2. 构建安装包:

    bash 复制代码
    npm run build
  3. 构建成功后,将 dist 目录下的以下文件上传到 IIS 对应目录:

    • win-unpacked 目录(可选,用于调试)
    • ElectronAdvancedUpdater-1.0.0 Setup.exe
    • ElectronAdvancedUpdater-1.0.0 Setup.exe.blockmap
    • ElectronAdvancedUpdater-1.0.0-full.nupkg
    • latest.yml

发布增量更新 (v1.1.0)

  1. 更新 package.json 中的版本号为 1.1.0

  2. 构建新版本:

    bash 复制代码
    npm run build
  3. electron-builder 会自动生成增量包(*-delta.nupkg

  4. 将新生成的文件上传到 IIS 服务器:

  • ElectronAdvancedUpdater-1.0.0 Setup.exe.blockmap
  • ElectronAdvancedUpdater-1.1.0 Setup.exe
  • ElectronAdvancedUpdater-1.1.0 Setup.exe.blockmap
  • latest.yml(更新版本信息)

6. Blockmap 工作原理验证

  1. 检查构建输出

    确认 dist 目录中存在 .blockmap 文件,例如:

    复制代码
    ElectronAdvancedUpdater-1.1.0 Setup.exe.blockmap
  2. 查看更新日志

    在应用数据目录下的日志文件中(如 %APPDATA%\ElectronAdvancedUpdater\logs\main.log),确认有 Blockmap 相关日志:

    复制代码
    [autoUpdater] Block map signature verification passed
    [autoUpdater] Calculating diff with block map
    [autoUpdater] Downloading block maps for differential update
  3. 验证增量包大小

    比较全量包(-full.nupkg)和增量包(-delta.nupkg)的大小,增量包应显著 smaller(通常小 50-80%)。

7. 常见问题与解决方案

问题 1:Blockmap 文件未生成

  • 检查 :确认 package.jsongenerateBlockmap 设为 true
  • 解决 :删除 node_modulesdist 目录,重新安装依赖并构建

问题 2:增量更新失败,总是下载全量包

  • 检查
    • 确认所有历史版本的安装包和 blockmap 文件都已上传到服务器
    • 检查 latest.yml 文件是否正确包含所有版本信息
  • 解决:确保版本号严格遵循语义化版本规范,且所有文件路径正确

问题 3:IIS 服务器无法访问 blockmap 文件

  • 检查:使用浏览器直接访问 blockmap 文件 URL,确认能正常下载
  • 解决 :重新配置 .blockmap 的 MIME 类型为 application/octet-stream

问题 4:更新后应用无法启动

  • 检查:查看应用日志,确认是否有文件权限问题
  • 解决:在 NSIS 脚本中添加适当的权限设置,或在更新前关闭所有应用进程

总结

通过整合 Electron-updater、Electron-builder、IIS、NSIS 和 Blockmap 技术,我们实现了一个高效的增量更新方案:

  • Blockmap 技术显著减小了更新包体积,节省带宽和下载时间
  • Electron-updater 提供了可靠的更新检测和安装机制
  • IIS 服务器提供了稳定的更新文件托管服务
  • NSIS 确保了良好的安装和更新体验
相关推荐
cvpv3 分钟前
我将封装史上最优雅的 Axios
前端
Undoom4 分钟前
体系化AI开发方案:豆包新模型矩阵与PromptPilot自动化调优平台深度解析
前端
李剑一5 分钟前
面试官:后端一次性返回给前端十万条数据,渲染这十万条数据怎么能保证不卡顿
前端·面试
xianxin_6 分钟前
HTML5 MathML
前端
小高0077 分钟前
⚡前端底层四连击:Event Loop → 渲染帧 → GC → AST,一篇打通任督二脉
前端·javascript·面试
xianxin_10 分钟前
HTML5 浏览器支持
前端
我叫黑大帅10 分钟前
📝 Java 文件 IO 入门教程
java·前端
今禾11 分钟前
# 🚀 深入浅出 TypeScript(下):掌握泛型、接口及 React 中的高级应用
前端·javascript·typescript
GISer_Jing27 分钟前
浏览器缓存机制全解析:强缓存与协商缓存
前端·javascript·缓存
fatsheep洋38 分钟前
XSS的原型链污染1--原型链解释
前端·xss