Electron 自动化上传安装包与自动更新

1,628 阅读11分钟

Electron 自动化上传安装包与自动更新指南

Electron 是一个强大的开源框架,开发者可以使用 HTML、CSS 和 JavaScript 等前端技术栈来构建跨平台的桌面应用程序。Electron 通过结合 Chromium 和 Node.js,使开发者可以在熟悉的前端环境下构建支持 Windows、macOS 和 Linux 的应用。这种“写一次代码,运行在多个平台”的特性大大降低了开发和维护成本。

凭借其跨平台支持、前端技术栈的易用性以及丰富的系统级 API,Electron 被越来越多的开发者和企业采用。开发者无需掌握复杂的原生语言即可快速构建功能强大的桌面应用,同时确保在不同操作系统间的一致体验。这使得从初创公司到大型企业都能借助 Electron 快速推出产品,验证市场需求。

本文将重点介绍如何使用 electron-builder 进行自动化打包、上传以及自动更新应用版本信息。


环境配置

在打包之前,我们需要说明环境变量的配置。环境变量的主要作用是区分测试环境和正式环境。我们可以在项目中创建一个 config 文件夹,并在其中创建 default.jsonproduction.json 文件。这两个文件分别用于存放开发和生产环境的配置信息,包括请求路径、上传信息等内容。以下是文件的基本结构示例:

    // config/default.json
    {
        "CONFIG_URL": "http://localhost:3000/api",
        "OSS_UPLOAD_URL": "http://localhost:3000/oss-upload",
        "OSS_BUCKET": "your-bucket-name",
        "OSS_REGION": "your-region",
        "OSS_ACCESS_KEY_ID": "your-access-key-id",
        "OSS_ACCESS_KEY_SECRET": "your-access-key-secret"
    }
    
    // config/production.json
    {
        "CONFIG_URL": "https://your-production-url/api",
        "OSS_UPLOAD_URL": "https://your-production-url/oss-upload",
        "OSS_BUCKET": "your-production-bucket-name",
        "OSS_REGION": "your-production-region",
        "OSS_ACCESS_KEY_ID": "your-production-access-key-id",
        "OSS_ACCESS_KEY_SECRET": "your-production-access-key-secret"
    }

图片:

4F60163A-335C-4273-A2C0-5082E5748FEB.png

通过这种方式,我们可以方便地在不同环境下使用不同的配置。


自动化上传安装包与自动更新配置

为了提升开发效率,Electron 提供了 electron-builder 工具,用于自动化应用的打包、上传及版本管理。以下将深入探讨如何配置打包脚本、实现将打包文件分片上传至阿里云 OSS,并自动更新应用版本信息。


一、自动化上传安装包

1. 配置打包脚本

首先,在 package.json 文件中添加打包脚本,并通过环境变量来区分不同的平台环境。以下示例展示了如何为 Windows 和 macOS 平台配置打包命令:

    
    {
        "scripts": {
            "build:win": "NODE_ENV=production electron-builder --win && node script/buildUpload.js win32",
            "build:mac": "cross-env NODE_ENV=production CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --mac && node script/buildUpload.js darwin"
        }
    }

其中,buildUpload.js 是负责打包完成后执行上传操作的脚本。通过设置环境变量,脚本可以在不同的环境下工作,例如区分生产环境和测试环境。


二、读取配置并初始化 OSS 客户端

1. 读取应用版本和平台信息

buildUpload.js 中,我们需要从 package.json 读取应用版本号,并获取打包的目标平台(如 macOS、Windows)。这可以通过命令行参数传递,也可以设置默认平台为 macOS:

    
    const packageJson = require('../package.json');
    const version = packageJson.version;
    const processPlatform = process.argv[2] || 'darwin';

2. 初始化 OSS 客户端

接下来,我们从配置文件中读取阿里云 OSS 的相关配置信息,并初始化 OSS 客户端。这样可以确保后续文件上传时的正确连接。

    const OSS = require('ali-oss');
    const config = require('config');
    
    // 初始化 OSS 客户端
    const client = new OSS({
        region: config.get('OSS_REGION'),
        accessKeyId: config.get('OSS_ACCESS_KEY_ID'),
        accessKeySecret: config.get('OSS_ACCESS_KEY_SECRET'),
        bucket: config.get('OSS_BUCKET'),
        secure: true,
    });

