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();
});
相关推荐
会豪2 小时前
Electron-Vite (一)快速构建桌面应用
前端
中微子2 小时前
React 执行阶段与渲染机制详解(基于 React 18+ 官方文档)
前端
唐某人丶2 小时前
教你如何用 JS 实现 Agent 系统(2)—— 开发 ReAct 版本的“深度搜索”
前端·人工智能·aigc
中微子2 小时前
深入剖析 useState产生的 setState的完整执行流程
前端
遂心_2 小时前
JavaScript 函数参数传递机制:一道经典面试题解析
前端·javascript
小徐_23332 小时前
uni-app vue3 也能使用 Echarts?Wot Starter 是这样做的!
前端·uni-app·echarts
RoyLin2 小时前
TypeScript设计模式:适配器模式
前端·后端·node.js
遂心_3 小时前
深入理解 React Hook:useEffect 完全指南
前端·javascript·react.js
Moonbit3 小时前
MoonBit 正式加入 WebAssembly Component Model 官方文档 !
前端·后端·编程语言
龙在天3 小时前
ts中的函数重载
前端