实现 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"))
    })
}
相关推荐
Doris_20232 小时前
Python条件判断语句 if、elif 、else
前端·后端·python
Doris_20232 小时前
Python 模式匹配match case
前端·后端·python
森林的尽头是阳光2 小时前
vue防抖节流,全局定义,使用
前端·javascript·vue.js
YiHanXii2 小时前
React.memo 小练习题 + 参考答案
前端·javascript·react.js
zero13_小葵司2 小时前
Vue 3 前端工程化规范
前端·javascript·vue.js
Yolanda_20222 小时前
vue-sync修饰符解析以及切换iframe页面进行保存提示功能的思路
前端·javascript·vue.js
伍哥的传说2 小时前
Vite Plugin PWA – 零配置构建现代渐进式Web应用
开发语言·前端·javascript·web app·pwa·service worker·workbox
alphageek82 小时前
Electron开源库入门教程:跨平台桌面应用框架
javascript·其他·electron·开源