实现 Electron 资源下载与更新:实时进度监控

项目中存在一些大文件需要在用的时候才会去下载,并且下载过程需要用户能够感知,下面是对这个功能的实现。

功能概述

我们实现的下载系统具备以下核心功能:

  • 多阶段下载进度追踪(准备、元数据获取、版本比对、下载、解压)
  • 基于 YAML 配置文件的版本管理
  • 断点续传与缓存机制
  • 实时速度计算与进度反馈
  • 完整的错误处理与状态反馈

核心代码

IPC 通信设置

javascript 复制代码
// 主进程注册处理器
module.exports.downloadHander = () => {
    ipcMain.handle("download-sources-by-link-to-path", async (event, params) => {
        // 处理下载请求
    })
}

在 Electron 中,主进程与渲染进程之间通过 IPC(进程间通信)进行通信。我们使用 ipcMain.handle 注册一个异步处理器,渲染进程可以通过 ipcRenderer.invoke 调用此方法。

多阶段进度追踪

javascript 复制代码
let state = { phase: "开始下载", progress: 0 }
const updateProgress = (newState) => {
    state = { ...state, ...newState }
    event.sender.send("download-progress", state)
}

我们定义了状态对象和更新函数,确保在每个关键阶段都能向渲染进程发送进度更新。

版本比对机制

javascript 复制代码
// 检查本地版本与远程版本差异
let needDownload = true
if (await fsExtra.pathExists(cacheYmlPath)) {
    const localYml = yaml.load(await fsExtra.readFile(cacheYmlPath, "utf8"))
    needDownload = localYml.version !== remoteYml.version
}

通过比较本地和远程的 YAML 配置文件中的版本号,决定是否需要下载新资源,避免不必要的下载。

带进度条的下载实现

javascript 复制代码
// 使用 Stream Pipeline 实现带进度追踪的下载
await pipeline(
    res.body,
    new Transform({
        transform(chunk, _, callback) {
            downloaded += chunk.length
            const progress = totalSize > 0 
                ? Math.min(80, 30 + (downloaded / totalSize) * 50) 
                : 30 + (downloaded / 1e6) * 0.1
            updateProgress({
                progress: Math.floor(progress),
                speed: downloaded / ((Date.now() - startTime) / 1000)
            })
            callback(null, chunk)
        }
    }),
    writer
)

这里使用了 Node.js 的 stream/promises 模块中的 pipeline 方法,确保流资源正确管理。通过 Transform 流拦截数据传输过程,实时计算下载进度和速度。

解压处理

javascript 复制代码
// 使用 AdmZip 解压下载的资源
updateProgress({ phase: "解压资源中", progress: 85 })
const zip = new AdmZip(resourcePath)
await new Promise((resolve, reject) => {
    zip.extractAllToAsync(dirPath, true, (err) => {
        err ? reject(err) : resolve()
    })
})

下载完成后,使用 adm-zip 库进行解压操作,并更新进度状态。

错误处理

javascript 复制代码
try {
    // 所有下载和解压操作
} catch (error) {
    updateProgress({ 
        phase: "下载错误,请重试", 
        progress: -1, 
        status: "exception" 
    })
    return {
        success: false,
        error: error.message,
        code: error.code || "UNKNOWN"
    }
}

完整的 try-catch 块确保任何错误都能被捕获并反馈给用户,同时提供详细的错误信息。

资源清理

javascript 复制代码
// 应用退出时清理缓存
app.on("before-quit", async () => {
    fsExtra.emptyDir(path.join(path.resolve("."), "cache"))
})

将资源下载在cache文件夹下以便可能的二次操作,在应用退出时自动清理缓存目录,避免占用过多磁盘空间。

使用示例

在渲染进程中,可以这样使用下载功能:

vue 复制代码
<template>
  <div>
    <button @click="startDownload">开始下载</button>
    <div v-if="progress">
      阶段: {{ progress.phase }} 
      进度: {{ progress.progress }}%
      <span v-if="progress.speed">
        速度: {{ (progress.speed / 1024).toFixed(2) }} KB/s
      </span>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { ipcRenderer } from 'electron'

const progress = ref(null)

const startDownload = async () => {
  try {
    const result = await ipcRenderer.invoke('download-sources-by-link-to-path', {
      downloadLink: 'https://example.com/update.yml',
      dirPath: '/path/to/install',
      cachePath: '/path/to/cache'
    })
    
    if (result.success) {
      console.log('下载成功')
    } else {
      console.error('下载失败:', result.error)
    }
  } catch (error) {
    console.error('调用下载接口失败:', error)
  }
}

