脱离 Electron autoUpdater:uni-app跨端更新:Windows+Android统一实现方案

前置说明

本文方案适用于「基于uni-app开发、同时打包Android App和Windows Electron客户端」的跨端应用,核心解决"双端更新逻辑不统一、依赖Electron autoUpdater不灵活"的问题。

技术前提:已具备Android端更新逻辑+后端版本接口,Electron版本≥13,uni-app版本≥3.0,Node.js≥14。

目录

  1. 方案核心:用一套逻辑搞定双端更新

  2. 核心流程:双端通用的更新闭环

  3. 具体实现:三步落地跨端更新 3.1 第一步:封装跨端公共工具(复用Android逻辑) 3.2 第二步:Electron主进程实现核心能力 3.3 第三步:uni-app渲染进程(Windows端)实现 3.4 Android端兼容性保障

  4. 兼容性与可靠性:这套方案靠谱吗?

  5. 实施注意事项:避坑关键

  6. 进阶优化:断点续传(Windows端)

  7. 总结:统一更新的核心价值

在开发同时支持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端,更新流程都遵循下面这套统一逻辑,保证两端用户的更新体验一致:

  1. 应用启动后,自动调用后端接口,获取最新版本信息(包括版本号、发布时间、安装包下载链接、校验码等);

  2. 对比本地当前版本和后端的最新版本,判断是否需要更新;

  3. 如果需要更新,先检查本地有没有已经下载好的最新版本安装包,避免重复下载;

  4. 如果本地没有缓存,就提示用户下载(也可以设置成自动静默下载),并实时显示下载进度;

  5. 下载完成后,可选步骤:校验安装包是否完整(比如用MD5或SHA256校验,避免下载的文件损坏);

  6. 提示用户安装,用户确认后关闭当前应用,启动安装包完成更新;

  7. 支持手动清理旧的更新缓存,避免占用过多存储空间;

三、具体实现步骤

具体实现分为三步:"封装跨端通用工具""实现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端不用改核心逻辑,只要注意两点:

  1. 复用前面封装的通用工具(updateHelper.js)里的逻辑,比如判断版本、弹提示框等,保证Android和Windows端的用户操作体验一致;

  2. 后端接口根据平台返回对应的安装包链接(给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端的功能,避免崩溃;

五、实施注意事项:避坑关键

  1. 权限问题:
  • 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端的更新体验一致,让用户更新应用更顺畅。

相关推荐
PieroPC18 小时前
用FastAPI 一个 后端 和 两个前端 原生HTML/CSS/JS 、Vue3 写一个博客系统 例
前端·后端
wulijuan88866618 小时前
BroadcastChannel API 同源的多个标签页可以使用 BroadcastChannel 进行通讯
前端·javascript·vue.js
kilito_0118 小时前
数字时钟翻页效果
javascript·css·css3
Van_Moonlight18 小时前
RN for OpenHarmony 实战 TodoList 项目:今日任务数量统计
javascript·开源·harmonyos
逝川长叹18 小时前
利用 SSI-COV 算法自动识别线状结构在环境振动下的模态参数研究(Matlab代码实现)
前端·算法·支持向量机·matlab
xkxnq19 小时前
第一阶段:Vue 基础入门(第 13天)
前端·javascript·vue.js
qq_4198540519 小时前
Excel预览
前端
薛晓刚19 小时前
MySQL的replace使用分析
android·adb
PieroPc19 小时前
用FastAPI 后端 和 Vue3 前端写一个博客系统 例
前端·vue·fastapi