electron-vite_20外部依赖包上线后如何更新

Electron 项目中使用 electron-vite(Vite的Electron 构建工具)时的配置文件,告诉 Vite哪些依赖(包)在打包时不用一起打包,而是运行时让Electron去外部加载;
什么场景会用,比方说你依赖一个声网做音视频能力,然后你业务层本身没啥更新,只是这个SDK有Bug需要修复,这个时候你差量更新声网这个依赖就行了;
为什么有些模块需要被外部化? 举例:dingrtc-electron-sdk;这类第三方 SDK 通常包含:Node.js 原生扩展(C++ 代码编译的二进制模块),无法被 Vite 打包处理

假设要配置依赖dingrtc-electron-sdk去外部加载

typescript 复制代码
const commonjsPackages = [
  'electron',
  'electron/main',
  'dingrtc-electron-sdk',
] as const;
export default defineConfig({
  commonjsExternals(
    { externals: commonjsPackages },
  ),
});
编译后路径
typescript 复制代码
// win电脑右键图标=>属性=>打开文件所在的位置;
// mac公司没有mac有的兄弟可以评论区告诉我怎么弄;
resources/app.asar.unpacked/node_modules/dingrtc-electron-sdk

需要外部依赖 adm-zip

typescript 复制代码
npm i adm-zip -S

实现流程

  • 1.获取本地版本号 resources/app.asar.unpacked/node_modules/dingrtc-electron-sdk下package.json里面的版本号{"version": "0.0.1"}
  • 2.获取文件服务器最新版本号;
  • 3.对比版本号,不一致的时候就去下载zip,并覆盖本地目录; (第三方依赖有时候新版本有问题,得回滚,监测到只要版本号不一致就更新)
使用方法

1.指定线上最新的版本号文件;(可以改为自己的文件地址) const VERSION_URL = 'test.cn/electronDin...';

2.根据版本号获取到zip地址;(可以改为自己的文件地址) const getZipUrl = (version) => https://test.cn/electronDingrtcUpdate/dingrtc-electron-sdk-${version}.zip;

3.在主进程main.js调用

核心代码
typescript 复制代码
const { app } = require('electron');
const fs = require('fs/promises');
const fsSync = require('fs');
const path = require('path');
const https = require('https');
const AdmZip = require('adm-zip'); // 需要安装: npm install adm-zip

// 1. 本地 SDK 目录(asar.unpacked 内)
const localDir = path.join(
  process.resourcesPath,
  'app.asar.unpacked',
  'node_modules',
  'dingrtc-electron-sdk'
);

// 2. 线上版本接口
const VERSION_URL = 'https://test.cn/electronDingrtcUpdate/package.json';

// 3. 线上 zip 包地址
const getZipUrl = (version) => 
  `https://test.cn/electronDingrtcUpdate/dingrtc-electron-sdk-${version}.zip`;


/**
 * 读取本地 SDK 版本号
 */
async function getLocalVersion() {
  try {
    const packageJsonPath = path.join(localDir, 'package.json');
    // 检查文件是否存在
    await fs.access(packageJsonPath);
    
    // 读取版本号
    const content = await fs.readFile(packageJsonPath, 'utf8');
    const packageInfo = JSON.parse(content);
    
    return packageInfo.version || '0.0.0';
  } catch (error) {
    console.warn('获取本地版本失败,可能是首次安装', error.message);
    return '0.0.0'; // 默认为初始版本
  }
}

/**
 * 获取线上版本号
 */
async function getRemoteVersion() {
  return new Promise((resolve, reject) => {
    https.get(VERSION_URL, (response) => {
      let data = '';
      
      response.on('data', (chunk) => {
        data += chunk;
      });
      
      response.on('end', () => {
        try {
          const remoteInfo = JSON.parse(data);
          if (remoteInfo.version) {
            resolve(remoteInfo.version);
          } else {
            reject(new Error('线上版本信息格式不正确'));
          }
        } catch (error) {
          reject(new Error(`解析线上版本失败: ${error.message}`));
        }
      });
    }).on('error', (error) => {
      reject(new Error(`获取线上版本失败: ${error.message}`));
    });
  });
}

/**
 * 比较版本号 (简单比较,适用于 x.y.z 格式)
 * @returns true 如果 remoteVersion 大于 localVersion 则需要更新
 */
function shouldUpdate(localVersion, remoteVersion) {
  const localParts = localVersion.split('.').map(Number);
  const remoteParts = remoteVersion.split('.').map(Number);
  for (let i = 0; i < Math.max(localParts.length, remoteParts.length); i++) {
    const local = localParts[i] || 0;
    const remote = remoteParts[i] || 0;
    
    if (remote > local) return true;
    if (remote < local) return false;
  }
  return false; // 版本相同
}

/**
 * 下载文件到临时路径
 */
async function downloadFile(url, tempFilePath) {
  return new Promise((resolve, reject) => {
    const file = fsSync.createWriteStream(tempFilePath);
    
    https.get(url, (response) => {
      if (response.statusCode !== 200) {
        file.destroy();
        return reject(new Error(`下载失败,状态码: ${response.statusCode}`));
      }
      
      response.pipe(file);
      
      file.on('finish', () => {
        file.close(() => {
          resolve(tempFilePath);
        });
      });
    }).on('error', (error) => {
      fs.unlink(tempFilePath).catch(() => {});
      reject(new Error(`下载过程出错: ${error.message}`));
    });
  });
}

/**
 * 备份原目录
 */
