掌握File与Blob黑科技:从浏览器操控本地文件夹到现代应用实战指南(上篇)

前言:被低估的浏览器文件能力

在前端开发的技术栈中,File与Blob API如同深藏不露的武林高手,常常被开发者视为"简单的文件处理工具"。然而,当我们真正深入探索这些API的底层能力时,会发现它们是构建现代Web应用的基石------从实现媲美原生应用的文件管理体验,到突破浏览器沙盒限制的创新方案,再到支撑AI模型在客户端高效运行的内存管理技术。

本文将带你重新认识File与Blob API的强大潜力,通过20+实战案例,揭示如何利用这些"黑科技"解决前端开发中的棘手问题,包括:浏览器端文件系统的完全控制、GB级大文件的断点续传方案、WebAssembly内存优化技巧、Electron应用的沙盒突破策略等。无论你是构建企业级Web应用、开发跨平台桌面软件,还是探索前沿的Web AI技术,这些知识都将成为你的秘密武器。

一、File与Blob核心原理:从二进制数据到文件对象

1.1 Blob:二进制数据的容器化表示

Blob(Binary Large Object)本质上是对二进制数据的容器化封装,它允许我们以结构化的方式处理二进制数据。与普通ArrayBuffer不同,Blob提供了更高级的抽象,支持MIME类型标识和分片操作,这使其成为在浏览器中处理文件数据的理想选择。

javascript 复制代码
// 创建一个简单的Blob对象
const textBlob = new Blob(['Hello, Blob World!'], { type: 'text/plain' });

// 创建一个包含JSON数据的Blob
const jsonBlob = new Blob(
  [JSON.stringify({ name: 'Blob', type: 'binary' }, null, 2)],
  { type: 'application/json' }
);

// 从ArrayBuffer创建Blob
const arrayBuffer = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]).buffer;
const binaryBlob = new Blob([arrayBuffer], { type: 'application/octet-stream' });

Blob的核心优势在于其不可变性和可组合性。一旦创建,Blob的数据就不能被直接修改,但我们可以通过Blob.slice()方法创建新的Blob,实现数据的安全分割与重组------这一特性是实现大文件分片上传的关键基础。

1.2 File:操作系统文件的Web表示

File接口继承自Blob,并在其基础上添加了文件系统的元数据(如文件名、修改日期、文件大小等)。这一继承关系使得File对象既能享受Blob的所有二进制处理能力,又能与用户的本地文件系统进行交互。

javascript 复制代码
// 从<input type="file">获取File对象
document.getElementById('file-input').addEventListener('change', (e) => {
  const file = e.target.files[0];
  console.log('文件名:', file.name);
  console.log('MIME类型:', file.type);
  console.log('文件大小:', file.size);
  console.log('最后修改时间:', file.lastModifiedDate);
  
  // File对象同时也是Blob,可以直接使用Blob的方法
  console.log('是否为Blob:', file instanceof Blob); // true
});

// 从Blob创建File对象(客户端生成文件)
const generatedFile = new File(
  [textBlob], 
  'generated-file.txt', 
  { type: 'text/plain', lastModified: Date.now() }
);

1.3 FileReader与Streams API:数据读取的两种范式

在处理File和Blob数据时,我们有两种截然不同的读取范式:基于事件的FileReader和基于流的Streams API。理解这两种范式的适用场景,对于优化文件处理性能至关重要。

javascript 复制代码
// 传统FileReader方式(适用于小文件)
const reader = new FileReader();
reader.onload = (e) => {
  console.log('文件内容:', e.target.result);
};
reader.onerror = (e) => {
  console.error('读取错误:', e.target.error);
};
reader.readAsText(file);

// 现代Streams API方式(适用于大文件和流式处理)
async function processLargeFile(file) {
  const stream = file.stream();
  const reader = stream.getReader();
  let totalBytesRead = 0;
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    totalBytesRead += value.length;
    console.log(`已处理: ${(totalBytesRead / file.size * 100).toFixed(2)}%`);
    
    // 处理当前数据块(例如:加密、压缩、分析等)
    const processedChunk = await processChunk(value);
    
    // 将处理后的数据写入WritableStream
    await writeToDestination(processedChunk);
  }
  
  console.log('文件处理完成');
}

Streams API的优势在于其内存效率------它不需要将整个文件加载到内存中,而是可以分块处理数据,这使得处理GB级别的大文件成为可能。在后面的章节中,我们将深入探讨如何利用Streams API构建高性能的文件处理管道。

二、浏览器文件系统完全控制:从沙盒限制到自由操作

2.1 File System Access API:突破浏览器沙盒的革命性API

长久以来,浏览器的沙盒安全模型严重限制了Web应用对本地文件系统的访问能力。而File System Access API(也称为Native File System API)的出现,彻底改变了这一局面,它允许Web应用获得用户授权的本地文件系统访问权限,实现真正的文件系统级操作。

javascript 复制代码
// 请求访问用户选择的目录
async function accessUserDirectory() {
  try {
    const directoryHandle = await window.showDirectoryPicker({
      mode: 'readwrite', // 请求读写权限
      startIn: 'documents', // 建议从文档目录开始
      id: 'my-app-storage', // 存储权限的唯一标识
      // 可选:指定可接受的文件类型
      types: [{
        description: '文本文件',
        accept: { 'text/plain': ['.txt', '.md'] }
      }]
    });
    
    // 保存目录句柄以便后续使用(需要用户授权)
    if (await directoryHandle.requestPermission({ mode: 'readwrite' }) === 'granted') {
      // 使用IndexedDB保存目录句柄
      await saveDirectoryHandle(directoryHandle);
      console.log('获得了目录的持久访问权限');
    }
    
    return directoryHandle;
  } catch (err) {
    console.error('访问目录失败:', err);
    if (err.name === 'AbortError') {
      console.log('用户取消了操作');
    }
    return null;
  }
}

// 递归遍历目录结构
async function traverseDirectory(directoryHandle, path = '') {
  const entries = [];
  
  for await (const [name, entry] of directoryHandle.entries()) {
    const fullPath = path ? `${path}/${name}` : name;
    
    if (entry.kind === 'file') {
      // 读取文件内容
      const file = await entry.getFile();
      entries.push({
        path: fullPath,
        type: 'file',
        name,
        size: file.size,
        lastModified: file.lastModified,
        file
      });
    } else if (entry.kind === 'directory') {
      // 递归处理子目录
      const subEntries = await traverseDirectory(entry, fullPath);
      entries.push({
        path: fullPath,
        type: 'directory',
        name,
        entries: subEntries
      });
    }
  }
  
  return entries;
}

// 创建和写入文件
async function createAndWriteFile(directoryHandle, filePath, content) {
  // 解析文件路径
  const pathParts = filePath.split('/');
  const fileName = pathParts.pop();
  let currentDir = directoryHandle;
  
  // 创建必要的子目录
  for (const dirName of pathParts) {
    currentDir = await currentDir.getDirectoryHandle(dirName, { create: true });
  }
  
  // 创建并写入文件
  const fileHandle = await currentDir.getFileHandle(fileName, { create: true });
  const writable = await fileHandle.createWritable();
  
  // 支持写入字符串、ArrayBuffer或Blob
  if (typeof content === 'string') {
    await writable.write(content);
  } else if (content instanceof ArrayBuffer) {
    await writable.write(new Uint8Array(content));
  } else if (content instanceof Blob) {
    await writable.write(content);
  }
  
  await writable.close();
  console.log(`文件已创建: ${filePath}`);
  
  return fileHandle;
}

File System Access API的强大之处在于:

  1. 持久化权限:用户授权后,应用可以在后续会话中继续访问同一目录
  2. 完整文件系统操作:支持创建、删除、重命名文件和目录,以及移动和复制操作
  3. 实时文件监控:可以监听目录变化,实现类似IDE的文件自动刷新功能
  4. 高性能访问:直接操作本地文件系统,避免了传统文件上传/下载的网络开销

2.2 实战案例:构建浏览器端Markdown编辑器

利用File System Access API,我们可以构建一个功能完备的浏览器端Markdown编辑器,实现与桌面应用相媲美的文件管理体验:

javascript 复制代码
class BrowserMarkdownEditor {
  constructor() {
    this.currentDirectory = null;
    this.currentFile = null;
    this.init();
  }
  
  async init() {
    // 尝试恢复上次访问的目录
    this.currentDirectory = await this.restoreDirectoryHandle();
    if (this.currentDirectory) {
      await this.loadDirectoryStructure();
    }
    
    // 绑定UI事件
    this.bindEvents();
  }
  
  async bindEvents() {
    // 打开目录按钮
    document.getElementById('open-dir').addEventListener('click', async () => {
      const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
      this.currentDirectory = dirHandle;
      await this.saveDirectoryHandle(dirHandle);
      await this.loadDirectoryStructure();
    });
    
    // 新建文件按钮
    document.getElementById('new-file').addEventListener('click', async () => {
      if (!this.currentDirectory) {
        alert('请先打开一个目录');
        return;
      }
      
      const fileName = prompt('请输入文件名', 'untitled.md');
      if (!fileName) return;
      
      await this.createAndWriteFile(
        fileName, 
        '# 新文档\n\n在此处开始编写Markdown内容...'
      );
      await this.loadDirectoryStructure();
    });
    
    // 保存文件按钮
    document.getElementById('save-file').addEventListener('click', async () => {
      if (!this.currentFile || !this.currentDirectory) return;
      
      const content = document.getElementById('editor').value;
      await this.writeToFile(this.currentFile, content);
      alert('文件已保存');
    });
  }
  
  // 从目录树中加载文件
  async loadFile(fileHandle) {
    this.currentFile = fileHandle;
    const file = await fileHandle.getFile();
    const content = await file.text();
    
    // 更新UI
    document.getElementById('editor-title').textContent = fileHandle.name;
    document.getElementById('editor').value = content;
  }
  
  // 目录结构持久化
  async saveDirectoryHandle(handle) {
    try {
      // 使用FileSystemDirectoryHandle的持久化功能
      await navigator.storage.persist();
      const serialized = await window.showSaveFilePicker({
        suggestedName: 'directory-permission.json',
        types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }]
      });
      
      // 实际应用中,我们会使用IndexedDB存储权限令牌
      // 这里简化处理
      localStorage.setItem('hasDirectoryAccess', 'true');
    } catch (err) {
      console.error('保存目录权限失败:', err);
    }
  }
  
  // 其他方法实现...
}

