掌握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黑科技:从浏览器操控本地文件夹到现代应用实战指南(下篇)

相关推荐
计算机魔术师1 小时前
管谷歌的不是Pichai一个人,是一整层AI智能体
前端
米米商聊1 小时前
图标能点,按钮就可用了吗?键盘、名称与收藏状态
前端·javascript
Younglina2 小时前
三个晚上,我和 WorkBuddy 做了个生图网站
前端·后端·ai编程
xy34532 小时前
Axure 9.0 内联框架加载方式
前端·ui·html·axure·原型·产品设计
技术小甜甜3 小时前
【Cursor】Skill 被标黄色警告,怎么改成合格的 SKILL.md
开发语言·javascript·ai编程·cursor·1024程序员节·ai agent·skills
陈随易3 小时前
副业搞钱点子之收入查看器
前端·后端
Profile排查笔记3 小时前
浏览器指纹逆向怎么分析:从网络请求追踪到 JavaScript 采集链
前端·人工智能·后端·自动化
hudie7654 小时前
LikeShop 二开最常改的 5 个文件(附改前改后对比)
前端·uni-app·php·个人开发
qetfw4 小时前
Bash 函数与流程控制:read、if、for、case 验证
linux·前端·javascript·bash
鬼手点金5 小时前
从 API 消息到模型 Token:Chat Template
java·前端·javascript·数据库·人工智能·深度学习·1024程序员节