前置说明
本文方案适用于「基于uni-app开发、同时打包Android App和Windows Electron客户端」的跨端应用,核心解决"双端更新逻辑不统一、依赖Electron autoUpdater不灵活"的问题。
技术前提:已具备Android端更新逻辑+后端版本接口,Electron版本≥13,uni-app版本≥3.0,Node.js≥14。
目录
-
方案核心:用一套逻辑搞定双端更新
-
核心流程:双端通用的更新闭环
-
具体实现:三步落地跨端更新 3.1 第一步:封装跨端公共工具(复用Android逻辑) 3.2 第二步:Electron主进程实现核心能力 3.3 第三步:uni-app渲染进程(Windows端)实现 3.4 Android端兼容性保障
-
兼容性与可靠性:这套方案靠谱吗?
-
实施注意事项:避坑关键
-
进阶优化:断点续传(Windows端)
-
总结:统一更新的核心价值
在开发同时支持Android和Windows的跨端应用时,很多开发者都会觉得维护两端的更新逻辑很麻烦。 如果你的应用是用uni-app做的,既不想用Electron官方的更新工具(autoUpdater),又想直接用已经写好的Android端更新逻辑,完全可以自己实现一套更新流程来满足需求。 这篇文章会把这个方案的思路、具体步骤和兼容性注意事项讲清楚,确保这套逻辑在Windows(Electron)和Android端都能稳定运行。让读者(尤其是 uni-app+Electron 开发者)能直接落地这套方案,同时规避实际开发中的问题。
一、方案核心:用一套逻辑搞定双端更新
本文方案的核心思路是:将Electron视为普通Windows桌面程序,复用Android端"检查版本→下载安装包→校验完整性→触发安装"的核心更新逻辑,通过Electron的基础进程通信能力(ipcMain/ipcRenderer)和Node.js原生模块(https、fs等)实现更新全流程,彻底摆脱 Electron autoUpdater 的束缚。
这个方案的好处:
✅ 跨端逻辑统一:Android和Windows用一套更新核心逻辑,不用分开维护,减少工作量;
✅ 自主可控:不依赖第三方更新工具,更新的每一步、安装包的管理都由自己说了算;
✅ 兼容性好:基于uni-app和Node.js的基础能力,能适配大多数Windows系统和Android设备;
✅ 衔接现有逻辑:可以直接用已经有的后端版本接口,不用额外开发适配接口;
二、核心流程:实现双端通用的更新
不管是Android端还是Windows端,更新流程都遵循下面这套统一逻辑,保证两端用户的更新体验一致:
-
应用启动后,自动调用后端接口,获取最新版本信息(包括版本号、发布时间、安装包下载链接、校验码等);
-
对比本地当前版本和后端的最新版本,判断是否需要更新;
-
如果需要更新,先检查本地有没有已经下载好的最新版本安装包,避免重复下载;
-
如果本地没有缓存,就提示用户下载(也可以设置成自动静默下载),并实时显示下载进度;
-
下载完成后,可选步骤:校验安装包是否完整(比如用MD5或SHA256校验,避免下载的文件损坏);
-
提示用户安装,用户确认后关闭当前应用,启动安装包完成更新;
-
支持手动清理旧的更新缓存,避免占用过多存储空间;