// 初始化编辑器
const editor = new BrowserMarkdownEditor();

这个案例展示了如何利用File System Access API构建一个具有以下功能的浏览器应用:

  • 打开和管理本地目录
  • 创建、编辑和保存Markdown文件
  • 持久化访问权限,支持跨会话工作
  • 实时预览Markdown内容

2.3 兼容性处理与降级方案

尽管File System Access API非常强大,但目前并非所有浏览器都支持。我们需要实现优雅的降级方案,确保应用在所有环境中都能正常工作:

javascript 复制代码
// 检测File System Access API支持情况
function checkFileSystemSupport() {
  const support = {
    fileSystemAccess: 'showDirectoryPicker' in window,
    fileSystemFileHandle: 'FileSystemFileHandle' in window,
    streams: 'ReadableStream' in window && 'WritableStream' in window,
    showOpenFilePicker: 'showOpenFilePicker' in window,
    showSaveFilePicker: 'showSaveFilePicker' in window
  };
  
  console.log('浏览器功能支持情况:', support);
  return support;
}

// 文件系统操作的抽象层,封装不同API的实现
class FileSystemAdapter {
  constructor() {
    this.support = checkFileSystemSupport();
    this.initStrategy();
  }
  
  // 根据浏览器支持情况选择不同的实现策略
  initStrategy() {
    if (this.support.fileSystemAccess) {
      this.strategy = new NativeFileSystemStrategy();
    } else if (this.support.streams) {
      this.strategy = new StreamBasedFileSystemStrategy();
    } else {
      this.strategy = new LegacyFileSystemStrategy();
    }
    
    console.log(`使用文件系统策略: ${this.strategy.constructor.name}`);
  }
  
  // 代理方法,将调用转发给具体策略
  async openDirectory() {
    return this.strategy.openDirectory();
  }
  
  async readFile(fileHandle) {
    return this.strategy.readFile(fileHandle);
  }
  
  async writeFile(fileHandle, content) {
    return this.strategy.writeFile(fileHandle, content);
  }
  
  // 其他方法...
}

// 原生文件系统策略(支持File System Access API)
class NativeFileSystemStrategy {
  async openDirectory() {
    return window.showDirectoryPicker({ mode: 'readwrite' });
  }
  
  async readFile(fileHandle) {
    const file = await fileHandle.getFile();
    return file.text();
  }
  
  async writeFile(fileHandle, content) {
    const writable = await fileHandle.createWritable();
    await writable.write(content);
    await writable.close();
    return true;
  }
  
  // 其他方法实现...
}

// 基于Stream的降级策略(不支持File System Access API但支持Streams)
class StreamBasedFileSystemStrategy {
  async openDirectory() {
    // 使用传统的<input type="file" webkitdirectory>
    return new Promise((resolve) => {
      const input = document.createElement('input');
      input.type = 'file';
      input.webkitdirectory = true;
      
      input.onchange = (e) => {
        // 模拟目录句柄对象
        const dirHandle = {
          files: Array.from(e.target.files),
          kind: 'directory',
          name: 'Selected Files'
        };
        resolve(dirHandle);
      };
      
      input.click();
    });
  }
  
  // 其他方法实现...
}

// 传统降级策略(仅支持基本文件操作)
class LegacyFileSystemStrategy {
  async openDirectory() {
    alert('您的浏览器不支持目录访问功能,请使用最新版Chrome或Edge浏览器以获得最佳体验。');
    return null;
  }
  
  // 其他方法实现...
}

通过这种抽象设计,我们的应用可以在不同浏览器环境中提供最佳可能的用户体验,同时为支持File System Access API的浏览器提供高级功能。

三、大文件处理与上传:突破浏览器限制的高级技巧

3.1 分片上传原理与实现:轻松处理GB级文件

大文件上传是Web应用开发中的常见挑战,传统的整体上传方式容易导致超时、内存溢出和用户体验差等问题。分片上传技术通过将文件分割成小块,分批次上传,然后在服务器端重组,有效解决了这些问题。

javascript 复制代码
class LargeFileUploader {
  constructor(options = {}) {
    this.chunkSize = options.chunkSize || 2 * 1024 * 1024; // 默认2MB分片
    this.concurrency = options.concurrency || 3; // 并发上传数量
    this.retryTimes = options.retryTimes || 3; // 重试次数
    this.apiUrl = options.apiUrl || '/api/upload';
    this.file = null;
    this.fileId = null;
    this.uploadedChunks = new Set();
    this.chunks = [];
    this.progress = 0;
    this.abortController = null;
  }
  
  // 初始化上传
  async initUpload(file) {
    this.file = file;
    this.abortController = new AbortController();
    
    // 生成唯一文件ID(基于文件名、大小和最后修改时间)
    this.fileId = await this.generateFileId(file);
    
    // 检查是否已存在上传记录
    const uploadStatus = await this.checkUploadStatus();
    if (uploadStatus) {
      this.uploadedChunks = new Set(uploadStatus.uploadedChunks);
      this.progress = uploadStatus.progress;
      console.log(`发现续传记录,已上传 ${this.progress.toFixed(2)}%`);
    }
    

    // 分割文件为分片
    this.chunks = await this.splitFileIntoChunks(file);

    // 创建上传任务队列
    this.uploadQueue = this.createUploadQueue();

    return {
      fileId: this.fileId,
      totalChunks: this.chunks.length,
      uploadedChunks: this.uploadedChunks.size,
      progress: this.progress
    };
    }

    // 生成唯一文件ID
    async generateFileId(file) {
    // 使用文件名、大小和最后修改时间生成唯一标识
    const fileInfo = `${file.name}-${file.size}-${file.lastModified}`;

    // 使用SHA-1哈希生成文件ID
    const encoder = new TextEncoder();
    const data = encoder.encode(fileInfo);
    const hashBuffer = await crypto.subtle.digest('SHA-1', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const fileId = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

    return fileId;
    }

    // 分割文件为分片
    async splitFileIntoChunks(file) {
    const chunks = [];
    const totalChunks = Math.ceil(file.size / this.chunkSize);

    // 使用File.slice方法分割文件
    for (let i = 0; i < totalChunks; i++) {
    const start = i * this.chunkSize;
    const end = Math.min(start + this.chunkSize, file.size);
    const chunk = file.slice(start, end);

    chunks.push({
      chunkIndex: i,
      start,
      end,
      size: end - start,
      blob: chunk,
      fileId: this.fileId
    });
    }

    return chunks;
    }

    // 创建上传任务队列
    createUploadQueue() {
    // 过滤掉已上传的分片
    const chunksToUpload = this.chunks.filter(chunk => !this.uploadedChunks.has(chunk.chunkIndex));

    // 创建上传任务
    return chunksToUpload.map(chunk => ({
      chunk,
      retryCount: 0,
      upload: async () => this.uploadChunk(chunk)
    }));
    }

    // 上传单个分片
    async uploadChunk(chunk) {
    const formData = new FormData();
    formData.append('fileId', chunk.fileId);
    formData.append('chunkIndex', chunk.chunkIndex);
    formData.append('totalChunks', this.chunks.length);
    formData.append('fileName', this.file.name);
    formData.append('chunk', chunk.blob, `${chunk.fileId}-${chunk.chunkIndex}`);

    try {
    const response = await fetch(`${this.apiUrl}/chunk`, {
      method: 'POST',
      body: formData,
      signal: this.abortController.signal
    });

    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

    const result = await response.json();
    if (result.success) {
      this.uploadedChunks.add(chunk.chunkIndex);
      this.updateProgress();
      return true;
    } else {
      throw new Error(result.message || '上传分片失败');
    }
    } catch (error) {
    if (error.name === 'AbortError') {
      console.log('上传已取消');
      throw error;
    }

    // 重试逻辑
    if (chunk.retryCount < this.retryTimes) {
      chunk.retryCount++;
      const delay = Math.pow(2, chunk.retryCount) * 1000; // 指数退避策略
      console.log(`分片 ${chunk.chunkIndex} 上传失败,将在 ${delay}ms 后重试(${chunk.retryCount}/${this.retryTimes})`);

      await new Promise(resolve => setTimeout(resolve, delay));
      return this.uploadChunk(chunk); // 递归重试
    } else {
      console.error(`分片 ${chunk.chunkIndex} 上传失败,已达到最大重试次数`);
      throw error;
    }
    }
    }

    // 开始上传
    async startUpload() {
    if (!this.file || this.chunks.length === 0) {
    throw new Error('请先初始化上传');
    }

    try {
    // 通知服务器开始上传
    await this.notifyUploadStart();

    // 使用Promise.allSettled和并发控制上传分片
    const results = await this.runConcurrentTasks(
      this.uploadQueue.map(task => task.upload),
      this.concurrency
    );

    // 检查是否所有分片都上传成功
    const failedChunks = results
      .map((result, index) => ({ result, chunkIndex: this.uploadQueue[index].chunk.chunkIndex }))
      .filter(({ result }) => result.status === 'rejected');

    if (failedChunks.length > 0) {
      throw new Error(`上传失败,${failedChunks.length}个分片上传失败`);
    }

    // 通知服务器所有分片已上传完成,可以合并文件
    const finalResult = await this.notifyUploadComplete();
    return finalResult;
    } catch (error) {
    console.error('上传过程出错:', error);
    throw error;
    }
    }

    // 并发任务执行器
    async runConcurrentTasks(tasks, concurrency) {
    const results = [];
    const executing = new Set();

    for (const task of tasks) {
    // 创建一个立即执行的Promise
    const p = Promise.resolve().then(() => task());

    results.push(p);
    executing.add(p);

    // 当Promise完成后从executing集合中移除
    const clean = () => executing.delete(p);
    p.then(clean).catch(clean);

    // 如果达到并发限制,等待一个Promise完成
    if (executing.size >= concurrency) {
      await Promise.race(executing);
    }
    }

    // 等待所有任务完成
    return Promise.allSettled(results);
    }

    // 更新上传进度
    updateProgress() {
    this.progress = (this.uploadedChunks.size / this.chunks.length) * 100;

    // 可以通过回调函数通知UI更新进度
    if (this.onProgress) {
    this.onProgress({
      percent: this.progress,
      uploadedChunks: this.uploadedChunks.size,
      totalChunks: this.chunks.length,
      uploadedSize: this.uploadedChunks.size * this.chunkSize,
      totalSize: this.file.size
    });
    }
    }

    // 通知服务器开始上传
    async notifyUploadStart() {
    await fetch(`${this.apiUrl}/start`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      fileId: this.fileId,
      fileName: this.file.name,
      fileSize: this.file.size,
      totalChunks: this.chunks.length,
      chunkSize: this.chunkSize
    })
    });
    }

    // 通知服务器上传完成,请求合并文件
    async notifyUploadComplete() {
    const response = await fetch(`${this.apiUrl}/complete`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      fileId: this.fileId,
      fileName: this.file.name,
      totalChunks: this.chunks.length
    })
    });

    return response.json();
    }

    // 取消上传
    abortUpload() {
    if (this.abortController) {
    this.abortController.abort();
    console.log('上传已取消');
    }
    }
    }

    // 使用示例
    const uploader = new LargeFileUploader({
    apiUrl: 'https://your-upload-server.com',
    chunkSize: 5 * 1024 * 1024, // 5MB分片
    concurrency: 4, // 4个并发上传
    onProgress: (progress) => {
    console.log(`上传进度: ${progress.percent.toFixed(2)}%`);
    // 更新UI进度条
    document.getElementById('progress-bar').style.width = `${progress.percent}%`;
    document.getElementById('progress-text').textContent = 
      `${(progress.uploadedSize / (1024 * 1024)).toFixed(2)}MB / ${(progress.totalSize / (1024 * 1024)).toFixed(2)}MB`;
    }
    });

    // 绑定文件选择事件
    document.getElementById('file-input').addEventListener('change', async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    try {
    console.log(`准备上传文件: ${file.name} (${(file.size / (1024 * 1024)).toFixed(2)}MB)`);
    await uploader.initUpload(file);
    await uploader.startUpload();
    alert('文件上传成功!');
    } catch (error) {
    console.error('上传失败:', error);
    alert(`上传失败: ${error.message}`);
    }
    });

    // 绑定取消按钮事件
    document.getElementById('cancel-upload').addEventListener('click', () => {
    uploader.abortUpload();
    });

