7.5 文件 API 与文件系统

7.5 文件 API 与文件系统

现代Web应用经常需要处理用户文件,HTML5提供了一系列文件处理API,使开发者能够在浏览器环境中安全地访问和操作文件系统。这些API为Web应用带来了接近原生应用的文件处理能力。

文件访问基础API

1. 文件选择与基础操作

通过input元素获取文件

<input type="file" id="fileInput" multiple accept="image/*,.pdf">
<script>
  document.getElementById('fileInput').addEventListener('change', (event) => {
    const files = event.target.files; // FileList对象
    Array.from(files).forEach(file => {
      console.log(`文件名: ${file.name}`);
      console.log(`文件类型: ${file.type}`);
      console.log(`文件大小: ${(file.size / 1024).toFixed(2)} KB`);
      console.log(`最后修改: ${new Date(file.lastModified).toLocaleString()}`);
    });
  });
</script>

拖放文件处理

const dropZone = document.getElementById('dropZone');

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropZone.classList.add('dragover');
});

dropZone.addEventListener('dragleave', () => {
  dropZone.classList.remove('dragover');
});

dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  dropZone.classList.remove('dragover');
  
  const items = e.dataTransfer.items;
  for (let i = 0; i < items.length; i++) {
    if (items[i].kind === 'file') {
      const file = items[i].getAsFile();
      processFile(file);
    }
  }
});

2. 文件内容读取

使用FileReader API

function readFile(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    
    reader.onload = (event) => {
      resolve(event.target.result);
    };
    
    reader.onerror = (error) => {
      reject(error);
    };
    
    // 选择读取方式
    if (file.type.startsWith('image/')) {
      reader.readAsDataURL(file);  // 作为DataURL读取
    } else if (file.type === 'application/json') {
      reader.readAsText(file);     // 作为文本读取
    } else {
      reader.readAsArrayBuffer(file); // 作为二进制读取
    }
  });
}

// 使用示例
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (event) => {
  const file = event.target.files[0];
  try {
    const content = await readFile(file);
    console.log('文件内容:', content);
  } catch (error) {
    console.error('读取失败:', error);
  }
});

文件系统访问API

1. 文件系统访问授权

请求文件系统访问权限

async function requestFileAccess() {
  try {
    // 请求目录句柄
    const dirHandle = await window.showDirectoryPicker({
      mode: 'readwrite' // 或 'readonly'
    });
    
    // 检查权限状态
    if ((await dirHandle.queryPermission()) !== 'granted') {
      const permission = await dirHandle.requestPermission();
      if (permission !== 'granted') {
        throw new Error('用户拒绝了权限请求');
      }
    }
    
    return dirHandle;
  } catch (error) {
    console.error('文件访问错误:', error);
    return null;
  }
}

2. 目录与文件操作

目录遍历与文件操作

async function listDirectory(dirHandle) {
  const fileList = [];
  
  for await (const entry of dirHandle.values()) {
    if (entry.kind === 'file') {
      fileList.push({
        name: entry.name,
        type: 'file',
        handle: entry
      });
    } else if (entry.kind === 'directory') {
      fileList.push({
        name: entry.name,
        type: 'directory',
        handle: entry,
        children: await listDirectory(entry)
      });
    }
  }
  
  return fileList;
}

async function saveFile(dirHandle, fileName, content) {
  // 创建或获取文件句柄
  const fileHandle = await dirHandle.getFileHandle(fileName, { create: true });
  
  // 创建可写流
  const writable = await fileHandle.createWritable();
  
  // 写入内容
  await writable.write(content);
  
  // 关闭文件
  await writable.close();
  
  console.log(`文件 ${fileName} 保存成功`);
}

高级文件操作

1. 文件切片与分块上传

