在Electron中实现实时下载进度显示的完整指南

在开发Electron应用时,提供良好的用户体验至关重要,尤其是在下载大文件时。用户需要知道下载进度、预计完成时间以及当前下载速度。本文将详细介绍如何在Electron应用中实现实时下载进度显示功能,从主进程到渲染进程的完整流程。
技术栈是electron+vue3作为示例,其它的技术栈同样可以使用

系统架构概述

实现下载进度显示功能需要以下三个主要组件协同工作:

  1. 主进程(Main Process):负责实际的文件下载和进度跟踪
  2. 预加载脚本(Preload Script):安全地暴露主进程的功能和事件给渲染进程
  3. 渲染进程(Renderer Process):负责显示下载进度界面和用户交互

下面是整个系统的工作流程:

复制代码
主进程(main.js) ─┐
                 │ IPC通信
预加载脚本(preload.js) ─┐
                       │ 暴露API
渲染进程(App.vue + DownloadModal.vue)

实现步骤

1. 主进程中实现下载和进度跟踪

首先,在main.js中实现下载处理器,并添加进度跟踪逻辑:

javascript 复制代码
// 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];
}

主进程实现了以下关键功能:

  1. 通过downloadFileWithProgress函数下载文件,同时跟踪进度
  2. 计算下载百分比、下载速度和格式化的文件大小
  3. 使用event.sender.send()方法向渲染进程发送实时进度更新
  4. 处理重定向、错误和完成事件
  5. 包含辅助函数formatBytes将字节大小转换为可读格式

2. 预加载脚本中暴露下载功能和事件

preload.js中,我们需要安全地暴露下载功能和进度事件给渲染进程:

javascript 复制代码
// 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');
    };
  }
});

预加载脚本完成了两个关键任务:

  1. 暴露downloadUpdate方法,使渲染进程能够调用主进程的下载功能
  2. 暴露onDownloadProgress事件监听器,使渲染进程能够接收下载进度更新
  3. 提供清理函数,确保不会留下多余的事件监听器

3. 渲染进程中接收和处理下载进度

在App.vue中,我们需要设置状态变量和事件监听器来处理下载进度:

javascript 复制代码
// 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中的关键实现包括:

  1. 创建downloadState响应式对象,存储下载状态信息
  2. 使用onMountedonUnmounted生命周期钩子管理进度事件监听器
  3. confirmUpdate函数中开始下载流程并重置下载状态
  4. 将下载状态传递给DownloadModal组件显示进度信息
  5. 通过handleDownloadComplete处理下载完成后的逻辑

4. 创建下载进度显示组件

最后,创建DownloadModal.vue组件来显示下载进度:

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组件的核心功能包括:

  1. 接收并显示下载进度、文件大小和下载速度
  2. 提供动态进度条,显示当前下载百分比
  3. 根据进度显示相应的提示消息
  4. 提供startDownloadcompleteDownload方法供父组件调用

关键技术点解析

1. 实时进度计算和格式化

在主进程中,我们不仅计算下载百分比,还计算下载速度并格式化文件大小:

javascript 复制代码
// 计算下载速度
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通信

通过预加载脚本,我们安全地桥接了主进程和渲染进程的通信:

javascript 复制代码
// 在预加载脚本中暴露事件监听器
onDownloadProgress: (callback) => {
  ipcRenderer.removeAllListeners('download-progress');
  ipcRenderer.on('download-progress', (event, progressData) => {
    callback(progressData);
  });
  
  return () => {
    ipcRenderer.removeAllListeners('download-progress');
  };
}

3. 响应式UI更新

在Vue组件中,我们使用响应式对象和计算属性来确保UI与下载状态同步:

javascript 复制代码
// 通过 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 = '即将完成下载...';
  }
});

最佳实践与优化建议

  1. 节流进度更新:对于较大的文件,每个数据块都发送进度更新会导致性能问题。我们使用时间间隔(每秒更新一次)来节流进度更新。

  2. 格式化显示大小 :使用formatBytes函数将字节数转换为人类可读的格式,如KB、MB和GB。

  3. 提供下载速度:显示当前下载速度,帮助用户估计剩余时间。

  4. 正确清理资源:在组件卸载时清理事件监听器,避免内存泄漏。

  5. 显示不同阶段的消息:根据下载进度显示不同的提示消息,增强用户体验。

  6. 处理下载错误:捕获并显示下载过程中的错误,提供有意义的错误信息。

  7. 保留下载历史:可以考虑添加下载历史记录功能,允许用户查看和管理历史下载。

应用场景

这种实时下载进度显示功能可以应用于多种场景:

  1. 应用自动更新:显示新版本下载进度
  2. 大文件下载:下载大型资源文件,如视频、音乐或文档
  3. 插件安装:下载和安装第三方插件或扩展
  4. 批量下载:同时下载多个文件并显示总体进度
  5. 数据导入/导出:在数据迁移过程中显示进度

总结

在Electron应用中实现实时下载进度显示是提升用户体验的重要一环。通过主进程跟踪下载进度、预加载脚本安全地暴露IPC通信,以及渲染进程中的响应式UI更新,我们可以创建一个流畅、信息丰富的下载体验。

这种架构不仅保证了安全性,还提供了良好的性能和用户体验。通过显示下载百分比、文件大小和下载速度,用户可以清楚地了解下载状态,减少等待过程中的焦虑感。

相关推荐
极小狐6 分钟前
极狐GitLab 容器镜像仓库功能介绍
java·前端·数据库·npm·gitlab
程序猿阿伟18 分钟前
《Flutter社交应用暗黑奥秘:模式适配与色彩的艺术》
前端·flutter
rafael(一只小鱼)22 分钟前
黑马点评实战笔记
前端·firefox
weifont22 分钟前
React中的useSyncExternalStore使用
前端·javascript·react.js
初遇你时动了情27 分钟前
js fetch流式请求 AI动态生成文本,实现逐字生成渲染效果
前端·javascript·react.js
影子信息41 分钟前
css 点击后改变样式
前端·css
几何心凉1 小时前
如何使用 React Hooks 替代类组件的生命周期方法?
前端·javascript·react.js
小堃学编程1 小时前
前端学习(1)—— 使用HTML编写一个简单的个人简历展示页面
前端·javascript·html
hnlucky2 小时前
通俗易懂版知识点:Keepalived + LVS + Web + NFS 高可用集群到底是干什么的?
linux·前端·学习·github·web·可用性测试·lvs
懒羊羊我小弟2 小时前
使用 ECharts GL 实现交互式 3D 饼图:技术解析与实践
前端·vue.js·3d·前端框架·echarts