3.2 断点续传与上传状态持久化

断点续传功能允许用户在上传中断后从中断处继续上传,而不必重新开始。实现这一功能需要在客户端和服务器端都进行状态记录:

javascript 复制代码
// 扩展LargeFileUploader类以支持断点续传
class ResumableFileUploader extends LargeFileUploader {
constructor(options) {
super(options);
this.persistenceKey = `upload_${options.userId || 'anonymous'}`;
}

// 检查上传状态(服务器端)
async checkUploadStatus() {
try {
const response = await fetch(`${this.apiUrl}/status/${this.fileId}`);
if (!response.ok) {
  // 如果文件不存在,服务器返回404,这是正常情况
  if (response.status === 404) return null;
  throw new Error(`获取上传状态失败: ${response.status}`);
}

const status = await response.json();
return status;
} catch (error) {
console.error('检查上传状态失败:', error);
return null;
}
}

// 从本地存储加载上传状态
async loadUploadState(fileId = null) {
try {
const storedState = localStorage.getItem(this.persistenceKey);
if (!storedState) return null;

const uploadStates = JSON.parse(storedState);
const targetFileId = fileId || this.fileId;

return uploadStates[targetFileId] || null;
} catch (error) {
console.error('加载上传状态失败:', error);
return null;
}
}

// 保存上传状态到本地存储
async saveUploadState() {
try {
const storedState = localStorage.getItem(this.persistenceKey) || '{}';
const uploadStates = JSON.parse(storedState);

// 记录当前上传状态
uploadStates[this.fileId] = {
  fileId: this.fileId,
  fileName: this.file.name,
  fileSize: this.file.size,
  chunkSize: this.chunkSize,
  totalChunks: this.chunks.length,
  uploadedChunks: Array.from(this.uploadedChunks),
  progress: this.progress,
  lastModified: Date.now()
};

// 只保留最近的5个上传状态,避免存储过大
const recentUploads = Object.entries(uploadStates)
  .sort(([, a], [, b]) => b.lastModified - a.lastModified)
  .slice(0, 5);

localStorage.setItem(
  this.persistenceKey,
  JSON.stringify(Object.fromEntries(recentUploads))
);
} catch (error) {
console.error('保存上传状态失败:', error);
}
}

// 重写updateProgress方法以保存状态
updateProgress() {
super.updateProgress();
this.saveUploadState(); // 每次进度更新时保存状态
}

// 初始化上传时恢复状态
async initUpload(file) {
// 首先尝试从本地存储加载状态
const localState = await this.loadUploadState();
if (localState) {
// 如果本地有状态,使用本地记录的fileId
this.fileId = localState.fileId;
} else {
// 否则生成新的fileId
this.fileId = await this.generateFileId(file);
}

this.file = file;
this.abortController = new AbortController();

// 检查服务器端状态
const serverStatus = await this.checkUploadStatus();
if (serverStatus) {
this.uploadedChunks = new Set(serverStatus.uploadedChunks);
this.progress = serverStatus.progress;
console.log(`从服务器恢复上传状态: ${this.uploadedChunks.size}/${serverStatus.totalChunks}个分片已上传`);
} else if (localState) {
// 如果服务器没有状态但本地有,使用本地状态(可能不完全可靠)
this.uploadedChunks = new Set(localState.uploadedChunks);
this.progress = localState.progress;
console.log(`从本地存储恢复上传状态: ${this.uploadedChunks.size}/${localState.totalChunks}个分片已上传`);
}

// 分割文件为分片
this.chunks = await this.splitFileIntoChunks(file);

// 创建上传任务队列
this.uploadQueue = this.createUploadQueue();

// 保存初始状态
this.saveUploadState();

return {
fileId: this.fileId,
totalChunks: this.chunks.length,
uploadedChunks: this.uploadedChunks.size,
progress: this.progress
};
}

// 获取用户的所有上传历史
async getUploadHistory() {
try {
const storedState = localStorage.getItem(this.persistenceKey);
if (!storedState) return [];

const uploadStates = JSON.parse(storedState);
return Object.values(uploadStates);
} catch (error) {
console.error('获取上传历史失败:', error);
return [];
}
}

// 清除上传状态
async clearUploadState(fileId) {
try {
const storedState = localStorage.getItem(this.persistenceKey);
if (!storedState) return;

const uploadStates = JSON.parse(storedState);
delete uploadStates[fileId || this.fileId];

localStorage.setItem(this.persistenceKey, JSON.stringify(uploadStates));
} catch (error) {
console.error('清除上传状态失败:', error);
}
}
}

3.3 客户端文件校验与完整性保证

为确保文件上传的完整性,我们需要在客户端和服务器端都进行文件校验:

javascript 复制代码
// 文件校验工具类
class FileVerifier {
// 计算文件的MD5哈希值(适用于小文件)
static async calculateMD5(file) {
const fileReader = new FileReader();

return new Promise((resolve, reject) => {
  fileReader.onload = async (e) => {
    try {
      const arrayBuffer = e.target.result;
      const hashBuffer = await crypto.subtle.digest('MD5', arrayBuffer);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
      resolve(hashHex);
    } catch (error) {
      reject(error);
    }
  };

  fileReader.onerror = () => reject(fileReader.error);
  fileReader.readAsArrayBuffer(file);
});
}

// 计算大文件的MD5哈希值(使用分片计算)
static async calculateLargeFileMD5(file, chunkSize = 2 * 1024 * 1024, onProgress) {
const fileId = await this.generateFileId(file);
const chunks = Math.ceil(file.size / chunkSize);
const chunkHashes = [];

// 先检查是否有缓存的分片哈希
const cachedHashes = await this.getCachedChunkHashes(fileId);
if (cachedHashes && cachedHashes.chunks === chunks) {
  console.log('使用缓存的分片哈希');
  chunkHashes.push(...cachedHashes.hashes);
} else {
  // 计算每个分片的哈希
  for (let i = 0; i < chunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    
    const arrayBuffer = await new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error);
      reader.readAsArrayBuffer(chunk);
    });
    
    const hashBuffer = await crypto.subtle.digest('MD5', arrayBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    chunkHashes.push(hashHex);
    
    // 更新进度
    if (onProgress) {
      onProgress({
        percent: (i / chunks) * 100,
        chunk: i,
        totalChunks: chunks
      });
    }
  }
  
  // 缓存分片哈希
  await this.cacheChunkHashes(fileId, chunks, chunkHashes);
}

// 计算整体文件哈希(基于分片哈希的组合)
const combinedHashInput = chunkHashes.join('');
const combinedHashBuffer = await crypto.subtle.digest(
  'MD5', 
  new TextEncoder().encode(combinedHashInput)
);
const combinedHashArray = Array.from(new Uint8Array(combinedHashBuffer));
const fileHash = combinedHashArray.map(b => b.toString(16).padStart(2, '0')).join('');

return {
fileHash,
chunkHashes,
verifyChunk: (chunkIndex, chunkHash) => {
  return chunkHashes[chunkIndex] === chunkHash;
}
};
}

