项目中存在一些大文件需要在用的时候才会去下载,并且下载过程需要用户能够感知,下面是对这个功能的实现。
功能概述
我们实现的下载系统具备以下核心功能:
- 多阶段下载进度追踪(准备、元数据获取、版本比对、下载、解压)
- 基于 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"))
})
}