async function backupOriginalDir() {
  const backupDir = `${localDir}_backup_${Date.now()}`;
  
  try {
    // 如果目录存在则备份
    if (fsSync.existsSync(localDir)) {
      await fs.rename(localDir, backupDir);
      console.log(`已备份原目录到: ${backupDir}`);
    }
    return backupDir;
  } catch (error) {
    console.warn(`备份目录失败: ${error.message}`);
    return null;
  }
}

/**
 * 解压并覆盖文件
 */
async function extractAndReplace(zipPath, targetDir) {
  try {
    // 创建目标目录(如果不存在)
    await fs.mkdir(targetDir, { recursive: true });
    // 解压 zip
    const zip = new AdmZip(zipPath);
    zip.extractAllTo(targetDir, true); // true 表示覆盖现有文件
    console.log(`文件已解压到: ${targetDir}`);
    return true;
  } catch (error) {
    throw new Error(`解压文件失败: ${error.message}`);
  }
}

/**
 * 主函数:检查并更新 DingRTC SDK
 * @returns {Object} 更新结果
 */
export async function checkAndUpdateDingRtc() {
  let tempZipPath = null;
  let backupDir = null;
  
  try {
    console.log('开始检查 DingRTC SDK 更新...');
    
    // 1. 获取版本信息
    const [localVersion, remoteVersion] = await Promise.all([
      getLocalVersion(),
      getRemoteVersion()
    ]);
    
    console.log(`本地版本: ${localVersion}, 线上版本: ${remoteVersion}`);
    
    // 2. 比较版本
    // if (!shouldUpdate(localVersion, remoteVersion)) {
    //   return {
    //     updated: false,
    //     message: '已是最新版本',
    //     localVersion,
    //     remoteVersion
    //   };
    // }
    if (localVersion === remoteVersion) {
      console.log('版本相同,无需更新');
      return {
        updated: false,
        message: '已是最新版本',
        localVersion,
        remoteVersion
      };
    }
    // 3. 准备临时文件
    const tempDir = path.join(app.getPath('temp'), `dingrtc_update_${Date.now()}`);
    await fs.mkdir(tempDir, { recursive: true });
    tempZipPath = path.join(tempDir, `dingrtc-sdk-${remoteVersion}.zip`);
    
    // 4. 下载更新包
    console.log(`开始下载更新包: ${getZipUrl(remoteVersion)}`);
    await downloadFile(getZipUrl(remoteVersion), tempZipPath);
    
    // 5. 备份原目录
    backupDir = await backupOriginalDir();
    
    // 6. 解压并替换
    console.log('开始更新 SDK...');
    await extractAndReplace(tempZipPath, localDir);
    
    // 7. 清理临时文件
    await fs.rm(tempDir, { recursive: true, force: true });
    // 8. 验证更新结果
    const newLocalVersion = await getLocalVersion();
    if (newLocalVersion !== remoteVersion) {
      throw new Error(`更新验证失败,实际版本: ${newLocalVersion}, 期望版本: ${remoteVersion}`);
    }
    
    console.log('DingRTC SDK 更新成功');
    // 升级成功删除备份
    await fs.remove(backupDir);
    return {
      updated: true,
      message: '更新成功',
      localVersion,
      remoteVersion,
      newLocalVersion
    };
    
  } catch (error) {
    console.error('更新失败:', error.message);
    // 回滚操作:如果有备份,尝试恢复
    if (backupDir && fsSync.existsSync(backupDir)) {
      try {
        // 先删除可能损坏的目录
        if (fsSync.existsSync(localDir)) {
          await fs.rm(localDir, { recursive: true, force: true });
        }
        // 恢复备份
        await fs.rename(backupDir, localDir);
        console.log('已回滚到更新前的版本');
      } catch (rollbackError) {
        console.error('回滚失败:', rollbackError.message);
      }
    }
    return {
      updated: false,
      message: `更新失败: ${error.message}`,
      error: error.message
    };
  } finally {
    // 确保临时文件被清理
    if (tempZipPath && fsSync.existsSync(tempZipPath)) {
      try {
        await fs.unlink(tempZipPath);
      } catch (cleanupError) {
        console.warn('清理临时文件失败:', cleanupError.message);
      }
    }
  }
}
主进程main.js使用
typescript 复制代码
const { app } = require("electron");

import { checkAndUpdateDingRtc } from './update-dingrtc.js';

app.whenReady().then(async () => {
  await checkAndUpdateDingRtc();
});
相关推荐
fly啊2 分钟前
前端 vs 后端请求:核心差异与实战对比
前端
陈哥聊测试7 分钟前
当DevOps落地实施撞上技术债务,如何量化债务突破困局
前端·自动化运维·devops
sorryhc12 分钟前
【AI解读源码系列】ant design mobile——CapsuleTabs胶囊选项卡
前端·javascript·react.js
狗头大军之江苏分军17 分钟前
频繁跳槽和稳定工作有什么区别?真的比稳定工作的人差吗?
前端·后端
木子雨廷20 分钟前
Flutter 局部刷新小组件汇总
前端·flutter
用户527096487449025 分钟前
组件库按需引入改造
前端
巧克力7927 分钟前
js数组去重的方法
javascript·面试
Sheeep30 分钟前
Cursor 的使用之学会使用 cursor rule
javascript·后端
CryptoRzz36 分钟前
使用Java对接印度股票市场API开发指南
前端·后端
码间舞36 分钟前
道路千万条,安全第一条!要对付XSS等攻击你有什么手段?你知道什么是CSP吗?
前端·后端·安全