// 生成文件ID(用于缓存)
static async generateFileId(file) {
  const fileInfo = `${file.name}-${file.size}-${file.lastModified}`;
  const encoder = new TextEncoder();
  const data = encoder.encode(fileInfo);
  const hashBuffer = await crypto.subtle.digest('SHA-1', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

// 缓存分片哈希
static async cacheChunkHashes(fileId, chunks, hashes) {
  try {
    const cacheData = {
      fileId,
      chunks,
      hashes,
      timestamp: Date.now(),
      ttl: Date.now() + (7 * 24 * 60 * 60 * 1000) // 缓存7天
    };
    
    // 使用localStorage缓存
    const cacheKey = `chunk_hashes_${fileId}`;
    localStorage.setItem(cacheKey, JSON.stringify(cacheData));
    
    // 清理过期缓存
    this.cleanupExpiredCache();
  } catch (error) {
    console.error('缓存分片哈希失败:', error);
  }
}

// 获取缓存的分片哈希
static async getCachedChunkHashes(fileId) {
  try {
    const cacheKey = `chunk_hashes_${fileId}`;
    const cachedData = localStorage.getItem(cacheKey);
    
    if (!cachedData) return null;
    
    const parsedData = JSON.parse(cachedData);
    if (Date.now() > parsedData.ttl) {
      // 缓存过期,删除并返回null
      localStorage.removeItem(cacheKey);
      return null;
    }
    
    return parsedData;
  } catch (error) {
    console.error('获取缓存分片哈希失败:', error);
    return null;
  }
}

// 清理过期缓存
static cleanupExpiredCache() {
  try {
    const keys = Object.keys(localStorage);
    const now = Date.now();
    
    keys.forEach(key => {
      if (key.startsWith('chunk_hashes_')) {
        try {
          const data = JSON.parse(localStorage.getItem(key));
          if (now > data.ttl) {
            localStorage.removeItem(key);
            console.log(`清理过期缓存: ${key}`);
          }
        } catch (error) {
          // 无效缓存,直接删除
          localStorage.removeItem(key);
        }
      }
    });
  } catch (error) {
    console.error('清理缓存失败:', error);
  }
}
}

// 集成文件校验功能到上传器
class VerifiedFileUploader extends ResumableFileUploader {
constructor(options) {
super(options);
this.fileVerifier = FileVerifier;
this.fileHashInfo = null;
}

// 重写初始化方法,添加文件校验
async initUpload(file) {
// 首先计算文件哈希
console.log(`正在计算文件哈希: ${file.name}`);
this.fileHashInfo = await this.fileVerifier.calculateLargeFileMD5(
  file,
  this.chunkSize,
  (progress) => {
    console.log(`哈希计算进度: ${progress.percent.toFixed(2)}% (分片 ${progress.chunk + 1}/${progress.totalChunks})`);
  }
);

console.log(`文件哈希计算完成: ${this.fileHashInfo.fileHash}`);
return super.initUpload(file);
}

// 重写分片上传方法,添加分片校验
async uploadChunk(chunk) {
// 添加上传前的分片校验
const chunkHash = this.fileHashInfo.chunkHashes[chunk.chunkIndex];
if (!chunkHash) {
throw new Error(`分片 ${chunk.chunkIndex} 的哈希信息不存在`);
}

// 在FormData中添加分片哈希
const formData = new FormData();
formData.append('fileId', chunk.fileId);
formData.append('chunkIndex', chunk.chunkIndex);
formData.append('totalChunks', this.chunks.length);
formData.append('fileName', this.file.name);
formData.append('chunkHash', chunkHash); // 添加分片哈希
formData.append('chunk', chunk.blob, `${chunk.fileId}-${chunk.chunkIndex}`);

try {
const response = await fetch(`${this.apiUrl}/chunk`, {
  method: 'POST',
  body: formData,
  signal: this.abortController.signal
});

if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

const result = await response.json();

// 验证服务器返回的分片哈希是否匹配
if (result.chunkHash && !this.fileHashInfo.verifyChunk(chunk.chunkIndex, result.chunkHash)) {
  throw new Error(`分片 ${chunk.chunkIndex} 校验失败:服务器返回的哈希不匹配`);
}

if (result.success) {
  this.uploadedChunks.add(chunk.chunkIndex);
  this.updateProgress();
  return true;
} else {
  throw new Error(result.message || '上传分片失败');
}
} catch (error) {
// 错误处理逻辑保持不变
if (error.name === 'AbortError') {
  console.log('上传已取消');
  throw error;
}

if (chunk.retryCount < this.retryTimes) {
  chunk.retryCount++;
  const delay = Math.pow(2, chunk.retryCount) * 1000;
  console.log(`分片 ${chunk.chunkIndex} 上传失败,将在 ${delay}ms 后重试(${chunk.retryCount}/${this.retryTimes})`);
  
  await new Promise(resolve => setTimeout(resolve, delay));
  return this.uploadChunk(chunk);
} else {
  console.error(`分片 ${chunk.chunkIndex} 上传失败,已达到最大重试次数`);
  throw error;
}
}
}

// 重写完成上传方法,添加最终文件校验
async notifyUploadComplete() {
const response = await fetch(`${this.apiUrl}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  fileId: this.fileId,
  fileName: this.file.name,
  totalChunks: this.chunks.length,
  fileHash: this.fileHashInfo.fileHash // 发送完整文件哈希
})
});

const result = await response.json();

// 验证服务器计算的文件哈希是否与客户端匹配
if (result.fileHash && result.fileHash !== this.fileHashInfo.fileHash) {
throw new Error(`文件校验失败:服务器计算的哈希(${result.fileHash})与客户端不匹配(${this.fileHashInfo.fileHash})`);
}

return result;
}
}

3.4 WebAssembly加速:让文件处理性能提升10-100倍

对于需要在客户端进行复杂文件处理(如压缩、加密、格式转换)的场景,JavaScript的性能往往无法满足需求。WebAssembly (Wasm) 提供了接近原生的性能,是解决这一问题的理想方案。

以下是一个使用WebAssembly加速文件压缩的示例:

javascript 复制代码
// WebAssembly文件压缩工具
class WasmFileProcessor {
constructor() {
this.module = null;
this.isLoaded = false;
}

// 加载WebAssembly模块
async loadWasmModule() {
if (this.isLoaded && this.module) return this.module;

console.log('正在加载WebAssembly模块...');

try {
// 使用WebAssembly.instantiateStreaming加载并实例化Wasm模块
const response = await fetch('/path/to/file-processor.wasm');
const result = await WebAssembly.instantiateStreaming(response, {
  env: {
    memoryBase: 0,
    tableBase: 0,
    memory: new WebAssembly.Memory({ initial: 256, maximum: 1024 }), // 初始256页(16MB),最大1024页(64MB)
    table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' }),
    // 日志回调函数
    emscripten_console_log: (ptr, length) => {
      const bytes = new Uint8Array(this.module.memory.buffer, ptr, length);
      const text = new TextDecoder().decode(bytes);
      console.log('[WASM]', text);
    },
    // 进度回调函数
    on_progress: (percent) => {
      this.onProgress && this.onProgress(percent);
    }
  }
});

this.module = result.instance.exports;
this.isLoaded = true;
console.log('WebAssembly模块加载成功');

return this.module;
} catch (error) {
console.error('WebAssembly模块加载失败:', error);
throw error;
}
}

// 压缩文件数据
async compressFileData(file, compressionLevel = 6) {
if (!this.isLoaded) {
await this.loadWasmModule();
}

console.log(`开始压缩文件: ${file.name} (压缩级别: ${compressionLevel})`);

// 将文件转换为ArrayBuffer
const arrayBuffer = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});

const inputArray = new Uint8Array(arrayBuffer);
const inputSize = inputArray.length;

// 计算所需内存大小(WASM模块需要知道输入大小来分配内存)
const maxOutputSize = this.module.calculate_max_output_size(inputSize);

// 分配WASM内存
const inputPtr = this.module.malloc(inputSize);
const outputPtr = this.module.malloc(maxOutputSize);

try {
// 将文件数据复制到WASM内存
new Uint8Array(this.module.memory.buffer, inputPtr, inputSize).set(inputArray);

// 调用WASM压缩函数
const compressedSize = this.module.compress_data(
inputPtr, 
inputSize, 
outputPtr, 
maxOutputSize, 
compressionLevel
);

if (compressedSize <= 0) {
throw new Error(`压缩失败,错误代码: ${compressedSize}`);
}

console.log(`压缩完成: 原始大小 ${inputSize} bytes, 压缩后 ${compressedSize} bytes (压缩率: ${(compressedSize / inputSize * 100).toFixed(2)}%)`);

// 从WASM内存中读取压缩后的数据
const compressedData = new Uint8Array(
this.module.memory.buffer, 
outputPtr, 
compressedSize
).slice(); // 使用slice()创建副本,避免内存释放后数据丢失

// 创建包含压缩数据的Blob
return new Blob([compressedData], { type: 'application/octet-stream' });
} finally {
// 释放WASM内存
this.module.free(inputPtr);
this.module.free(outputPtr);
}
}

// 解压文件数据
async decompressFileData(compressedBlob) {
if (!this.isLoaded) {
await this.loadWasmModule();
}

console.log('开始解压文件数据');

// 将压缩的Blob转换为ArrayBuffer
const arrayBuffer = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(compressedBlob);
});

const inputArray = new Uint8Array(arrayBuffer);
const inputSize = inputArray.length;

// 分配WASM内存
const inputPtr = this.module.malloc(inputSize);

// 首先获取原始大小(假设压缩数据中包含原始大小信息)
new Uint8Array(this.module.memory.buffer, inputPtr, inputSize).set(inputArray);
const originalSize = this.module.get_original_size(inputPtr, inputSize);

if (originalSize <= 0) {
this.module.free(inputPtr);
throw new Error(`获取原始大小失败,错误代码: ${originalSize}`);
}

const outputPtr = this.module.malloc(originalSize);

try {
// 调用WASM解压函数
const decompressedSize = this.module.decompress_data(
inputPtr, 
inputSize, 
outputPtr, 
originalSize
);

if (decompressedSize <= 0 || decompressedSize !== originalSize) {
throw new Error(`解压失败,错误代码: ${decompressedSize}, 预期大小: ${originalSize}`);
}

console.log(`解压完成: 压缩大小 ${inputSize} bytes, 解压后 ${decompressedSize} bytes`);

// 从WASM内存中读取解压后的数据
const decompressedData = new Uint8Array(
this.module.memory.buffer, 
outputPtr, 
decompressedSize
).slice();

// 创建包含解压数据的Blob
return new Blob([decompressedData], { type: 'application/octet-stream' });
} finally {
// 释放WASM内存
this.module.free(inputPtr);
this.module.free(outputPtr);
}
}

// 设置进度回调
onProgress(handler) {
this.onProgress = handler;
return this;
}
}

// 使用WebAssembly压缩功能的上传器
class CompressedFileUploader extends VerifiedFileUploader {
constructor(options) {
super(options);
this.wasmProcessor = new WasmFileProcessor();
this.compressionLevel = options.compressionLevel || 6;
}

// 重写分片上传方法,添加压缩步骤
async uploadChunk(chunk) {
// 首先压缩分片数据
const compressedBlob = await this.wasmProcessor.compressFileData(
new Blob([chunk.blob]), // 将分片转换为Blob
this.compressionLevel
);

// 创建压缩后的分片对象
const compressedChunk = {
...chunk,
blob: compressedBlob,
compressed: true,
originalSize: chunk.size,
compressedSize: compressedBlob.size
};

console.log(`分片 ${chunk.chunkIndex} 压缩完成: ${chunk.size} bytes -> ${compressedBlob.size} bytes (节省 ${(1 - compressedBlob.size / chunk.size) * 100.toFixed(2)}%)`);

// 使用压缩后的分片进行上传
return super.uploadChunk(compressedChunk);
}

// 设置进度回调
onCompressionProgress(handler) {
this.wasmProcessor.onProgress(handler);
return this;
}
}

// 使用示例
const uploader = new CompressedFileUploader({
apiUrl: 'https://your-upload-server.com',
chunkSize: 5 * 1024 * 1024, // 5MB分片
concurrency: 4,
compressionLevel: 5, // 1-9,越高压缩率越好但速度越慢
onProgress: (progress) => {
console.log(`上传进度: ${progress.percent.toFixed(2)}%`);
document.getElementById('upload-progress').style.width = `${progress.percent}%`;
document.getElementById('upload-status').textContent = 
`已上传: ${(progress.uploadedSize / (1024 * 1024)).toFixed(2)}MB / ${(progress.totalSize / (1024 * 1024)).toFixed(2)}MB`;
}
});

// 压缩进度显示
uploader.onCompressionProgress((percent) => {
document.getElementById('compression-progress').style.width = `${percent}%`;
document.getElementById('compression-status').textContent = `压缩进度: ${percent.toFixed(2)}%`;
});

// 绑定文件选择事件
document.getElementById('file-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;

try {
document.getElementById('upload-panel').style.display = 'block';
console.log(`准备上传文件: ${file.name} (${(file.size / (1024 * 1024)).toFixed(2)}MB)`);
await uploader.initUpload(file);
await uploader.startUpload();
alert('文件上传成功!');
} catch (error) {
console.error('上传失败:', error);
alert(`上传失败: ${error.message}`);
} finally {
document.getElementById('upload-panel').style.display = 'none';
}
});

四、WebAssembly与内存优化:释放底层性能潜力

4.1 WebAssembly内存模型与JavaScript交互

WebAssembly提供了接近原生的性能,但要充分发挥其潜力,必须理解其内存模型以及与JavaScript的交互方式:

javascript 复制代码
// WebAssembly内存管理与JavaScript交互示例
class WasmMemoryManager {
constructor() {
this.memory = null;
this.heap = {
  stackTop: 0,
  stackSize: 1024 * 1024, // 1MB栈空间
  allocatedBlocks: new Map() // 跟踪已分配的内存块
};
}

// 初始化内存
initMemory(initialPages = 256, maxPages = 1024) {
// WebAssembly内存以页为单位,每页64KB
this.memory = new WebAssembly.Memory({ 
initial: initialPages, 
maximum: maxPages 
});

// 初始化堆指针(栈空间之后)
this.heap.stackTop = this.heap.stackSize;
console.log(`WASM内存初始化完成: ${initialPages}页 (${initialPages * 64}KB), 最大: ${maxPages}页 (${maxPages * 64}KB)`);

return this.memory;
}

// 分配内存块
alloc(size, alignment = 8) {
if (!this.memory) {
throw new Error('WASM内存尚未初始化');
}

// 内存对齐处理
const alignedSize = Math.ceil(size / alignment) * alignment;

// 检查是否有足够的内存空间
const totalMemory = this.memory.buffer.byteLength;
if (this.heap.stackTop + alignedSize > totalMemory) {
  // 需要扩容内存
  const currentPages = this.memory.buffer.byteLength / (64 * 1024);
  const neededPages = Math.ceil((this.heap.stackTop + alignedSize) / (64 * 1024));
  const pagesToAdd = neededPages - currentPages;
  
  console.log(`WASM内存不足,需要扩容: ${pagesToAdd}页`);
  
  try {
    this.memory.grow(pagesToAdd);
    console.log(`WASM内存扩容成功,当前大小: ${this.memory.buffer.byteLength / (1024 * 1024)}MB`);
  } catch (error) {
    console.error('WASM内存扩容失败:', error);
    throw new Error('内存分配失败,无法扩容内存');
  }
}

// 记录分配的内存块
const ptr = this.heap.stackTop;
this.heap.allocatedBlocks.set(ptr, {
  size: alignedSize,
  alignment,
  allocatedAt: Date.now()
});

// 更新栈顶指针
this.heap.stackTop += alignedSize;

console.log(`分配内存: ${alignedSize} bytes (指针: 0x${ptr.toString(16)})`);
return ptr;
}

// 释放内存块
free(ptr) {
  if (!this.heap.allocatedBlocks.has(ptr)) {
    console.warn(`尝试释放未分配的内存块: 0x${ptr.toString(16)}`);
    return false;
  }
  
  const block = this.heap.allocatedBlocks.get(ptr);
  this.heap.allocatedBlocks.delete(ptr);
  
  // 在实际实现中,这里可以添加内存碎片整理逻辑
  console.log(`释放内存: ${block.size} bytes (指针: 0x${ptr.toString(16)})`);
  return true;
}

// 复制JavaScript数组到WASM内存
copyToWasm(array, type = 'Uint8Array') {
  const TypedArray = globalThis[type];
  if (!TypedArray) {
    throw new Error(`不支持的类型: ${type}`);
  }
  
  const byteLength = array.byteLength || array.length * TypedArray.BYTES_PER_ELEMENT;
  const ptr = this.alloc(byteLength);
  
  // 获取WASM内存视图并复制数据
  const wasmArray = new TypedArray(this.memory.buffer, ptr, array.length);
  wasmArray.set(array);
  
  return { ptr, byteLength, length: array.length, type };
}

// 从WASM内存复制数据到JavaScript
copyFromWasm(ptr, length, type = 'Uint8Array') {
  const TypedArray = globalThis[type];
  if (!TypedArray) {
    throw new Error(`不支持的类型: ${type}`);
  }
  
  if (!this.heap.allocatedBlocks.has(ptr)) {
    throw new Error(`无法访问未分配的内存块: 0x${ptr.toString(16)}`);
  }
  
  const block = this.heap.allocatedBlocks.get(ptr);
  const maxLength = Math.floor(block.size / TypedArray.BYTES_PER_ELEMENT);
  if (length > maxLength) {
    console.warn(`从WASM复制数据时长度超出内存块大小,自动截断: ${length} -> ${maxLength}`);
    length = maxLength;
  }
  
  const array = new TypedArray(this.memory.buffer, ptr, length);
  // 创建副本以避免WASM内存释放后的数据失效
  return new TypedArray(array);
}

// 内存使用统计
getMemoryStats() {
  const totalAllocated = Array.from(this.heap.allocatedBlocks.values())
    .reduce((sum, block) => sum + block.size, 0);
    
  const totalMemory = this.memory ? this.memory.buffer.byteLength : 0;
  
  return {
    totalMemory,
    totalAllocated,
    freeMemory: totalMemory - this.heap.stackTop,
    allocatedBlocks: this.heap.allocatedBlocks.size,
    utilization: totalMemory > 0 ? (totalAllocated / totalMemory) * 100 : 0
  };
}

// 打印内存使用情况
printMemoryStats() {
  const stats = this.getMemoryStats();
  
  console.log('WASM内存使用统计:');
  console.log(`  总内存: ${(stats.totalMemory / (1024 * 1024)).toFixed(2)}MB`);
  console.log(`  已分配: ${(stats.totalAllocated / (1024 * 1024)).toFixed(2)}MB`);
  console.log(`  空闲内存: ${(stats.freeMemory / (1024 * 1024)).toFixed(2)}MB`);
  console.log(`  分配块数量: ${stats.allocatedBlocks}`);
  console.log(`  内存利用率: ${stats.utilization.toFixed(2)}%`);
  
  return stats;
}

// 检测内存泄漏
detectLeaks(thresholdMs = 5000) {
  const now = Date.now();
  const potentialLeaks = [];
  
  for (const [ptr, block] of this.heap.allocatedBlocks) {
    if (now - block.allocatedAt > thresholdMs) {
      potentialLeaks.push({
        ptr,
        size: block.size,
        age: now - block.allocatedAt,
        alignment: block.alignment
      });
    }
  }
  
  if (potentialLeaks.length > 0) {
    console.warn(`检测到潜在的内存泄漏: ${potentialLeaks.length}个块`);
    potentialLeaks.forEach(block => {
      console.warn(`  指针: 0x${block.ptr.toString(16)}, 大小: ${block.size} bytes, 存在时间: ${block.age}ms`);
    });
  }
  
  return potentialLeaks;
}
}

// 高级文件处理应用:使用WebAssembly进行图像识别预处理
class WasmImageProcessor {
constructor() {
  this.memoryManager = new WasmMemoryManager();
  this.processor = new WasmFileProcessor();
  this.processor.onProgress = (percent) => {
    this.onProgress && this.onProgress({
      stage: 'processing',
      percent
    });
  };
}

async initialize() {
  await this.processor.loadWasmModule();
  this.memoryManager.initMemory(512); // 初始分配32MB内存(512页 * 64KB)
}

// 处理图像文件用于AI模型输入
async processImageForAI(file, targetSize = 224) {
  if (!this.processor.isLoaded) {
    await this.initialize();
  }
  
  this.onProgress && this.onProgress({
    stage: 'reading',
    percent: 0
  });
  
  // 读取图像文件
  const arrayBuffer = await new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = () => reject(reader.error);
    reader.readAsArrayBuffer(file);
  });
  
  this.onProgress && this.onProgress({
    stage: 'preprocessing',
    percent: 20
  });
  
  const inputArray = new Uint8Array(arrayBuffer);
  
  // 复制数据到WASM内存
  const { ptr: inputPtr, byteLength } = this.memoryManager.copyToWasm(inputArray);
  
  try {
    // 调用WASM图像处理函数
    // 参数: inputPtr, inputSize, targetWidth, targetHeight, outputFormat
    const outputPtr = this.processor.module.process_image_for_ai(
      inputPtr, 
      byteLength, 
      targetSize, 
      targetSize, 
      0 // 0 = RGB格式,1 = BGR格式,2 = GRAY格式
    );
    
    if (outputPtr === 0) {
      throw new Error('图像处理失败,WASM返回空指针');
    }
    
    this.onProgress && this.onProgress({
      stage: 'postprocessing',
      percent: 80
    });
    
    // 获取处理后的图像数据大小
    const outputSize = this.processor.module.get_image_output_size(outputPtr);
    
    // 从WASM内存复制数据到JavaScript
    const outputData = this.memoryManager.copyFromWasm(
      outputPtr, 
      outputSize / Float32Array.BYTES_PER_ELEMENT, 
      'Float32Array'
    );
    
    // 释放WASM内存
    this.memoryManager.free(outputPtr);
    
    this.onProgress && this.onProgress({
      stage: 'complete',
      percent: 100
    });
    
    // 返回处理后的图像数据和元信息
    return {
      data: outputData,
      width: targetSize,
      height: targetSize,
      channels: 3,
      mean: [0.485, 0.456, 0.406], // ImageNet均值
      std: [0.229, 0.224, 0.225],  // ImageNet标准差
      toTensor: () => {
        // 将数据转换为适合TensorFlow.js的格式
        return tf.tensor4d(outputData, [1, targetSize, targetSize, 3]);
      }
    };
  } finally {
    // 确保释放输入内存
    this.memoryManager.free(inputPtr);
    // 检查潜在的内存泄漏
    this.memoryManager.detectLeaks();
  }
}

// 设置进度回调
onProgress(handler) {
  this.onProgress = handler;
  return this;
}

// 清理资源
dispose() {
  console.log('清理WASM图像处理资源');
  this.memoryManager.printMemoryStats();
  // 释放所有分配的内存
  for (const ptr of this.memoryManager.heap.allocatedBlocks.keys()) {
    this.memoryManager.free(ptr);
  }
}
}

// 使用示例:浏览器端AI图像分类应用
async function browserImageClassificationDemo() {
  const imageProcessor = new WasmImageProcessor();
  
  // 监听处理进度
  imageProcessor.onProgress((status) => {
    console.log(`处理进度: ${status.stage} - ${status.percent.toFixed(2)}%`);
    document.getElementById('processing-status').textContent = 
      `${status.stage}: ${status.percent.toFixed(2)}%`;
  });
  
  try {
    // 选择图像文件
    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.accept = 'image/*';
    
    fileInput.onchange = async (e) => {
      const file = e.target.files[0];
      if (!file) return;
      
      document.getElementById('status').textContent = '开始处理图像...';
      
      // 处理图像
      const processedImage = await imageProcessor.processImageForAI(file);
      
      document.getElementById('status').textContent = '图像预处理完成,准备进行AI推理...';
      
      // 加载AI模型并进行推理
      const model = await tf.loadLayersModel('/models/mobilenet/model.json');
      const tensor = processedImage.toTensor();
      
      // 进行预测
      const predictions = await model.predict(tensor).data();
      
      // 处理预测结果...
      displayPredictions(predictions);
      
      // 清理资源
      tensor.dispose();
      model.dispose();
      imageProcessor.dispose();
    };
    
    fileInput.click();
  } catch (error) {
    console.error('图像分类演示失败:', error);
    document.getElementById('status').textContent = `处理失败: ${error.message}`;
  }
}

五、Electron应用中的文件系统突破:超越浏览器限制

5.1 Electron文件系统访问:无缝桥接Web与桌面

Electron应用结合了Web技术和桌面应用的优势,提供了对本地文件系统的完全访问权限。以下是如何在Electron中实现高级文件操作:

javascript 复制代码
// Electron主进程文件系统服务
const { ipcMain, dialog, app } = require('electron');
const fs = require('fs').promises;
const fsConstants = require('fs').constants;
const path = require('path');
const { pipeline } = require('stream/promises');
const crypto = require('crypto');

class ElectronFileSystemService {
  constructor(mainWindow) {
    this.mainWindow = mainWindow;
    this.initializeIpcHandlers();
    this.watchers = new Map(); // 跟踪文件系统监听器
  }

  initializeIpcHandlers() {
    // 选择目录
    ipcMain.handle('fs:select-directory', async (event, options) => {
      const result = await dialog.showOpenDialog(this.mainWindow, {
        properties: ['openDirectory', 'createDirectory'],
        defaultPath: options.defaultPath || app.getPath('documents'),
        title: options.title || '选择目录'
      });

      if (result.canceled) return null;
      return result.filePaths[0];
    });

    // 读取目录结构
    ipcMain.handle('fs:read-directory', async (event, dirPath, options = {}) => {
      try {
        const entries = await fs.readdir(dirPath, { withFileTypes: true });
        const result = [];

        for (const entry of entries) {
          // 过滤隐藏文件
          if (options.skipHidden && entry.name.startsWith('.')) continue;
          
          const fullPath = path.join(dirPath, entry.name);
          let stats;
          
          try {
            stats = await fs.stat(fullPath);
          } catch (error) {
            console.warn(`无法获取文件状态: ${fullPath}`, error);
            continue;
          }

          const entryInfo = {
            name: entry.name,
            path: fullPath,
            isDirectory: entry.isDirectory(),
            isFile: entry.isFile(),
            isSymbolicLink: entry.isSymbolicLink(),
            size: stats.size,
            createdAt: stats.birthtime,
            modifiedAt: stats.mtime,
            accessedAt: stats.atime
          };

          // 如果是目录且需要递归读取,递归获取子目录信息
          if (entry.isDirectory() && options.recursive) {
            try {
              entryInfo.children = await this.readDirectoryRecursive(
                fullPath, 
                options.depth ? options.depth - 1 : 0, 
                options
              );
            } catch (error) {
              console.warn(`无法读取子目录: ${fullPath}`, error);
              entryInfo.children = [];
              entryInfo.accessError = error.message;
            }
          }

          result.push(entryInfo);
        }

        // 排序:目录在前,文件在后,按名称排序
        result.sort((a, b) => {
          if (a.isDirectory() && !b.isDirectory()) return -1;
          if (!a.isDirectory() && b.isDirectory()) return 1;
          return a.name.localeCompare(b.name);
        });

        return result;
      } catch (error) {
        console.error(`读取目录失败: ${dirPath}`, error);
        throw error;
      }
    });

    // 读取大文件(流式传输)
    ipcMain.handle('fs:read-large-file', async (event, filePath, options = {}) => {
      try {
        const fileId = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
        const stats = await fs.stat(filePath);
        
        // 向渲染进程发送文件元信息
        this.mainWindow.webContents.send('fs:file-meta', {
          fileId,
          name: path.basename(filePath),
          path: filePath,
          size: stats.size,
          modifiedAt: stats.mtime
        });

        // 创建读取流
        const readStream = fs.createReadStream(filePath, {
          highWaterMark: options.chunkSize || 1024 * 1024 * 5 // 默认5MB块
        });

        // 监听流事件
        readStream.on('data', (chunk) => {
          // 将Buffer转换为ArrayBuffer以便在渲染进程中使用
          const arrayBuffer = chunk.buffer.slice(
            chunk.byteOffset, 
            chunk.byteOffset + chunk.byteLength
          );
          
          // 发送数据块到渲染进程
          this.mainWindow.webContents.send('fs:file-data', {
            fileId,
            data: arrayBuffer,
            position: readStream.bytesRead - chunk.byteLength,
            totalSize: stats.size
          });
        });

        readStream.on('end', () => {
          this.mainWindow.webContents.send('fs:file-complete', { fileId });
        });

        readStream.on('error', (error) => {
          this.mainWindow.webContents.send('fs:file-error', {
            fileId,
            error: error.message
          });
        });

        // 监听渲染进程的取消请求
        ipcMain.once(`fs:cancel-read-${fileId}`, () => {
          readStream.destroy(new Error('User canceled read operation'));
        });

        return { fileId, size: stats.size };
      }
              });

        return { fileId, size: stats.size };
      } catch (error) {
        console.error(`读取大文件失败: ${filePath}`, error);
        throw error;
      }
    });

    // 写入大文件(流式接收)
    ipcMain.handle('fs:write-large-file', async (event, options) => {
      const { filePath, fileName, totalSize } = options;
      const fullPath = path.join(filePath, fileName);
      const fileId = `write-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
      
      try {
        // 创建写入流
        const writeStream = fs.createWriteStream(fullPath);
        
        // 返回fileId给渲染进程
        return { fileId, path: fullPath };
      } catch (error) {
        console.error(`创建写入流失败: ${fullPath}`, error);
        throw error;
      }
    });

    // 接收文件数据块
    ipcMain.on('fs:write-chunk', (event, { fileId, data, position }) => {
      const writeStream = this.writeStreams.get(fileId);
      if (!writeStream) {
        event.reply('fs:write-error', { 
          fileId, 
          error: `未找到写入流: ${fileId}` 
        });
        return;
      }
      
      try {
        // 将ArrayBuffer转换为Buffer
        const buffer = Buffer.from(data);
        
        // 写入数据(指定位置确保顺序正确)
        writeStream.write(buffer, (error) => {
          if (error) {
            console.error(`写入数据块失败: ${fileId}`, error);
            event.reply('fs:write-error', { fileId, error: error.message });
          } else {
            event.reply('fs:chunk-written', { fileId, position, bytesWritten: buffer.length });
          }
        });
      } catch (error) {
        console.error(`处理数据块失败: ${fileId}`, error);
        event.reply('fs:write-error', { fileId, error: error.message });
      }
    });

    // 完成文件写入
    ipcMain.handle('fs:complete-write', async (event, fileId) => {
      const writeStream = this.writeStreams.get(fileId);
      if (!writeStream) {
        throw new Error(`未找到写入流: ${fileId}`);
      }
      
      return new Promise((resolve, reject) => {
        writeStream.end((error) => {
          if (error) {
            console.error(`完成文件写入失败: ${fileId}`, error);
            reject(error);
          } else {
            console.log(`文件写入完成: ${writeStream.path}`);
            this.writeStreams.delete(fileId);
            resolve({ 
              path: writeStream.path,
              size: (await fs.stat(writeStream.path)).size
            });
          }
        });
      });
    });

    // 监听文件系统变化
    ipcMain.handle('fs:watch-directory', async (event, dirPath) => {
      try {
        if (this.watchers.has(dirPath)) {
          return { watchId: this.watchers.get(dirPath).watchId };
        }
        
        const watchId = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
          if (!filename) return;
          
          const fullPath = path.join(dirPath, filename);
          this.mainWindow.webContents.send('fs:directory-change', {
            eventType, // 'rename' 或 'change'
            filename,
            fullPath,
            timestamp: Date.now()
          });
        });
        
        const watcherInfo = {
          watchId,
          dirPath,
          startedAt: Date.now()
        };
        
        this.watchers.set(dirPath, watcherInfo);
        return { watchId: watcherInfo.watchId };
      } catch (error) {
        console.error(`监听目录失败: ${dirPath}`, error);
        throw error;
      }
    });

    // 取消监听文件系统变化
    ipcMain.handle('fs:unwatch-directory', async (event, watchId) => {
      try {
        let found = false;
        
        for (const [dirPath, watcherInfo] of this.watchers.entries()) {
          if (watcherInfo.watchId === watchId) {
            fs.unwatchFile(watcherInfo.watchId);
            this.watchers.delete(dirPath);
            found = true;
            break;
          }
        }
        
        if (!found) {
          console.warn(`未找到监听ID: ${watchId}`);
          return false;
        }
        
        return true;
      } catch (error) {
        console.error(`取消监听失败: ${watchId}`, error);
        throw error;
      }
    });
  }

  // 递归读取目录结构
  async readDirectoryRecursive(dirPath, depth = 0, options = {}) {
    if (depth < 0) return [];
    
    const entries = await fs.readdir(dirPath, { withFileTypes: true });
    const result = [];
    
    for (const entry of entries) {
      if (options.skipHidden && entry.name.startsWith('.')) continue;
      
      const fullPath = path.join(dirPath, entry.name);
      let stats;
      
      try {
        stats = await fs.stat(fullPath);
      } catch (error) {
        console.warn(`无法获取文件状态: ${fullPath}`, error);
        continue;
      }
      
      const entryInfo = {
        name: entry.name,
        path: fullPath,
        isDirectory: entry.isDirectory(),
        isFile: entry.isFile(),
        isSymbolicLink: entry.isSymbolicLink(),
        size: stats.size,
        createdAt: stats.birthtime,
        modifiedAt: stats.mtime
      };
      
      // 如果是目录且深度允许,递归读取
      if (entry.isDirectory() && depth > 0) {
        try {
          entryInfo.children = await this.readDirectoryRecursive(
            fullPath, 
            depth - 1, 
            options
          );
        } catch (error) {
          console.warn(`无法读取子目录: ${fullPath}`, error);
          entryInfo.children = [];
          entryInfo.accessError = error.message;
        }
      }
      
      result.push(entryInfo);
    }
    
    return result;
  }
}

