在开发Electron应用时,提供良好的用户体验至关重要,尤其是在下载大文件时。用户需要知道下载进度、预计完成时间以及当前下载速度。本文将详细介绍如何在Electron应用中实现实时下载进度显示功能,从主进程到渲染进程的完整流程。
技术栈是electron+vue3作为示例,其它的技术栈同样可以使用
系统架构概述
实现下载进度显示功能需要以下三个主要组件协同工作:
- 主进程(Main Process):负责实际的文件下载和进度跟踪
- 预加载脚本(Preload Script):安全地暴露主进程的功能和事件给渲染进程
- 渲染进程(Renderer Process):负责显示下载进度界面和用户交互
下面是整个系统的工作流程:
主进程(main.js) ─┐
│ IPC通信
预加载脚本(preload.js) ─┐
│ 暴露API
渲染进程(App.vue + DownloadModal.vue)
实现步骤
1. 主进程中实现下载和进度跟踪
首先,在main.js
中实现下载处理器,并添加进度跟踪逻辑:
// client/electron/main.js
const { app, BrowserWindow, ipcMain } = require('electron');
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
// 处理下载请求
ipcMain.handle('download-update', async (event, options) => {
try {
const { url, filename, version } = options;
if (!url || !filename) {
return { success: false, error: '下载地址或文件名无效' };
}
// 准备下载路径,要下载到哪个目录下,userHomeDir系统的默认主目录
const userHomeDir = os.homedir();
const downloadDir = path.join(userHomeDir, '要下载到的目录名');
// 确保目录存在
if (!fs.existsSync(downloadDir)) {
fs.mkdirSync(downloadDir, { recursive: true });
}
// 确定下载文件路径
let filePath;
if (process.platform === "win32") {
filePath = path.join(downloadDir, '文件名.exe');
} else {
filePath = path.join(downloadDir, '文件名');
}
// 开始下载文件
const result = await downloadFileWithProgress(event, url, filePath);
return result;
} catch (error) {
console.error('下载失败:', error);
return { success: false, error: error.message };
}
});
// 实现带进度的下载函数
async function downloadFileWithProgress(event, url, filePath) {
return new Promise((resolve, reject) => {
// 根据URL选择协议
const requester = url.startsWith('https') ? https : http;
console.log('文件将下载到:', filePath);
const request = requester.get(url, (response) => {
// 处理重定向
if (response.statusCode === 301 || response.statusCode === 302) {
const redirectUrl = response.headers.location;
console.log('下载重定向到:', redirectUrl);
return resolve(downloadFileWithProgress(event, redirectUrl, filePath));
}
// 获取文件大小
const totalSize = parseInt(response.headers['content-length'], 10);
let downloadedSize = 0;
let lastProgressTime = Date.now();
let lastDownloadedSize = 0;
// 创建文件写入流
const file = fs.createWriteStream(filePath);
// 监听数据接收事件,更新进度
response.on('data', (chunk) => {
downloadedSize += chunk.length;
const percent = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0;
// 计算下载速度 (每秒更新一次)
const now = Date.now();
const elapsedTime = now - lastProgressTime;
if (elapsedTime >= 1000 || percent === 100) {
const bytesPerSecond = Math.round((downloadedSize - lastDownloadedSize) / (elapsedTime / 1000));
// 将下载大小格式化为可读的字符串
const formattedDownloaded = formatBytes(downloadedSize);
const formattedTotal = formatBytes(totalSize);
const formattedSpeed = formatBytes(bytesPerSecond) + '/s';
// 更新最后进度时间和大小
lastProgressTime = now;
lastDownloadedSize = downloadedSize;
// 发送进度给渲染进程
event.sender.send('download-progress', {
percent: percent,
downloaded: downloadedSize,
total: totalSize,
formattedDownloaded: formattedDownloaded,
formattedTotal: formattedTotal,
speed: formattedSpeed
});
}
});
// 将响应导入文件
response.pipe(file);
// 监听文件写入完成事件
file.on('finish', () => {
file.close();
console.log('文件下载完成:', filePath);
resolve({ success: true, filePath });
});
// 监听错误
file.on('error', (err) => {
fs.unlink(filePath, () => {});
console.error('文件写入错误:', err);
reject(err);
});
});
// 处理请求错误
request.on('error', (err) => {
console.error('下载失败:', err);
fs.unlink(filePath, () => {}); // 删除可能部分下载的文件
reject(err);
});
});
}
// 辅助函数:格式化字节大小
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
主进程实现了以下关键功能:
- 通过
downloadFileWithProgress
函数下载文件,同时跟踪进度 - 计算下载百分比、下载速度和格式化的文件大小
- 使用
event.sender.send()
方法向渲染进程发送实时进度更新 - 处理重定向、错误和完成事件
- 包含辅助函数
formatBytes
将字节大小转换为可读格式
2. 预加载脚本中暴露下载功能和事件
在preload.js
中,我们需要安全地暴露下载功能和进度事件给渲染进程:
// client/electron/preload.js
const { contextBridge, ipcRenderer } = require('electron');
// 安全地暴露主进程功能给渲染进程
contextBridge.exposeInMainWorld('electron', {
// 下载文件API
downloadUpdate: (options) => {
return ipcRenderer.invoke('download-update', options);
},
// 下载进度事件监听器
onDownloadProgress: (callback) => {
// 移除可能存在的旧监听器
ipcRenderer.removeAllListeners('download-progress');
// 添加新的监听器
ipcRenderer.on('download-progress', (event, progressData) => {
callback(progressData);
});
// 返回清理函数
return () => {
ipcRenderer.removeAllListeners('download-progress');
};
}
});
预加载脚本完成了两个关键任务:
- 暴露
downloadUpdate
方法,使渲染进程能够调用主进程的下载功能 - 暴露
onDownloadProgress
事件监听器,使渲染进程能够接收下载进度更新 - 提供清理函数,确保不会留下多余的事件监听器
3. 渲染进程中接收和处理下载进度
在App.vue中,我们需要设置状态变量和事件监听器来处理下载进度:
// client/src/App.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import DownloadModal from '@/components/DownloadModal.vue';
// 下载状态
const downloadState = ref({
visible: false,
fileName: '',
url: '',
version: '',
percentage: 0,
downloadedSize: '0 KB',
totalSize: '0 MB',
speed: '0 KB/s'
});
// 下载对话框引用
const downloadModalRef = ref(null);
// 清理函数引用
let cleanupProgressListener = null;
// 组件挂载时设置进度监听器
onMounted(() => {
if (window.electron && window.electron.onDownloadProgress) {
cleanupProgressListener = window.electron.onDownloadProgress((progressData) => {
// 更新下载状态
downloadState.value.percentage = progressData.percent;
downloadState.value.downloadedSize = progressData.formattedDownloaded;
downloadState.value.totalSize = progressData.formattedTotal;
downloadState.value.speed = progressData.speed;
console.log(`下载进度: ${progressData.percent}%, 速度: ${progressData.speed}`);
});
}
});
// 组件卸载时清理监听器
onUnmounted(() => {
if (cleanupProgressListener) {
cleanupProgressListener();
}
});
// 确认更新开始下载
const confirmUpdate = async () => {
// 隐藏确认对话框
updateConfirm.value.visible = false;
// 重置下载状态
downloadState.value = {
visible: true,
fileName: process.platform === 'win32' ? 'secmate.exe' : 'secmate',
url: updateConfirm.value.url,
version: updateConfirm.value.version,
percentage: 0,
downloadedSize: '0 KB',
totalSize: '计算中...',
speed: '0 KB/s'
};
// 显示下载对话框
if (downloadModalRef.value) {
downloadModalRef.value.startDownload();
}
try {
if (!window.electron || !window.electron.downloadUpdate) {
throw new Error('下载功能不可用');
}
// 开始下载
const result = await window.electron.downloadUpdate({
url: updateConfirm.value.url,
filename: downloadState.value.fileName,
version: updateConfirm.value.version
});
console.log('下载结果:', result);
if (result.success) {
// 下载成功,完成下载动画
if (downloadModalRef.value) {
downloadModalRef.value.completeDownload();
}
} else {
// 下载失败
showMessage('下载失败: ' + (result.error || '未知错误'), 'error');
downloadState.value.visible = false;
}
} catch (error) {
console.error('下载过程出错:', error);
showMessage('下载过程出错: ' + error.message, 'error');
downloadState.value.visible = false;
}
};
// 处理下载完成
const handleDownloadComplete = async () => {
console.log('下载已完成');
// 添加版本信息到成功消息
const versionText = downloadState.value.version ? ` (版本 ${downloadState.value.version})` : '';
showMessage(`更新文件已下载完成${versionText}!`, 'success');
// 关闭下载状态
downloadState.value.visible = false;
// 启动后端服务或其他后续操作...
};
</script>
<template>
<!-- 下载进度弹窗 -->
<DownloadModal
:visible="downloadState.visible"
:fileName="downloadState.fileName"
:progress="downloadState.percentage"
:total="downloadState.totalSize"
:downloadState="downloadState"
ref="downloadModalRef"
@complete="handleDownloadComplete"
/>
<!-- 其他组件... -->
</template>
App.vue中的关键实现包括:
- 创建
downloadState
响应式对象,存储下载状态信息 - 使用
onMounted
和onUnmounted
生命周期钩子管理进度事件监听器 - 在
confirmUpdate
函数中开始下载流程并重置下载状态 - 将下载状态传递给
DownloadModal
组件显示进度信息 - 通过
handleDownloadComplete
处理下载完成后的逻辑
4. 创建下载进度显示组件
最后,创建DownloadModal.vue
组件来显示下载进度:
<!-- client/src/components/DownloadModal.vue -->
<template>
<div class="download-modal" v-show="visible">
<div class="download-dialog">
<div class="download-header">
<img width="24px" height="24px" src="@/assets/zhuce.png" alt="下载" class="download-emoji">
<h3>正在下载…</h3>
</div>
<div class="progress-container">
<!-- 进度条 -->
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${progress}%` }"></div>
</div>
<!-- 进度信息 -->
<div class="progress-stats">
<div class="progress-text">{{ progress }}%</div>
<div class="progress-size">{{ downloadState.downloadedSize }}/{{ downloadState.totalSize }}</div>
</div>
<!-- 下载速度 -->
<div class="download-speed" v-if="downloadState.speed">
{{ downloadState.speed }}
</div>
</div>
<div class="download-message">{{ message }}</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
visible: Boolean,
progress: {
type: Number,
default: 0
},
fileName: String,
downloadState: {
type: Object,
default: () => ({
downloadedSize: '0 KB',
totalSize: '0 MB',
speed: '0 KB/s'
})
}
});
const emit = defineEmits(['complete']);
// 状态变量
const message = ref('');
let progressInterval = null;
// 监听进度变化,更新提示消息
watch(() => props.progress, (newProgress) => {
if (newProgress < 30) {
message.value = '正在下载更新包...';
} else if (newProgress < 60) {
message.value = '正在下载更新包...';
} else if (newProgress < 90) {
message.value = '下载中,请稍候...';
} else {
message.value = '即将完成下载...';
}
});
// 开始下载动画
function startDownload() {
// 清除可能存在的旧计时器
if (progressInterval) clearInterval(progressInterval);
// 设置初始消息
message.value = '正在连接下载服务器...';
}
// 完成下载
function completeDownload() {
clearInterval(progressInterval);
message.value = '下载完成!';
// 延迟关闭
setTimeout(() => {
emit('complete');
}, 1000);
}
// 暴露方法给父组件
defineExpose({
startDownload,
completeDownload
});
</script>
<style scoped>
.download-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000000;
}
.download-dialog {
width: 380px;
background-color: white;
border-radius: 14px;
padding: 30px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.download-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.progress-container {
margin-bottom: 16px;
}
.progress-bar {
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #7af2ff, #477cff);
width: 0;
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-stats {
display: flex;
justify-content: space-between;
margin-top: 8px;
}
.progress-text, .progress-size {
font-size: 14px;
color: #666;
}
.download-speed {
text-align: right;
margin-top: 4px;
font-size: 13px;
color: #888;
}
.download-message {
text-align: center;
font-size: 14px;
color: #555;
min-height: 20px;
margin-top: 10px;
}
</style>
DownloadModal组件的核心功能包括:
- 接收并显示下载进度、文件大小和下载速度
- 提供动态进度条,显示当前下载百分比
- 根据进度显示相应的提示消息
- 提供
startDownload
和completeDownload
方法供父组件调用
关键技术点解析
1. 实时进度计算和格式化
在主进程中,我们不仅计算下载百分比,还计算下载速度并格式化文件大小:
// 计算下载速度
const now = Date.now();
const elapsedTime = now - lastProgressTime;
if (elapsedTime >= 1000 || percent === 100) {
const bytesPerSecond = Math.round((downloadedSize - lastDownloadedSize) / (elapsedTime / 1000));
// 格式化大小为可读字符串
const formattedDownloaded = formatBytes(downloadedSize);
const formattedTotal = formatBytes(totalSize);
const formattedSpeed = formatBytes(bytesPerSecond) + '/s';
// 更新最后进度时间和大小
lastProgressTime = now;
lastDownloadedSize = downloadedSize;
// 发送进度数据...
}
2. 安全的IPC通信
通过预加载脚本,我们安全地桥接了主进程和渲染进程的通信:
// 在预加载脚本中暴露事件监听器
onDownloadProgress: (callback) => {
ipcRenderer.removeAllListeners('download-progress');
ipcRenderer.on('download-progress', (event, progressData) => {
callback(progressData);
});
return () => {
ipcRenderer.removeAllListeners('download-progress');
};
}
3. 响应式UI更新
在Vue组件中,我们使用响应式对象和计算属性来确保UI与下载状态同步:
// 通过 props 将下载状态传递给组件
<DownloadModal
:visible="downloadState.visible"
:progress="downloadState.percentage"
:downloadState="downloadState"
ref="downloadModalRef"
@complete="handleDownloadComplete"
/>
// 在组件内部监听进度变化
watch(() => props.progress, (newProgress) => {
if (newProgress < 30) {
message.value = '正在下载更新包...';
} else if (newProgress < 60) {
message.value = '正在下载更新包...';
} else if (newProgress < 90) {
message.value = '下载中,请稍候...';
} else {
message.value = '即将完成下载...';
}
});
最佳实践与优化建议
-
节流进度更新:对于较大的文件,每个数据块都发送进度更新会导致性能问题。我们使用时间间隔(每秒更新一次)来节流进度更新。
-
格式化显示大小:使用
formatBytes
函数将字节数转换为人类可读的格式,如KB、MB和GB。 -
提供下载速度:显示当前下载速度,帮助用户估计剩余时间。
-
正确清理资源:在组件卸载时清理事件监听器,避免内存泄漏。
-
显示不同阶段的消息:根据下载进度显示不同的提示消息,增强用户体验。
-
处理下载错误:捕获并显示下载过程中的错误,提供有意义的错误信息。
-
保留下载历史:可以考虑添加下载历史记录功能,允许用户查看和管理历史下载。
应用场景
这种实时下载进度显示功能可以应用于多种场景:
- 应用自动更新:显示新版本下载进度
- 大文件下载:下载大型资源文件,如视频、音乐或文档
- 插件安装:下载和安装第三方插件或扩展
- 批量下载:同时下载多个文件并显示总体进度
- 数据导入/导出:在数据迁移过程中显示进度
总结
在Electron应用中实现实时下载进度显示是提升用户体验的重要一环。通过主进程跟踪下载进度、预加载脚本安全地暴露IPC通信,以及渲染进程中的响应式UI更新,我们可以创建一个流畅、信息丰富的下载体验。
这种架构不仅保证了安全性,还提供了良好的性能和用户体验。通过显示下载百分比、文件大小和下载速度,用户可以清楚地了解下载状态,减少等待过程中的焦虑感。