三、获取文件路径与对象名称

在上传过程中,我们需要根据不同的平台类型来确定文件路径和对象名称。不同平台的打包文件格式不同,例如 Windows 平台的文件后缀为 .exe,macOS 则为 .dmg

    function getFilePathAndObjectName() {
        let filePath, objectName;
        if (processPlatform === 'win32') {
            filePath = path.join('release', `dm_desk Setup ${version}.exe`);
            objectName = `electron/dm_desk/${version}-${Date.now()}-win32.exe`;
        } else if (processPlatform === 'darwin') {
            filePath = path.join('release', `dm_desk-${version}.dmg`);
            objectName = `electron/dm_desk/${version}-${Date.now()}-darwin.dmg`;
        }
        return { filePath, objectName };
    }

四、分片上传至阿里云 OSS

为了提高上传大文件的效率,我们可以使用阿里云 OSS 的分片上传技术。通过 multipartUpload 方法,将文件分片上传,能够有效减少上传失败的几率并提高成功率。当然这里按照情况,如果你们有自己的存储方式,可以不使用阿里云的。

    async function multipartUploadFile() {
        try {
            const { filePath, objectName } = getFilePathAndObjectName();
            console.log(`开始上传文件: ${filePath}`);
    
            // 分片上传文件
            const result = await client.multipartUpload(objectName, filePath, {
                progress: async (percentage) => {
                    console.log(`上传进度: ${Math.round(percentage * 100)}%`);
                },
                partSize: 1024 * 1024 * 10, // 每片大小为10MB
                timeout: 120000,
            });
    
            console.log('文件上传成功');
            const downloadUrl = result.res.requestUrls?.[0] || '';
            if (downloadUrl) {
                const updateUrl = `${config.get('OSS_UPLOAD_URL')}/${objectName}`;
                await uploadVersionInfo(updateUrl);
            }
        } catch (err) {
            console.error('文件上传失败:', err);
        }
    }

五、上传版本信息

文件上传完成后,需将版本信息上传至服务器以便应用的自动更新功能获取到最新的版本。我们可以使用 axios 库发送 POST 请求,将下载链接和版本号等信息上传到指定的服务器接口。

    
    async function uploadVersionInfo(downloadUrl) {
        try {
            const params = {
                project: 'dw_desk',
                platform: processPlatform,
                title: '发现新版本',
                desc: '新版本功能更新',
                version: version,
                downloadUrl: downloadUrl,
                operator: getGitUserInfo().name || 'admin',
            };
    
            console.log('上传版本信息:', params);
            await axios.post(`${config.get('CONFIG_URL')}`, params, { timeout: 10000 });
            console.log('版本信息上传成功');
        } catch (error) {
            console.error('版本信息上传失败:', error);
        }
    }

六、获取当前 Git 提交者信息

为了记录发布操作的执行者,我们可以通过 Git 获取当前提交者的信息。这样,每次发布操作都能明确记录是哪位开发人员进行了版本发布。

    
    function getGitUserInfo() {
        const name = execSync('git config user.name').toString().trim();
        const email = execSync('git config user.email').toString().trim();
        return { name, email };
    }

执行本地打包脚本的日志 图片示例:

040F23A6-2D4C-4600-B5D3-7401526B6EF4.png

通过上面的的介绍,我们可以看到如何使用 Electron 和 electron-builder 实现跨平台应用的打包、分片上传至阿里云 OSS 以及自动更新。该流程不仅简化了开发者的操作,还能确保应用在多平台下的一致性和可维护性。并且我们是把版本的信息上传到了自己的服务器上,并且简单的搭建了一个后台页面,下面是这个后台页面的简单介绍。

版本管理的后台操作系统

D611AF01-404E-44F3-B805-D3D1088FF06A.png

后台的主要功能是查看对应的版本信息和以及下面我们要讲到的控制版本的更新,通过停启用来控制,停用当前版本之后,如果桌面端启动的时候,就会自动更新或者回退到目标版本,这样就保证版本信息的可控性,很大程度的增加了灵活性能。说完了自动上传和版本控制管理,下面我们所一下自动更新的逻辑吧。