// 渲染进程中的文件系统客户端
class FsClient {
  constructor() {
    this.ipcRenderer = window.require('electron').ipcRenderer;
    this.fileReaders = new Map();
    this.fileWriters = new Map();
    this.setupEventListeners();
  }

  setupEventListeners() {
    // 文件元信息
    this.ipcRenderer.on('fs:file-meta', (event, meta) => {
      const reader = this.fileReaders.get(meta.fileId);
      if (reader && reader.onMeta) {
        reader.onMeta(meta);
      }
    });

    // 文件数据块
    this.ipcRenderer.on('fs:file-data', (event, data) => {
      const reader = this.fileReaders.get(data.fileId);
      if (reader && reader.onData) {
        reader.onData({
          fileId: data.fileId,
          data: data.data,
          position: data.position,
          progress: data.position / data.totalSize * 100,
          totalSize: data.totalSize
        });
      }
    });

    // 文件读取完成
    this.ipcRenderer.on('fs:file-complete', (event, data) => {
      const reader = this.fileReaders.get(data.fileId);
      if (reader) {
        if (reader.onComplete) {
          reader.onComplete(data);
        }
        this.fileReaders.delete(data.fileId);
      }
    });

    // 文件读取错误
    this.ipcRenderer.on('fs:file-error', (event, data) => {
      const reader = this.fileReaders.get(data.fileId);
      if (reader) {
        if (reader.onError) {
          reader.onError(data.error);
        }
        this.fileReaders.delete(data.fileId);
      }
    });

    // 目录变化通知
    this.ipcRenderer.on('fs:directory-change', (event, change) => {
      this.onDirectoryChange && this.onDirectoryChange(change);
    });
  }