async function uploadLargeFile(file, chunkSize = 1024 * 1024) { // 默认1MB
  const totalChunks = Math.ceil(file.size / chunkSize);
  const fileId = generateFileId(file); // 生成唯一ID
  
  for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
    const start = chunkIndex * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    
    const formData = new FormData();
    formData.append('fileId', fileId);
    formData.append('chunkIndex', chunkIndex);
    formData.append('totalChunks', totalChunks);
    formData.append('chunk', chunk, file.name);
    
    try {
      await fetch('/upload-chunk', {
        method: 'POST',
        body: formData
      });
      
      const progress = ((chunkIndex + 1) / totalChunks) * 100;
      updateProgress(progress);
    } catch (error) {
      console.error(`分块 ${chunkIndex} 上传失败:`, error);
      throw error;
    }
  }
  
  // 通知服务器合并分块
  await fetch('/merge-chunks', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ fileId, fileName: file.name })
  });
}

2. 文件系统同步

保存文件句柄引用

// 存储文件句柄引用
async function saveFileHandle(key, handle) {
  const serializable = {
    name: handle.name,
    kind: handle.kind,
    id: handle.id
  };
  
  localStorage.setItem(key, JSON.stringify(serializable));
}

// 恢复文件句柄
async function restoreFileHandle(key) {
  const serialized = localStorage.getItem(key);
  if (!serialized) return null;
  
  const { name, kind, id } = JSON.parse(serialized);
  
  try {
    if (kind === 'file') {
      return await window.showOpenFilePicker({
        id,
        suggestedName: name
      });
    } else {
      return await window.showDirectoryPicker({
        id,
        suggestedName: name
      });
    }
  } catch (error) {
    console.error('恢复文件句柄失败:', error);
    return null;
  }
}

浏览器兼容性与降级方案

1. 特性检测与降级

function checkFileAPISupport() {
  return {
    fileInput: 'files' in HTMLInputElement.prototype,
    fileReader: 'FileReader' in window,
    dragDrop: 'draggable' in document.createElement('span'),
    fileSystem: 'showOpenFilePicker' in window,
    directoryAccess: 'showDirectoryPicker' in window
  };
}

async function handleFileSelection() {
  const support = checkFileAPISupport();
  
  if (support.fileSystem) {
    // 使用现代文件系统API
    const handle = await window.showOpenFilePicker();
    const file = await handle[0].getFile();
    processFile(file);
  } else if (support.fileInput) {
    // 回退到传统input方式
    const input = document.createElement('input');
    input.type = 'file';
    input.onchange = (e) => processFile(e.target.files[0]);
    input.click();
  } else {
    alert('您的浏览器不支持文件操作功能');
  }
}

2. 文件系统配额管理

async function checkStorageQuota() {
  if (!navigator.storage || !navigator.storage.estimate) {
    console.warn('存储配额API不可用');
    return null;
  }
  
  const estimation = await navigator.storage.estimate();
  console.log(`已使用: ${(estimation.usage / 1024 / 1024).toFixed(2)} MB`);
  console.log(`总配额: ${(estimation.quota / 1024 / 1024).toFixed(2)} MB`);
  
  return {
    used: estimation.usage,
    quota: estimation.quota,
    percentage: (estimation.usage / estimation.quota) * 100
  };
}

async function requestPersistence() {
  if (navigator.storage && navigator.storage.persist) {
    const isPersisted = await navigator.storage.persisted();
    if (!isPersisted) {
      const result = await navigator.storage.persist();
      console.log(`持久化存储${result ? '已授予' : '被拒绝'}`);
    }
  }
}

实际应用案例

1. 图片预览与处理

async function processImage(file) {
  // 读取图片
  const imgData = await readAsDataURL(file);
  
  // 创建预览
  const img = document.createElement('img');
  img.src = imgData;
  document.getElementById('preview').appendChild(img);
  
  // 使用Canvas处理图片
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  
  await new Promise((resolve) => {
    img.onload = () => {
      canvas.width = img.width;
      canvas.height = img.height;
      
      // 应用灰度滤镜
      ctx.drawImage(img, 0, 0);
      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
      const data = imageData.data;
      
      for (let i = 0; i < data.length; i += 4) {
        const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
        data[i] = avg;     // R
        data[i + 1] = avg; // G
        data[i + 2] = avg; // B
      }
      
      ctx.putImageData(imageData, 0, 0);
      resolve();
    };
  });
  
  // 转换回Blob
  return new Promise((resolve) => {
    canvas.toBlob((blob) => {
      resolve(new File([blob], `processed_${file.name}`, { type: file.type }));
    }, file.type);
  });
}