自动更新

1. 应用启动时进行更新检查

当应用程序启动时,自动更新机制应首先检查服务器上是否存在新的版本。这可以通过 Electron 的 app.whenReady() 方法来确保代码只在应用完全初始化后执行。此时,除了创建应用窗口外,还需要立即检查更新,以确保用户尽快获得更新通知。

app.whenReady().then(() => {
    createWindow();   // 创建主窗口
    autoUpdateApp();  // 启动自动更新逻辑
});
  • createWindow() : 创建并显示应用的主窗口,这是应用程序的用户界面。通常会加载 HTML 文件,包含用户的核心功能。
  • autoUpdateApp() : 自动更新的主要逻辑,包含检查服务器上是否存在新版本的功能。如果有更新,则会启动下载并提示用户。

通过这种方式,更新检查与应用启动过程同时进行,不会影响用户的正常使用。


2. 创建用于显示下载进度的窗口

在用户确认进行更新时,为了避免用户在下载过程中无所适从,我们需要提供可视化的进度条窗口。这个窗口可以通过 BrowserWindow 创建。这个窗口不会包含边框或其他不必要的界面元素,它的唯一功能就是显示下载进度。窗口会始终在应用的最前端显示,确保用户能够随时看到下载进度。

let progressWindow;

function createProgressWindow() {
    if (!progressWindow) {
        progressWindow = new BrowserWindow({
            width: 400,
            height: 150,
            frame: false,  // 无边框,保证界面简洁
            resizable: false,  // 禁止用户调整窗口大小
            alwaysOnTop: true,  // 确保窗口始终显示在其他窗口上方
            webPreferences: {
                nodeIntegration: true,  // 启用Node.js模块
                contextIsolation: false,
            }
        });

        progressWindow.loadFile('progress.html');  // 加载显示进度的HTML文件
    }
}
  • frame: false: 禁用窗口的默认边框,使得窗口看起来更简洁,只显示进度条。
  • alwaysOnTop: true: 确保用户在使用其他窗口时,进度窗口仍然始终可见。
  • loadFile('progress.html') : 这个文件将包含进度条的 HTML 和 CSS,用户可以根据需要自定义进度条的样式。

此窗口能够清晰地向用户展示下载进度,避免更新过程中的不确定性。


3. 检查服务器上的新版本信息

要实现自动更新,应用程序需要从服务器获取最新的版本信息。我们通常会将最新版本信息保存在服务器的一个 API 端点上,应用通过 axios 发出 GET 请求来获取这些信息。服务器会返回最新的版本号和下载链接。如果版本号大于当前应用的版本号,说明有更新可用。此时,我们会通过弹窗通知用户有新版本,并询问用户是否愿意立即更新。

const axios = require('axios');
const currentVersion = app.getVersion();  // 获取当前应用的版本号

function checkForUpdates() {
    axios.get('https://server.com/latest-version')  // 向服务器获取最新版本信息
        .then(response => {
            const { version, downloadUrl } = response.data;  // 解析返回的数据
            if (version > currentVersion) {  // 如果版本号大于当前版本,说明有更新
                dialog.showMessageBox({
                    type: 'info',
                    title: '更新可用',
                    message: `检测到新版本 ${version},是否立即更新?`,
                    buttons: ['更新', '稍后']
                }).then(result => {
                    if (result.response === 0) {
                        downloadUpdate(downloadUrl);  // 用户选择立即更新,开始下载更新
                    }
                });
            }
        })
        .catch(error => {
            console.error('检查更新失败', error);  // 如果请求失败,输出错误信息
        });
}
  • app.getVersion() : 获取当前应用程序的版本号,以便与服务器上的最新版本进行比较。
  • axios.get() : 向服务器发送 GET 请求,获取最新的版本信息。这些信息通常以 JSON 格式返回,包括最新版本号和下载地址。
  • dialog.showMessageBox() : 当发现新版本时,弹出对话框,询问用户是否愿意更新。用户可以选择“立即更新”或“稍后再说”。

当然在这里你可以做做一些细微的区分,比如本地已经有下载的文件,就不再执行下载这一步,直接执行安装流程