  // 选择目录
  async selectDirectory(options = {}) {
    return this.ipcRenderer.invoke('fs:select-directory', options);
  }

  // 读取目录结构
  async readDirectory(dirPath, options = {}) {
    return this.ipcRenderer.invoke('fs:read-directory', dirPath, options);
  }

  // 读取大文件(流式)
  async readLargeFile(filePath, callbacks = {}) {
    const result = await this.ipcRenderer.invoke('fs:read-large-file', filePath);
    
    this.fileReaders.set(result.fileId, callbacks);
    
    // 返回取消函数
    return {
      fileId: result.fileId,
      cancel: () => {
        this.ipcRenderer.send(`fs:cancel-read-${result.fileId}`);
        this.fileReaders.delete(result.fileId);
      }
    };
  }

  // 写入大文件
  async writeLargeFile(filePath, fileName, totalSize, callbacks = {}) {
    const result = await this.ipcRenderer.invoke('fs:write-large-file', {
      filePath,
      fileName,
      totalSize
    });
    
    const writer = {
      fileId: result.fileId,
      path: result.path,
      callbacks,
      writtenBytes: 0,
      totalSize
    };
    
    this.fileWriters.set(result.fileId, writer);
    
    // 设置数据块写入确认监听
    this.ipcRenderer.on('fs:chunk-written', (event, data) => {
      if (data.fileId === writer.fileId) {
        writer.writtenBytes += data.bytesWritten;
        if (writer.callbacks.onProgress) {
          writer.callbacks.onProgress({
            position: data.position,
            bytesWritten: data.bytesWritten,
            totalWritten: writer.writtenBytes,
            progress: writer.writtenBytes / writer.totalSize * 100,
            totalSize: writer.totalSize
          });
        }
      }
    });
    
    // 设置错误监听
    this.ipcRenderer.on('fs:write-error', (event, data) => {
      if (data.fileId === writer.fileId) {
        if (writer.callbacks.onError) {
          writer.callbacks.onError(data.error);
        }
        this.cleanupWriter(writer.fileId);
      }
    });
    
    return {
      fileId: writer.fileId,
      path: writer.path,
      writeChunk: (data, position) => this.writeChunk(writer.fileId, data, position),
      complete: () => this.completeWrite(writer.fileId),
      abort: () => this.abortWrite(writer.fileId)
    };
  }

