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)
-
确保
package.json
中版本号为1.0.0
-
构建安装包:
bashnpm 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
-
构建新版本:
bashnpm 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 确保了良好的安装和更新体验