// savePath 就是你本地下载的路径
if (fs.existsSync(savePath)) {
  downloadEnd = true;
  installerPath = savePath;
  console.log("文件已存在,跳过下载:", savePath);
  return;
}

此步骤确保应用始终保持与服务器同步,能够及时为用户提供更新。


4. 下载更新包并显示下载进度

在用户确认更新后,应用程序需要开始下载更新包。由于下载文件可能较大,我们需要实时向用户展示下载的进度。通过使用 https 模块下载文件,并根据文件大小计算下载的百分比进度。这个进度信息会通过 progressWindow 实时展示给用户。

const https = require('https');
const fs = require('fs');
const path = require('path');

function downloadUpdate(downloadUrl) {
    const file = fs.createWriteStream(path.join(__dirname, 'update.zip'));  // 创建写入流用于保存下载的文件
    let receivedBytes = 0;

    https.get(downloadUrl, response => {
        const totalBytes = parseInt(response.headers['content-length'], 10);  // 获取文件的总大小

        response.on('data', chunk => {
            receivedBytes += chunk.length;  // 每次下载部分数据时更新已接收的字节数
            const progress = Math.floor((receivedBytes / totalBytes) * 100);  // 计算下载进度百分比
            progressWindow.webContents.send('updateProgress', progress);  // 通过进度窗口显示下载进度
        });

        response.pipe(file);  // 将下载的数据写入文件

        file.on('finish', () => {
            file.close(() => installUpdate());  // 下载完成后关闭文件并开始安装
        });
    }).on('error', error => {
        console.error('下载更新失败', error);  // 处理下载过程中发生的错误
    });
}
  • fs.createWriteStream() : 创建一个可写入的文件流,更新文件将保存到本地磁盘。
  • response.on('data') : 每次接收到文件数据时,更新当前的已下载字节数,并根据总字节数计算进度。
  • progressWindow.webContents.send() : 将下载进度百分比发送给前端的进度窗口,使得用户能够看到实时更新。

通过此步骤,用户可以清楚地知道更新包的下载进度,减少因等待而带来的焦虑。


5. 安装更新包

下载完成后,应用程序需要根据用户操作系统的类型选择不同的安装方式。我们需要分别处理 macOS 和 Windows 平台的安装。

macOS: 安装 .dmg 文件

在 macOS 上,更新通常以 .dmg 文件形式提供。我们可以使用系统命令 hdiutil 来挂载 .dmg 文件并安装应用。

const { exec } = require('child_process');
const path = require('path');

function installUpdate() {
    if (process.platform === 'darwin') {
        exec(`hdiutil attach ${path.join(__dirname, 'update.dmg')}`, (error, stdout, stderr) => {
            if (!error) {
                // 成功挂载后可以执行安装流程
                dialog.showMessageBox({
                    type: 'info',
                    title: '更新完成',
                    message: '安装完成,请重新启动应用以应用更新。',
                    buttons: ['重新启动']
                }).then(() => {
                    app.quit();  // 退出应用以便重新启动
                });
            }
        });
    }
}
  • hdiutil attach: 系统命令,用于挂载 .dmg 文件,使其能够被安装。
  • app.quit() : 安装完成后,应用将退出并等待重新启动。

Windows: 运行 .exe 安装程序

在 Windows 上,我们通常会通过 .exe 安装程序来进行更新。此处使用 exec() 直接运行下载的安装程序。

function installUpdate() {
    if (process.platform === 'win32') {
        const updateFile = path.join(__dirname, 'update.exe');
        exec(updateFile, (error, stdout, stderr) => {
            if (!error) {
                dialog.showMessageBox({
                    type: 'info',
                    title: '更新完成',
                    message: '安装完成,请重新启动应用以应用更新。',
                    buttons: ['重新启动']
                }).then(() => {
                    app.quit();  // 退出并重新启动应用
                });
            }
        });
    }
}
  • exec() : 在 Windows 系统中执行 .exe 文件,启动安装程序。

安装过程根据操作系统的不同选择合适的安装方法,确保更新能够顺利完成。


6. 完成安装并重启应用