2. 本地Markdown编辑器

class MarkdownEditor {
  constructor() {
    this.currentFileHandle = null;
    this.unsavedChanges = false;
    
    this.setupEventListeners();
  }
  
  setupEventListeners() {
    document.getElementById('newBtn').addEventListener('click', this.newFile.bind(this));
    document.getElementById('openBtn').addEventListener('click', this.openFile.bind(this));
    document.getElementById('saveBtn').addEventListener('click', this.saveFile.bind(this));
    document.getElementById('editor').addEventListener('input', () => {
      this.unsavedChanges = true;
    });
  }
  
  async newFile() {
    if (this.unsavedChanges && !confirm('放弃当前未保存的更改?')) {
      return;
    }
    
    document.getElementById('editor').value = '';
    this.currentFileHandle = null;
    this.unsavedChanges = false;
  }
  
  async openFile() {
    try {
      [this.currentFileHandle] = await window.showOpenFilePicker({
        types: [{
          description: 'Markdown Files',
          accept: { 'text/markdown': ['.md', '.markdown'] }
        }]
      });
      
      const file = await this.currentFileHandle.getFile();
      const content = await file.text();
      
      document.getElementById('editor').value = content;
      this.unsavedChanges = false;
    } catch (error) {
      if (error.name !== 'AbortError') {
        alert(`打开文件失败: ${error.message}`);
      }
    }
  }
  
  async saveFile() {
    const content = document.getElementById('editor').value;
    
    try {
      if (!this.currentFileHandle) {
        this.currentFileHandle = await window.showSaveFilePicker({
          suggestedName: 'untitled.md',
          types: [{
            description: 'Markdown Files',
            accept: { 'text/markdown': ['.md', '.markdown'] }
          }]
        });
      }
      
      const writable = await this.currentFileHandle.createWritable();
      await writable.write(content);
      await writable.close();
      
      this.unsavedChanges = false;
      alert('文件保存成功!');
    } catch (error) {
      if (error.name !== 'AbortError') {
        alert(`保存文件失败: ${error.message}`);
      }
    }
  }
}

// 初始化编辑器
new MarkdownEditor();

安全最佳实践

  1. 用户授权原则

    • 所有文件访问必须通过用户显式操作触发
    • 清晰说明权限用途
  2. 内容安全检查

    function isSafeFileType(file) {
      const unsafeTypes = [
        'application/x-msdownload', // .exe
        'application/x-javascript', // .js
        'text/html'                // .html
      ];
      
      return !unsafeTypes.includes(file.type) && 
             !file.name.endsWith('.exe') &&
             !file.name.endsWith('.js');
    }
    
  3. 文件大小限制

    const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
    
    function validateFileSize(file) {
      if (file.size > MAX_FILE_SIZE) {
        throw new Error(`文件大小超过限制 (${MAX_FILE_SIZE/1024/1024}MB)`);
      }
    }
    
  4. 沙箱处理不可信文件

    function renderPDFPreview(file) {
      return new Promise((resolve) => {
        const objectUrl = URL.createObjectURL(file);
        const iframe = document.createElement('iframe');
        
        iframe.sandbox = 'allow-scripts allow-same-origin';
        iframe.src = `/pdf-viewer.html?file=${encodeURIComponent(objectUrl)}`;
        
        iframe.onload = () => {
          URL.revokeObjectURL(objectUrl);
          resolve(iframe);
        };
        
        document.getElementById('previewContainer').appendChild(iframe);
      });
    }
    

文件API与文件系统访问能力极大扩展了Web应用的功能边界,使Web应用能够处理更复杂的业务场景。开发者应当:

  • 优先使用现代文件系统API(如File System Access API)
  • 提供适当的降级方案
  • 始终遵循安全最佳实践
  • 尊重用户隐私和选择权

随着浏览器能力的不断增强,Web应用的文件处理能力将继续向原生应用靠拢,为开发更强大的Web应用奠定基础。

#前端开发 分享于 2025-05-20

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】