// 监听进度更新
const progressHandler = (event, newProgress) => {
  progress.value = newProgress
}

onMounted(() => {
  ipcRenderer.on('download-progress', progressHandler)
})

onUnmounted(() => {
  ipcRenderer.off('download-progress', progressHandler)
})
</script>

完整代码

js 复制代码
const { ipcRenderer, ipcMain, dialog, app } = require("electron")
const yaml = require("js-yaml")
const AdmZip = require("adm-zip")
const fetch = require("node-fetch")
const fsExtra = require("fs-extra");
const path = require("path");
const { pipeline } = require('stream/promises')
const { Transform } = require('stream');

// 资源文件下载
module.exports.downloadHander = () => {
    ipcMain.handle("download-sources-by-link-to-path", async (event, params) => {
        const { downloadLink, dirPath, cachePath } = params
        const startTime = Date.now()
        let state = { phase: "开始下载", progress: 0 }
        // 初始化状态
        const updateProgress = (newState) => {
            state = { ...state, ...newState }
            event.sender.send("download-progress", state)
        }
        try {
            // 阶段1: 准备目录
            updateProgress({ phase: "校验下载环境", progress: 5 })
            await fsExtra.ensureDir(cachePath)
            await fsExtra.ensureDir(dirPath)
            // 阶段2: 获取元数据
            updateProgress({ phase: "获取更新信息", progress: 10 })
            const ymlRes = await fetch(downloadLink)
            if (!ymlRes.ok) throw new Error(`YML获取失败: ${ymlRes.statusText}`)

            const ymlText = await ymlRes.text()
            const remoteYml = yaml.load(ymlText)
            const cacheYmlPath = path.join(cachePath, "manifest.yml")
            // 阶段3: 版本比对
            updateProgress({ phase: "检查更新环境", progress: 20 })
            let needDownload = true
            if (await fsExtra.pathExists(cacheYmlPath)) {
                const localYml = yaml.load(await fsExtra.readFile(cacheYmlPath, "utf8"))
                needDownload = localYml.version !== remoteYml.version
            } else {
                const ymlPath = path.join(cachePath, "manifest.yml")
                await fsExtra.writeFile(ymlPath, ymlText)
            }
            const resourceUrl = new URL(remoteYml.path, downloadLink.replace(/\/[^/]*$/, "/"))
            const resourcePath = path.join(cachePath, path.basename(resourceUrl.pathname))
            const havenResource = await fsExtra.pathExists(resourcePath)
            if (needDownload || !havenResource) {
                // 阶段4: 下载资源
                updateProgress({ phase: "正在下载", progress: 30 })
                // 带进度下载
                const res = await fetch(resourceUrl)
                if (!res.ok) throw new Error(`资源下载失败: ${res.statusText}`)
                const writer = fsExtra.createWriteStream(resourcePath)
                let downloaded = 0
                const totalSize = parseInt(res.headers.get("content-length"), 10) || 0
                await pipeline(
                    res.body,
                    new Transform({
                        transform(chunk, _, callback) {
                            downloaded += chunk.length
                            const progress = totalSize > 0 ? Math.min(80, 30 + (downloaded / totalSize) * 50) : 30 + (downloaded / 1e6) * 0.1 // 每MB增加0.1%
                            updateProgress({
                                progress: Math.floor(progress),
                                speed: downloaded / ((Date.now() - startTime) / 1000)
                            })
                            callback(null, chunk)
                        }
                    }),
                    writer
                )
            } 
            // 阶段5: 解压处理
            updateProgress({ phase: "解压资源中", progress: 85 })
            const zip = new AdmZip(resourcePath)
            await new Promise((resolve, reject) => {
                zip.extractAllToAsync(dirPath, true, (err) => {
                    err ? reject(err) : resolve()
                })
            })
            updateProgress({ phase: "安装完成", progress: 100, status: "success" })
            return { success: true }
        } catch (error) {
            updateProgress({ phase: "下载错误,请重试", progress: -1, status: "exception" })
            return {
                success: false,
                error: error.message,
                code: error.code || "UNKNOWN"
            }
        }
    })
    // 应用退出清理
    app.on("before-quit", async () => {
        fsExtra.emptyDir(path.join(path.resolve("."), "cache"))
    })
}
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax