在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更新,我们可以创建一个流畅、信息丰富的下载体验。

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

相关推荐
前端小张同学7 分钟前
前端Vue后端Nodejs 实现 pdf下载和预览,如何实现?
前端·javascript·node.js
独孤求败Ace9 分钟前
第59天:Web攻防-XSS跨站&反射型&存储型&DOM型&接受输出&JS执行&标签操作&SRC复盘
前端·xss
天空之枫11 分钟前
node-sass替换成Dart-sass(全是坑)
前端·css·sass
SecPulse12 分钟前
xss注入实验(xss-lab)
服务器·前端·人工智能·网络安全·智能路由器·github·xss
路遥努力吧15 分钟前
el-input 不可编辑,但是点击的时候出现弹窗/或其他操作面板,并且带可清除按钮
前端·vue.js·elementui
绝顶少年19 分钟前
确保刷新页面后用户登录状态不会失效,永久化存储用户登录信息
前端
VT.馒头1 小时前
【力扣】2666. 只允许一次函数调用——认识高阶函数
javascript·算法·leetcode·职场和发展
初学者7.1 小时前
Webpack总结
前端·webpack·node.js
fridayCodeFly1 小时前
使用 request 的 axios 状态码分析
前端·servlet
祈澈菇凉1 小时前
解释什么是受控组件和非受控组件
前端·javascript·react.js