  // 写入数据块
  async writeChunk(fileId, data, position) {
    const writer = this.fileWriters.get(fileId);
    if (!writer) {
      throw new Error(`未找到写入器: ${fileId}`);
    }
    
    return new Promise((resolve, reject) => {
      // 使用一次性监听器等待确认
      const listener = (event, data) => {
        if (data.fileId === fileId && data.position === position) {
          this.ipcRenderer.removeListener('fs:chunk-written', listener);
          resolve(data);
        }
      };
      
      this.ipcRenderer.on('fs:chunk-written', listener);
      
      // 发送数据块
      this.ipcRenderer.send('fs:write-chunk', {
        fileId,
        data,
        position
      });
      
      // 设置超时
      setTimeout(() => {
        this.ipcRenderer.removeListener('fs:chunk-written', listener);
        reject(new Error(`写入数据块超时: 位置 ${position}`));
      }, 30000);
    });
  }

  // 完成写入
  async completeWrite(fileId) {
    const result = await this.ipcRenderer.invoke('fs:complete-write', fileId);
    this.cleanupWriter(fileId);
    return result;
  }

  // 中止写入
  async abortWrite(fileId) {
    // 实现中止逻辑
    this.cleanupWriter(fileId);
  }

  // 清理写入器资源
  cleanupWriter(fileId) {
    const writer = this.fileWriters.get(fileId);
    if (writer) {
      this.ipcRenderer.removeAllListeners(`fs:chunk-written-${fileId}`);
      this.ipcRenderer.removeAllListeners(`fs:write-error-${fileId}`);
      this.fileWriters.delete(fileId);
    }
  }

  // 监听目录变化
  async watchDirectory(dirPath) {
    return this.ipcRenderer.invoke('fs:watch-directory', dirPath);
  }

  // 取消监听目录变化
  async unwatchDirectory(watchId) {
    return this.ipcRenderer.invoke('fs:unwatch-directory', watchId);
  }

  // 设置目录变化回调
  onDirectoryChange(handler) {
    this.onDirectoryChange = handler;
  }
}

// 渲染进程使用示例
async function electronFileManagerDemo() {
  const fsClient = new FsClient();
  
  // 选择目录
  const dirPath = await fsClient.selectDirectory({ title: '选择要管理的目录' });
  if (!dirPath) {
    console.log('用户取消了目录选择');
    return;
  }
  
  console.log(`已选择目录: ${dirPath}`);
  
  // 读取目录结构
  try {
    const dirStructure = await fsClient.readDirectory(dirPath, {
      recursive: true,
      depth: 3,
      skipHidden: true
    });
    
    console.log('目录结构:', dirStructure);
    renderDirectoryTree(dirStructure);
  } catch (error) {
    console.error('读取目录失败:', error);
    showError(`无法读取目录: ${error.message}`);
  }
  
  // 监听目录变化
  const watchResult = await fsClient.watchDirectory(dirPath);
  console.log(`开始监听目录变化,watchId: ${watchResult.watchId}`);
  
  fsClient.onDirectoryChange = (change) => {
    console.log('目录变化:', change);
    showNotification(`文件变化: ${change.filename} (${change.eventType})`);
    // 更新UI显示
    updateDirectoryView(change);
  };
  
  // 读取大文件示例
  const largeFile = findLargestFile(dirStructure);
  if (largeFile) {
    console.log(`准备读取大文件: ${largeFile.name} (${formatSize(largeFile.size)})`);
    
    const reader = await fsClient.readLargeFile(largeFile.path, {
      onMeta: (meta) => {
        console.log('文件元信息:', meta);
        updateFileInfoDisplay(meta);
      },
      onData: (data) => {
        console.log(`接收数据块: 位置 ${data.position}, 进度 ${data.progress.toFixed(2)}%`);
        updateFileProgress(data.progress);
        
        // 处理数据块(例如显示文件内容预览)
        processFileChunk(data.data, data.position);
      },
      onComplete: () => {
        console.log('文件读取完成');
        showNotification('文件读取完成');
      },
      onError: (error) => {
        console.error('文件读取错误:', error);
        showError(`文件读取失败: ${error}`);
      }
  });
    
    // 5秒后取消读取(演示用)
    // setTimeout(() => {
    //   console.log('取消文件读取');
    //   reader.cancel();
    // }, 5000);
  }
}

5.2 跨平台文件操作适配:Windows、macOS与Linux差异处理

不同操作系统的文件系统存在差异,如路径分隔符、文件权限、特殊文件等。以下是跨平台文件操作的关键适配策略:

javascript 复制代码
// 跨平台文件系统工具
class CrossPlatformFsUtils {
  constructor() {
    this.platform = process.platform; // 'win32', 'darwin', 'linux' 等
    console.log(`检测到操作系统: ${this.platform}`);
  }

