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. 环境准备
安装依赖
# 核心依赖
npm install electron-updater --save# 开发依赖
npm install electron --save-dev
npm install electron-builder --save-dev
2. 项目配置
package.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)
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)
{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)
<!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)
; 引入必要的库
!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 == 0MessageBox MB_ICONEXCLAMATION "应用程序正在运行,请先关闭再继续安装。" /SD IDOKAbort${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.blockmapElectronAdvancedUpdater-1.1.0 Setup.exeElectronAdvancedUpdater-1.1.0 Setup.exe.blockmaplatest.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)
- 确保
package.json
中版本号为1.0.0
- 构建安装包:
npm run build
- 构建成功后,将
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)
- 更新
package.json
中的版本号为1.1.0
- 构建新版本:
npm run build
- electron-builder 会自动生成增量包(
*-delta.nupkg
) - 将新生成的文件上传到 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 工作原理验证
-
检查构建输出:
确认dist
目录中存在.blockmap
文件,例如:ElectronAdvancedUpdater-1.1.0 Setup.exe.blockmap
-
查看更新日志:
在应用数据目录下的日志文件中(如%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
-
验证增量包大小:
比较全量包(-full.nupkg
)和增量包(-delta.nupkg
)的大小,增量包应显著 smaller(通常小 50-80%)。
7. 常见问题与解决方案
问题 1:Blockmap 文件未生成
- 检查:确认
package.json
中generateBlockmap
设为true
- 解决:删除
node_modules
和dist
目录,重新安装依赖并构建
问题 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 确保了良好的安装和更新体验