一旦安装完成,应用程序将提示用户重启应用以应用更新。此时,用户需要手动确认重启操作。完成重启后,应用将启动最新的版本,在这一需要执行一下删除本地安装包的逻辑哈,不然一直安装最新的安装包,后面更新的次数多了,用户本地会有很多的安装包信息。

删除逻辑比较简单就一行代码

fs.unlink(savePath, () => reject(err));

然后就是弹窗提示:

dialog.showMessageBox({
    type: 'info',
    title: '更新成功',
    message: '应用已成功更新,是否立即重新启动?',
    buttons: ['重启', '稍后']
}).then(result => {
    if (result.response === 0) {
        app.relaunch();  // 重新启动应用
        app.exit();  // 退出当前运行的应用
    }
});
  • app.relaunch() : 重新启动应用程序,确保更新生效。

这样,我们就为 Electron 应用程序实现了完整的自动更新流程,包括版本检查、下载、安装以及重启。每一步都有详尽的反馈和处理机制,确保用户体验顺畅。 当然这里面还有一个进度条的 html 文件。主要的代码是样式和通信在这里也简单的粘贴一下:

<body>
        <div class="container">
            <h2>正在更新...</h2>
            <div id="progress-bar">
                <div id="progress-fill"></div>
            </div>
            <div id="progress-text">准备开始更新</div>
        </div>
        <script>
            const { ipcRenderer } = require('electron');

            ipcRenderer.on('update-progress', (event, progress, message) => {
                const progressFill = document.getElementById('progress-fill');
                const progressText = document.getElementById('progress-text');
                progressFill.style.width = progress + '%';
                progressText.textContent = message;
                if (message === '更新完成' && progress === 100 && window) {
                    setTimeout(() => {
                        window?.close();
                    }, 1000);
                }
            });
        </script>
    </body>

下面是 Mac 系统的启动更新的简单示例图:

1、检测到有版本更新的时候: 98AEF641-53F1-432A-B63A-9A11F1E49DB8.png

2、下载远程安装包

FF61E3CB-DFBF-49AD-87C4-003EA4BB0D52.png

3、安装

53E41A54-9945-41C6-BD3D-B2A9ADD21648.png

4、最后是安装完成的弹窗提示

B17B8A62-641E-487D-A410-137A210E74C9.png

结语

通过上面的详细介绍,我们不仅掌握了如何使用 Electron 和 electron-builder 实现自动化打包与上传,还探讨了分片上传至阿里云 OSS 以及自动更新应用的最佳实践。作为一种广受开发者喜爱的跨平台开发框架,Electron 大大降低了开发桌面应用的技术门槛,让更多前端开发者能够进入桌面应用开发的领域。其强大的打包和更新工具,如 electron-builder,在帮助开发者快速迭代产品和发布更新的同时,也减少了手动操作的复杂性,提高了开发效率。

此外,自动化上传与版本管理功能在实际的产品开发和发布流程中发挥了至关重要的作用。通过分片上传、版本信息管理、Git 用户追踪等功能,不仅确保了大文件上传的成功率,还使得版本控制变得更加透明和可追溯。特别是在多平台应用的开发过程中,这些功能为确保一致性和减少差异提供了可靠的保障。

值得一提的是,自动更新功能的集成极大地提升了用户体验。用户无需手动下载更新文件,只需在应用启动时自动完成版本升级,这不仅节省了用户的时间,还提升了应用的稳定性和安全性。通过后台的版本管理系统,开发团队可以灵活地控制版本的推送和回滚,为应用的持续改进提供了有力的支持。

未来,随着 Electron 社区的持续发展和优化,自动化打包和更新流程将变得更加智能和高效。开发者可以期待更多的工具和插件来进一步简化工作流,从而专注于应用功能本身的开发和创新。无论是初创企业的快速产品迭代,还是大型企业的持续产品更新,Electron 的自动化工具链将成为不可或缺的利器,为桌面应用的开发和维护带来更多的可能性。

总之,利用 Electron 构建跨平台桌面应用不仅是提升开发效率的选择,也是对未来技术趋势的积极拥抱。在这个快速变化的技术世界中,掌握高效的开发工具和自动化流程将为团队和企业带来更大的竞争优势,也能让开发者从繁琐的手动操作中解放出来,专注于为用户提供更加优秀的产品体验。

作者:洞窝-海林