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();
安全最佳实践
-
用户授权原则:
- 所有文件访问必须通过用户显式操作触发
- 清晰说明权限用途
-
内容安全检查:
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'); } -
文件大小限制:
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)`); } } -
沙箱处理不可信文件:
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