三、具体实现步骤
具体实现分为三步:"封装跨端通用工具""实现Electron主进程核心功能""实现uni-app Windows端页面逻辑",同时保证能兼容已有的Android端逻辑。
第一步:封装跨端公共工具(复用Android端逻辑)
创建src/utils/updateHelper.js文件,把两端都能用的逻辑(比如判断是否有新版本、弹出提示框、清理更新状态等)封装起来,避免重复写代码:
go
/**
- 跨端更新通用工具类
*/
import { showToast, showModal } from'@dcloudio/uni-app';
import { formatBytesToMB } from'./formatBytesToMB';
// 检查是否有新版本
exportconst hasNewVersion = (currentVersion, remoteInfo) => {
if (!remoteInfo || !remoteInfo.version) returnfalse;
return currentVersion !== remoteInfo.version;
};
// 格式化版本信息(统一字段格式)
exportconst formatUpdateInfo = (updateStatus) => {
return {
percent: updateStatus.percent || 0,
transferred: updateStatus.transferred || 0,
total: updateStatus.total || 0,
version: updateStatus.version || '',
uploadTime: updateStatus.uploadTime || '未知',
fileUrl: updateStatus.fileBean?.fileUrl || '', // 安装包下载地址(APK/EXE)
md5: updateStatus.fileBean?.md5 || '', // 安装包校验值(可选)
};
};
// 通用更新提示弹窗
exportconst showUpdateModal = async (options) => {
const { title = '更新提示', content, confirmText = '确认', cancelText = '取消' } = options;
returnnewPromise((resolve) => {
showModal({ title, content, confirmText, cancelText, success: (res) => resolve(res.confirm), fail: () => resolve(false) });
});
};
// 通用错误处理
exportconst handleUpdateError = (error, defaultMsg = '操作失败,请重试') => {
const message = error?.message || error || defaultMsg;
console.error('更新流程错误:', message);
showModal({ title: '操作失败', content: message, confirmText: '知道了', showCancel: false });
};
// 清理更新相关状态
exportconst clearUpdateState = (stateRefs) => {
const { updateStatus, hasDownloadedInstaller } = stateRefs;
updateStatus.value = { percent: 0, transferred: 0, total: 0, version: '', uploadTime: '', fileUrl: '', md5: '' };
if (hasDownloadedInstaller) hasDownloadedInstaller.value = false;
};
// Windows端专用:判断系统架构(x64/ia32)
exportconst judgeSystemArch = () => {
let arch = 'x64';
const ARCH_CONFIG = { x64: { id: '2009210216359993346' }, ia32: { id: '2009206997420417026' } };
try { if (process?.arch) arch = process.arch === 'ia32' ? 'ia32' : 'x64'; } catch (err) { console.error('判断系统架构失败,默认使用64位:', err); }
return { systemArch: arch, currentAppId: ARCH_CONFIG[arch].id };
};
第二步:Electron主进程实现核心能力
在Electron的主进程文件(比如background.js)里,用Node.js的基础工具实现下载、安装、检查缓存等功能,再通过ipcMain把这些功能暴露给页面调用,全程不用官方更新工具:
go
// Electron 主进程 - background.js
const { app, ipcMain, shell } = require('electron');
const fs = require('fs');
const path = require('path');
const https = require('https');
const crypto = require('crypto');
let downloadProgress = { percent: 0, transferred: 0, total: 0 };
// 1. 检查版本(调用后端接口)
ipcMain.handle('check-update', async (event, appId) => {
returnnewPromise((resolve) => {
const req = https.request({ hostname: '你的后端域名', path: `/api/setting/check?appId=${appId}`, method: 'GET' }, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve(JSON.parse(data)));
});
req.on('error', (err) => resolve({ code: -1, message: err.message }));
req.end();
});
});
// 2. 下载安装包(EXE)
ipcMain.handle('download-update', async (event, updateInfo) => {
const { version, fileUrl, md5 } = updateInfo;
const downloadDir = path.join(app.getPath('userData'), 'updates');
if (!fs.existsSync(downloadDir)) fs.mkdirSync(downloadDir, { recursive: true });
const exePath = path.join(downloadDir, `app-v${version}.exe`);
if (fs.existsSync(exePath)) return { success: true, path: exePath };
returnnewPromise((resolve, reject) => {
https.get(fileUrl, (res) => {
const totalLength = parseInt(res.headers['content-length'], 10);
downloadProgress = { total: totalLength, transferred: 0, percent: 0 };
const writeStream = fs.createWriteStream(exePath);
res.pipe(writeStream);
// 推送下载进度
res.on('data', (chunk) => {
downloadProgress.transferred += chunk.length;
downloadProgress.percent = Math.floor((downloadProgress.transferred / totalLength) * 100);
event.sender.send('update-progress', downloadProgress);
});
// 下载完成校验
writeStream.on('finish', () => {
if (md5) {
const fileMd5 = crypto.createHash('md5').update(fs.readFileSync(exePath)).digest('hex');
if (fileMd5 !== md5) { fs.unlinkSync(exePath); return reject(newError('安装包校验失败')); }
}
resolve({ success: true, path: exePath });
});
writeStream.on('error', (err) => { fs.existsSync(exePath) && fs.unlinkSync(exePath);
reject(err); });
}).on('error', (err) => reject(err));
});
});
// 3. 安装更新
ipcMain.handle('install-update', async (event, version) => {
const exePath = path.join(app.getPath('userData'), 'updates', `app-v${version}.exe`);
if (!fs.existsSync(exePath)) return reject(newError('安装包不存在'));
app.quit();
shell.openPath(exePath).catch((err) =>console.error('启动安装包失败:', err));
});
// 4. 检查本地缓存
ipcMain.handle('has-cached-installer', async (event, version) => {
const exePath = path.join(app.getPath('userData'), 'updates', `app-v${version}.exe`);
return { cached: fs.existsSync(exePath) };
});
// 5. 清理更新缓存
ipcMain.handle('clear-update-cache', async (event, version) => {
const exePath = path.join(app.getPath('userData'), 'updates', `app-v${version}.exe`);
fs.existsSync(exePath) && fs.unlinkSync(exePath);
returntrue;
});
第三步:uni-app渲染进程(Windows端)实现
在uni-app的Windows端更新页面里,通过ipcRenderer调用主进程暴露的功能,同时复用前面封装的通用工具逻辑,就能完成整个更新流程:
go
<template>
<view class="app-version-page">
<image class="app-version-logo" src="/static/mars-logo.png" mode="scaleToFill" />
<view class="app-version-progress" v-if="updateStatus.percent">
<u-line-progress class="progress-bar" height="4" :percentage="updateStatus.percent" activeColor="#ff464b" :showText="false"/>
<view>{{ formatBytesToMB(updateStatus.transferred) + '/' + formatBytesToMB(updateStatus.total) }}</view>
</view>
<view class="app-version-section">当前版本:{{ versionCurrent }}</view>
<view class="app-version-section" v-if="hasNewVersionFlag">最新版本:{{ updateStatus.version }}</view>
<view class="handle-btn" @tap="handleUpdateClick" v-if="hasNewVersionFlag">
{{ hasDownloadedInstaller ? '立即安装' : '下载更新' }}
</view>
<view class="app-version-section" v-else>当前已是最新版本</view>
</view>
</template>
<script setup>
import { ref, nextTick, computed } from "vue";
import { onLoad, onUnload } from "@dcloudio/uni-app";
import { version } from '/package.json';
import { formatBytesToMB } from '@/utils/formatBytesToMB';
import { hasNewVersion, formatUpdateInfo, showUpdateModal, handleUpdateError, clearUpdateState, judgeSystemArch } from '@/utils/updateHelper';
// 响应式数据
const versionCurrent = ref(version);
const updateStatus = ref({ percent: 0, transferred: 0, total: 0, version: '', fileUrl: '', md5: '' });
const hasDownloadedInstaller = ref(false);
const { currentAppId } = judgeSystemArch();
const hasNewVersionFlag = computed(() => hasNewVersion(versionCurrent.value, updateStatus.value));
// 监听下载进度
const listenProgress = () => {
window.electron.ipcRenderer.on('update-progress', (_, progress) => {
updateStatus.value = { ...updateStatus.value, ...progress };
});
};
// 检查更新
const checkUpdate = async () => {
try {
const res = await window.electron.ipcRenderer.invoke('check-update', currentAppId);
if (res.code === -1) throw new Error(res.message);
updateStatus.value = formatUpdateInfo(res.datas);
if (!hasNewVersionFlag.value) { uni.showToast({ title: '当前已是最新版本', icon: 'none' }); return; }
const { cached } = await window.electron.ipcRenderer.invoke('has-cached-installer', updateStatus.value.version);
hasDownloadedInstaller.value = cached;
if (cached) {
const confirm = await showUpdateModal({ title: '更新已就绪', content: `检测到新版本 v${updateStatus.value.version}\n是否立即安装?`, confirmText: '立即安装' });
confirm && installUpdate();
return;
}
const confirm = await showUpdateModal({ title: '更新提示', content: `检测到新版本 v${updateStatus.value.version}\n是否立即下载?` });
confirm && downloadUpdate();
} catch (err) {
handleUpdateError(err, '检查更新失败');
}
};
// 下载安装包
const downloadUpdate = async () => {
try {
uni.showToast({ title: '开始下载...', icon: 'loading', mask: true });
const result = await window.electron.ipcRenderer.invoke('download-update', {
version: updateStatus.value.version,
fileUrl: updateStatus.value.fileUrl,
md5: updateStatus.value.md5,
});
uni.hideToast();
if (result.success) {
hasDownloadedInstaller.value = true;
const confirm = await showUpdateModal({ title: '下载完成', content: '新版本已下载完成,是否立即安装?' });
confirm && installUpdate();
}
} catch (err) {
uni.hideToast();
handleUpdateError(err, '下载失败');
}
};
// 安装更新
const installUpdate = async () => {
try {
await window.electron.ipcRenderer.invoke('install-update', updateStatus.value.version);
} catch (err) {
handleUpdateError(err, '启动安装包失败');
}
};
// 统一更新按钮逻辑
const handleUpdateClick = () => {
hasDownloadedInstaller.value ? installUpdate() : checkUpdate();
};
// 生命周期
onLoad(async () => { listenProgress(); await nextTick(); await checkUpdate(); });
onUnload(() => { window.electron.ipcRenderer.removeAllListeners('update-progress'); clearUpdateState({ updateStatus, hasDownloadedInstaller }); });
</script>
Android端兼容性保障
Android端不用改核心逻辑,只要注意两点:
-
复用前面封装的通用工具(
updateHelper.js)里的逻辑,比如判断版本、弹提示框等,保证Android和Windows端的用户操作体验一致; -
后端接口根据平台返回对应的安装包链接(给Android返回APK链接,给Windows返回EXE链接);
四、兼容性与可靠性:这套方案靠谱吗?
能不能用?靠谱吗?
完全脱离官方更新工具:核心只用到Electron的进程通信和Node.js的基础工具,没用到任何Electron官方的更新相关功能;
逻辑自己说了算:从查版本到装完成的每一步都能自己控制,想加静默下载、强制更新等功能都可以灵活调整;
复用性强:通用工具可以直接给Android端用,后端接口也不用额外改,能减少开发和维护的工作量;
兼容性怎么样?
Windows端:支持Windows 10及以上系统,兼容Electron 13及以上版本,用到的都是Node.js自带的工具,不会有兼容性问题;
Android端:支持Android 7.0及以上设备,复用的是uni-app成熟的App端更新功能,稳定性有保障;
跨端互不干扰:通过条件编译和平台判断,确保Android端不会调用Windows端的功能,Windows端也不会调用Android端的功能,避免崩溃;
五、实施注意事项:避坑关键
- 权限问题:
-
Windows:Electron写入userData目录无需管理员权限,但EXE安装包可能需要,打包时可配置Inno Setup的
PrivilegesRequired=lowest; -
Android:确保APK下载后有安装权限,Android 10+需申请MANAGE_EXTERNAL_STORAGE权限。
-
网络异常:
-
-
补充网络断开后的重试逻辑(比如下载失败时,提示用户"网络异常,是否重试?");
-
后端接口返回格式需统一,建议增加接口版本号(如/api/v1/setting/check),避免后续迭代兼容问题。
-
-
安装包管理:
-
-
定期清理旧版本安装包(比如更新完成后删除旧EXE/APK),避免占用存储空间;
-
Windows端EXE安装包建议加数字签名,避免系统报"未知应用"。
-
-
强制更新场景:
-
- 如需强制更新,可在后端接口返回
forceUpdate: true,前端判断后屏蔽取消按钮,强制用户更新。
进阶优化:断点续传(Windows端)
(Windows 端)
go// 改造Electron主进程download-update方法 ipcMain.handle('download-update', async (event, updateInfo) => { const { version, fileUrl, md5 } = updateInfo; const downloadDir = path.join(app.getPath('userData'), 'updates'); if (!fs.existsSync(downloadDir)) fs.mkdirSync(downloadDir, { recursive: true }); const exePath = path.join(downloadDir, `app-v${version}.exe`); // 检查是否已有部分下载的文件 let startByte = 0; if (fs.existsSync(exePath)) { const stat = fs.statSync(exePath); startByte = stat.size; // 已下载的字节数 } if (startByte > 0) { // 推送断点续传的初始进度 downloadProgress = { transferred: startByte, total: 0, percent: 0 }; event.sender.send('update-progress', downloadProgress); } returnnewPromise((resolve, reject) => { const options = { headers: { 'Range': `bytes=${startByte}-`// 断点续传核心:请求从已下载位置开始的字节 } }; https.get(fileUrl, options, (res) => { const totalLength = startByte + parseInt(res.headers['content-length'], 10); downloadProgress.total = totalLength; // 追加写入(而非覆盖) const writeStream = fs.createWriteStream(exePath, { flags: startByte > 0 ? 'a' : 'w' }); res.pipe(writeStream); // 推送下载进度 res.on('data', (chunk) => { downloadProgress.transferred += chunk.length; downloadProgress.percent = Math.floor((downloadProgress.transferred / totalLength) * 100); event.sender.send('update-progress', downloadProgress); }); // 下载完成校验(逻辑不变) writeStream.on('finish', () => { if (md5) { const fileMd5 = crypto.createHash('md5').update(fs.readFileSync(exePath)).digest('hex'); if (fileMd5 !== md5) { fs.unlinkSync(exePath); return reject(newError('安装包校验失败')); } } resolve({ success: true, path: exePath }); }); writeStream.on('error', (err) => { fs.existsSync(exePath) && fs.unlinkSync(exePath); reject(err); }); }).on('error', (err) => reject(err)); }); });七、总结:统一更新的核心价值
这篇文章讲的方案,通过"封装跨端通用工具+实现Electron主进程功能+复用uni-app页面逻辑",实现了不用Electron官方工具的自动更新,还能兼容已有的Android端逻辑。最大的好处是跨端逻辑统一、自己能掌控全流程、维护成本低,适合用uni-app开发跨端应用的场景。
用这套方案,开发者能完全控制更新的每一步,不用依赖第三方工具,还能保证Windows和Android端的更新体验一致,让用户更新应用更顺畅。
- 如需强制更新,可在后端接口返回