  // 规范化路径格式
  normalizePath(pathString) {
    if (!pathString) return '';
    
    // 处理Windows路径
    if (this.platform === 'win32') {
      // 将正斜杠转换为反斜杠
      return pathString.replace(/\//g, '\\');
    } else {
      // 在Unix系统上使用正斜杠
      return pathString.replace(/\\/g, '/');
    }
  }

  // 获取系统特定的路径
  getSystemPath(type) {
    switch (type) {
      case 'documents':
        return app.getPath('documents');
      case 'downloads':
        return app.getPath('downloads');
      case 'desktop':
        return app.getPath('desktop');
      case 'pictures':
        return app.getPath('pictures');
      case 'music':
        return app.getPath('music');
      case 'videos':
        return app.getPath('videos');
      case 'home':
        return app.getPath('home');
      default:
        throw new Error(`不支持的路径类型: ${type}`);
    }
  }

  // 验证文件路径有效性(跨平台)
  isValidFilePath(pathString) {
    if (typeof pathString !== 'string' || pathString.trim() === '') {
      return false;
    }

    // 根据不同平台检查非法字符
    let invalidChars;
    if (this.platform === 'win32') {
      // Windows非法路径字符
      invalidChars = /[<>:"/\\|?*]/g;
    } else {
      // Unix/Linux/macOS非法路径字符
      invalidChars = /[\/]/g;
    }

    // 检查路径中是否包含非法字符
    if (invalidChars.test(pathString)) {
      return false;
    }

    // 检查是否为保留文件名(Windows)
    if (this.platform === 'win32') {
      const baseName = path.basename(pathString).toLowerCase();
      const reservedNames = [
        'con', 'prn', 'aux', 'nul', 
        'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
        'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'
      ];
      
      if (reservedNames.includes(baseName) || 
          reservedNames.some(name => baseName.startsWith(`${name}.`))) {
        return false;
      }
    }

    return true;
  }

  // 获取文件图标(跨平台实现)
  async getFileIcon(filePath, options = { size: 64 }) {
    try {
      if (this.platform === 'win32') {
        // Windows平台通过Shell获取图标
        const { shell } = window.require('electron');
        return shell.readFileIcon(filePath, options);
      } else if (this.platform === 'darwin') {
        // macOS平台实现
        return this.getMacOSFileIcon(filePath, options);
      } else {
        // Linux平台实现
        return this.getLinuxFileIcon(filePath, options);
      }
    } catch (error) {
      console.error(`获取文件图标失败: ${filePath}`, error);
      // 返回默认图标
      return this.getDefaultFileIcon(options.size);
    }
  }

  // macOS特定图标获取
  async getMacOSFileIcon(filePath, options) {
    // macOS实现细节...
    const iconSize = options.size || 64;
    // 实际实现会调用macOS的原生API或命令行工具
    return this.getDefaultFileIcon(iconSize);
  }

  // Linux特定图标获取
  async getLinuxFileIcon(filePath, options) {
    // Linux实现细节...
    const iconSize = options.size || 64;
    // 实际实现会读取freedesktop规范的图标主题
    return this.getDefaultFileIcon(iconSize);
  }

  // 默认图标
  getDefaultFileIcon(size) {
    // 返回默认图标...
    const canvas = document.createElement('canvas');
    canvas.width = size;
    canvas.height = size;
    const ctx = canvas.getContext('2d');
    
    // 绘制简单的默认文件图标
    ctx.fillStyle = '#cccccc';
    ctx.fillRect(0, 0, size, size);
    ctx.fillStyle = '#666666';
    ctx.fillRect(size * 0.2, size * 0.2, size * 0.6, size * 0.6);
    
    return canvas.toDataURL('image/png');
  }

  // 文件权限检查(跨平台)
  async checkFilePermissions(filePath, requiredPermissions) {
    try {
      const stats = await fs.stat(filePath);
      let hasPermissions = true;
      
      // 根据平台和文件类型检查权限
      if (this.platform === 'win32') {
        // Windows权限检查实现
        // 简化处理,实际实现需要调用Windows API
        return true;
      } else {
        // Unix/Linux/macOS权限检查
        const mode = stats.mode;
        const isOwner = stats.uid === process.getuid();
        const isGroup = stats.gid === process.getgid();
        
        // 解析权限位
        const permissions = {
          read: isOwner ? !!(mode & 0o400) : isGroup ? !!(mode & 0o040) : !!(mode & 0o004),
          write: isOwner ? !!(mode & 0o200) : isGroup ? !!(mode & 0o020) : !!(mode & 0o002),
          execute: isOwner ? !!(mode & 0o100) : isGroup ? !!(mode & 0o010) : !!(mode & 0o001)
        };
        
        // 检查所需权限
        if (requiredPermissions.includes('read') && !permissions.read) hasPermissions = false;
        if (requiredPermissions.includes('write') && !permissions.write) hasPermissions = false;
        if (requiredPermissions.includes('execute') && !permissions.execute) hasPermissions = false;
      }
      
      return hasPermissions;
    } catch (error) {
      console.error(`检查文件权限失败: ${filePath}`, error);
      return false;
    }
  }
}

// 跨平台文件操作应用示例
class CrossPlatformFileManager {
  constructor() {
    this.fsClient = new FsClient();
    this.platformUtils = new CrossPlatformFsUtils();
    this.isInitialized = false;
  }

  async initialize() {
    if (this.isInitialized) return;
    
    // 初始化逻辑
    this.isInitialized = true;
    console.log(`跨平台文件管理器初始化完成 (平台: ${this.platformUtils.platform})`);
  }

  // 安全复制文件(跨平台实现)
  async safeCopyFile(sourcePath, destDir, options = {}) {
    await this.initialize();
    
    // 验证源路径
    if (!await this.pathExists(sourcePath)) {
      throw new Error(`源文件不存在: ${sourcePath}`);
    }
    
    // 检查源文件权限
    const hasReadPerm = await this.platformUtils.checkFilePermissions(sourcePath, ['read']);
    if (!hasReadPerm) {
      throw new Error(`没有读取源文件的权限: ${sourcePath}`);
    }
    
    // 检查目标目录权限
    const hasWritePerm = await this.platformUtils.checkFilePermissions(destDir, ['write']);
    if (!hasWritePerm) {
      throw new Error(`没有写入目标目录的权限: ${destDir}`);
    }
    
    // 获取源文件名
    let fileName = path.basename(sourcePath);
    
    // 处理目标文件名冲突
    const destPath = await this.resolveFileNameConflict(destDir, fileName, options);
    
    console.log(`正在复制文件: ${sourcePath} -> ${destPath}`);
    
    // 执行复制操作(大文件使用流式复制)
    const fileStats = await fs.stat(sourcePath);
    const fileSize = fileStats.size;
    
    // 根据文件大小选择复制策略
    if (fileSize > 1024 * 1024 * 50) { // 50MB以上的文件使用流式复制
      await this.streamCopyFile(sourcePath, destPath, options.onProgress);
    } else {
      // 小文件直接复制
      await fs.copyFile(sourcePath, destPath);
      options.onProgress && options.onProgress(100);
    }
    
    console.log(`文件复制完成: ${destPath}`);
    return destPath;
  }

  // 流式复制大文件
  async streamCopyFile(sourcePath, destPath, progressCallback) {
    const readStream = fs.createReadStream(sourcePath);
    const writeStream = fs.createWriteStream(destPath);
    
    return new Promise((resolve, reject) => {
      let totalBytesCopied = 0;
      const fileSize = fs.statSync(sourcePath).size;
      
      readStream.on('data', (chunk) => {
        totalBytesCopied += chunk.length;
        const progress = (totalBytesCopied / fileSize) * 100;
        progressCallback && progressCallback(progress);
      });
      
      readStream.on('error', reject);
      writeStream.on('error', reject);
      writeStream.on('finish', resolve);
      
      readStream.pipe(writeStream);
    });
  }

  // 解决文件名冲突
  async resolveFileNameConflict(destDir, fileName, options) {
    const conflictAction = options.conflictAction || 'rename';
    let destPath = path.join(destDir, fileName);
    
    if (await this.pathExists(destPath)) {
      switch (conflictAction) {
        case 'overwrite':
          // 直接覆盖
          return destPath;
        case 'skip':
          // 跳过复制
          throw new Error(`文件已存在,已跳过: ${destPath}`);
        case 'ask':
          // 询问用户(在UI应用中实现)
          return this.promptUserForConflictResolution(destDir, fileName);
        case 'rename':
        default:
          // 自动重命名
          const baseName = path.basename(fileName, path.extname(fileName));
          const extName = path.extname(fileName);
          let counter = 1;
          
          // 找到可用的文件名
          while (await this.pathExists(destPath)) {
            const newFileName = `${baseName} (${counter})${extName}`;
            destPath = path.join(destDir, newFileName);
            counter++;
          }
          
          return destPath;
      }
    }
    
    return destPath;
  }

  // 检查路径是否存在
  async pathExists(path) {
    try {
      await fs.access(path);
      return true;
    } catch {
      return false;
    }
  }

  // 提示用户解决冲突(UI应用中实现)
  async promptUserForConflictResolution(destDir, fileName) {
    // 在实际UI应用中,这里会显示对话框让用户选择
    // 这里简化处理,直接使用重命名策略
    return this.resolveFileNameConflict(destDir, fileName, { conflictAction: 'rename' });
  }
}

掌握File与Blob黑科技:从浏览器操控本地文件夹到现代应用实战指南(上篇)

掌握File与Blob黑科技:从浏览器操控本地文件夹到现代应用实战指南(中篇)

掌握File与Blob黑科技:从浏览器操控本地文件夹到现代应用实战指南(下篇)

相关推荐
前端大卫20 小时前
Vue 和 React 受控组件的区别!
前端
Hy行者勇哥20 小时前
前端代码结构详解
前端
代码AI弗森21 小时前
使用 JavaScript 构建 RAG(检索增强生成)库:原理与实现
开发语言·javascript·ecmascript
Lhy@@21 小时前
Axios 整理常用形式及涉及的参数
javascript
练习时长一年21 小时前
Spring代理的特点
java·前端·spring
水星记_21 小时前
时间轴组件开发:实现灵活的时间范围选择
前端·vue
2501_930124701 天前
Linux之Shell编程(三)流程控制
linux·前端·chrome
潘小安1 天前
『译』React useEffect:早知道这些调试技巧就好了
前端·react.js·面试
@大迁世界1 天前
告别 React 中丑陋的导入路径,借助 Vite 的魔法
前端·javascript·react.js·前端框架·ecmascript
EndingCoder1 天前
Electron Fiddle:快速实验与原型开发
前端·javascript·electron·前端框架