前言:被低估的浏览器文件能力
在前端开发的技术栈中,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的强大之处在于:
- 持久化权限:用户授权后,应用可以在后续会话中继续访问同一目录
- 完整文件系统操作:支持创建、删除、重命名文件和目录,以及移动和复制操作
- 实时文件监控:可以监听目录变化,实现类似IDE的文件自动刷新功能
- 高性能访问:直接操作本地文件系统,避免了传统文件上传/下载的网络开销
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黑科技:从浏览器操控本地文件夹到现代应用实战指南(上篇)