第一章:革新 Web 应用 - File System Access API 简介
本章旨在阐明文件处理在 Web 发展历史中的背景和局限性,并将文件系统访问 API(File System Access API)定位为一项革命性的技术。
1.1 超越沙箱:为何传统方式(<input type="file">)已然不足
数十年来,浏览器的安全模型一直建立在严格的“沙箱”原则之上,旨在防止网页直接与用户的本地文件系统进行交互。这是一个至关重要的安全特性,但同时也极大地限制了构建功能强大的、桌面级应用程序的能力 1。
传统的网页文件处理方式主要依赖于 <input type="file"> 元素和 <a download> 标签。当用户通过 <input type="file"> 选择一个文件时,Web 应用获得的是一个只读的 File 对象,其中包含了文件的内容和一些元数据,但这个对象与磁盘上的原始文件之间没有持久的联系 3。如果应用需要“保存”文件,它只能创建一个新的文件副本,然后通过
<a download> 属性触发浏览器下载这个副本。这种模式无法实现对原始文件的就地修改 3。
这种工作流程形成了一种笨拙的“上传 -> 处理 -> 下载副本”的循环。这种模式迫使 Web 应用只能成为本地文件的消费者,而无法成为真正的编辑器。其核心问题在于:
-
用户通过
<input type="file">选择文件。 -
浏览器在内存中创建一个包含文件数据的
File对象(一个数据“快照”)3。 -
应用程序可以读取这份数据,但它完全不知道该文件在磁盘上的原始位置。
-
当用户点击“保存”时,应用程序实际上是创建了一个新的数据块(Blob),并通过
<a download>标签让用户下载这个新文件。 -
最终,用户得到了两个文件:原始文件和新下载的修改后版本。这在桌面应用中相当于每次都执行“另存为”,而不是真正的“保存”。这种糟糕的用户体验严重阻碍了 Web 应用与原生应用(如文本编辑器、IDE)在功能上竞争。
1.2 迎接 File System Access API:浏览器中的真正文件 I/O
File System Access API 是一项现代 Web 标准,旨在安全地打破旧有的沙箱模型,允许 Web 应用在获得用户明确授权后,直接读取、写入和管理用户本地设备上的文件和目录 2。它的核心目标是为 Web 平台带来构建专业级应用(如 IDE、文本编辑器、图像和视频编辑器)所必需的文件系统交互能力 2。
这项新 API 与传统方法相比,在功能和用户体验上都实现了质的飞跃。下表清晰地总结了二者之间的关键差异,直观地展示了学习这项新 API 的价值所在。
表 1:File System Access API 与传统方法的对比
| 功能 | 传统方法 (<input>, <a download>) | File System Access API |
| 读取文件 | 用户选择文件,应用获得一个临时的内存副本。 | 用户选择文件,应用获得一个持久的文件句柄。 |
| 保存文件 | 只能下载一个新副本,无法覆盖原始文件。 | 能够覆盖原始文件,实现真正的“保存”操作。 |
| 目录访问 | 依赖非标准的 webkitdirectory 属性,功能有限。 | 提供标准 API,可遍历、创建和删除文件及子目录。 |
| 用户体验 | “上传/下载”模式,页面刷新后状态丢失。 | “打开/保存”模式,状态可以被持久化。 |
| 权限模型 | 隐式授予单次读取权限。 | 显式的、粒度化的读/写权限控制。 |
1.3 核心概念:句柄、用户手势与安全承诺
要理解 File System Access API,必须掌握其几个核心概念。
首先是句柄(Handles)。API 的核心抽象是 FileSystemFileHandle(文件句柄)和 FileSystemDirectoryHandle(目录句柄)。它们代表了文件系统中的一个文件或目录的引用或“指针”,而不是文件内容本身 6。这个句柄是轻量级的,并且可以被序列化,这意味着它可以被长期保存,从而实现持久化访问。
其次是安全上下文(Secure Context)和用户手势(User Gesture)。由于该 API 的强大能力,浏览器实施了严格的安全措施。所有能够触发文件或目录选择器的 API 调用(如 window.showOpenFilePicker())都必须在安全上下文(即 HTTPS)中执行,并且必须由用户的主动交互(如点击按钮)触发 2。这意味着网页无法在后台静默地访问用户文件,用户始终掌握着控制权。
与传统 File 对象不同,FileSystemFileHandle 是一个指向磁盘上某个位置的动态指针。传统的 File 对象一旦被读入内存,就与磁盘上的原始文件失去了联系;如果原始文件被其他程序修改,内存中的 File 对象不会有任何变化。而文件句柄则是一个“实时”链接。每次调用 handle.getFile() 方法时,它都会从磁盘上读取文件的当前状态 8。这意味着如果用户使用其他程序修改了文件,Web 应用下一次通过句柄读取时将能获取到最新的内容。这种设计对于需要与其他工具协同工作的应用至关重要。
1.4 双系统传说:用户可见文件系统与源私有文件系统(OPFS)
File System Access API 能够与两种截然不同的文件系统进行交互。第一种是用户可见的常规文件系统,例如用户的“文档”或“下载”文件夹。第二种是源私有文件系统(Origin Private File System, OPFS) 1。
OPFS 是一个为每个网站源(origin)独立提供的、与操作系统文件系统隔离的沙箱化存储空间。它对用户不可见,但为 Web 应用提供了一个高性能的文件存储后端。OPFS 中的文件操作经过高度优化,尤其适合需要频繁、快速读写的场景,例如 WebAssembly 应用、数据库引擎或大型游戏资源的管理 1。我们将在第四章深入探讨 OPFS 的高级应用。
第二章:读者的工具箱 - 访问本地文件和目录
本章将提供详尽的实践代码,指导您开始读取本地文件和目录。
2.1 开启门户:使用 window.showOpenFilePicker() 读取单个和多个文件
读取文件的入口点是 window.showOpenFilePicker() 方法。调用它会向用户显示一个标准的文件选择对话框 2。该方法返回一个 Promise,成功解析后会得到一个包含文件句柄的数组。
读取单个文件:
JavaScript
let fileHandle;
const button = document.getElementById('open-file-btn');
button.addEventListener('click', async () => {
try {
// showOpenFilePicker 返回一个数组,即使只选择一个文件
// 使用数组解构可以方便地获取第一个句柄
[fileHandle] = await window.showOpenFilePicker();
console.log(fileHandle);
} catch (err) {
if (err.name === 'AbortError') {
// 用户取消了文件选择,这不是一个真正的错误
console.log('User cancelled the file picker.');
} else {
console.error(err);
}
}
});
自定义文件选择器并读取多个文件:
可以通过传递一个 options 对象来自定义文件选择器的行为。例如,可以限制可选的文件类型或允许选择多个文件 2。
JavaScript
const openImagesButton = document.getElementById('open-images-btn');
openImagesButton.addEventListener('click', async () => {
const pickerOpts = {
// 允许选择多种类型的文件
types: [
{
description: 'Images',
accept: {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
},
},
{
description: 'Text',
accept: {
'text/plain': ['.txt'],
},
},
],
// 不显示“所有文件”选项
excludeAcceptAllOption: true,
// 允许用户选择多个文件
multiple: true,
};
try {
const fileHandles = await window.showOpenFilePicker(pickerOpts);
for (const handle of fileHandles) {
console.log(`Selected file: ${handle.name}`);
}
} catch (err) {
console.error(err);
}
});
2.2 理解 FileSystemFileHandle:通往文件的钥匙
FileSystemFileHandle 是您与文件交互的凭证。它有两个核心属性 2:
-
kind: 一个字符串,对于文件句柄,其值为'file'。 -
name: 文件的名称,例如'document.txt'。
需要再次强调,句柄本身不是文件内容。它只是一个引用,一个通往文件的“钥匙”。
2.3 从句柄到内容:使用 getFile() 读取数据
获得了文件句柄后,下一步就是读取文件内容。这需要两步完成:
-
调用
fileHandle.getFile()方法。这个异步方法会返回一个 Promise,解析后得到一个标准的File对象。这个File对象包含了文件的快照数据 8。 -
使用
File对象上的标准方法(如file.text()、file.arrayBuffer()或file.stream())来读取具体内容 2。
这种设计将 API 的新部分(句柄)与开发者已经熟悉的部分(File API)巧妙地连接起来,降低了学习曲线。
完整示例:选择并显示文本文件内容
JavaScript
const openBtn = document.getElementById('open-btn');
const contentEl = document.getElementById('content');
openBtn.addEventListener('click', async () => {
try {
const [fileHandle] = await window.showOpenFilePicker();
// 1. 从句柄获取 File 对象
const file = await fileHandle.getFile();
// 2. 读取文件内容为文本
const contents = await file.text();
// 3. 将内容显示在页面上
contentEl.textContent = contents;
} catch (err) {
console.error(err);
}
});
2.4 探索迷宫:使用 window.showDirectoryPicker() 浏览目录结构
除了文件,该 API 还提供了强大的目录访问能力。window.showDirectoryPicker() 方法是访问目录的入口点,它会提示用户选择一个文件夹 6。
调用此方法会返回一个 FileSystemDirectoryHandle。如果希望对目录进行写入操作(例如创建或删除文件),可以在调用时请求写权限 12:
JavaScript
const openDirBtn = document.getElementById('open-dir-btn');
openDirBtn.addEventListener('click', async () => {
try {
// 请求读写权限
const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
console.log('Directory handle:', dirHandle);
} catch (err) {
console.error(err);
}
});
2.5 掌握异步迭代:使用 for await...of 遍历目录条目
FileSystemDirectoryHandle 提供了一组异步迭代器方法,用于遍历目录内容,包括 entries()、keys() 和 values() 14。处理这些异步迭代器的最佳方式是使用 ES2018 引入的
for await...of 循环语法。
这种现代语法不仅代码可读性极高,而且完美契合了 API 的设计理念。由于目录可能包含成千上万个文件,一次性加载所有条目会非常低效。异步迭代器允许我们按需、逐个地处理条目,而 for await...of 则优雅地处理了这一过程中的 Promise 解析 16。学习使用此 API 的过程,本身也是一次对现代 JavaScript 异步编程范式的实践。
示例:递归遍历目录并打印所有文件名
以下函数展示了如何递归地遍历一个目录及其所有子目录,并打印出每个文件的相对路径。
JavaScript
async function listAllFilesAndDirs(dirHandle, path = dirHandle.name) {
// dirHandle.values() 返回一个异步迭代器
for await (const entry of dirHandle.values()) {
const newPath = `${path}/${entry.name}`;
console.log(newPath);
if (entry.kind === 'directory') {
await listAllFilesAndDirs(entry, newPath);
}
}
}
// 假设 dirHandle 是通过 showDirectoryPicker 获取的
// listAllFilesAndDirs(dirHandle);
注意:有报告指出,在处理包含数千个文件的非常大的目录时,异步迭代器可能会出现不一致的结果,并非每次都能返回完整的条目列表。开发者在使用时应意识到这一潜在的局限性 19。
第三章:创造者的工坊 - 写入、保存与修改文件
本章将全面介绍 API 的“写”操作,让您能够构建可以创建和修改数据的应用程序。
3.1 从白板到存档:window.showSaveFilePicker() 工作流
当需要创建一个新文件并保存时,应使用 window.showSaveFilePicker() 方法。它会弹出一个文件保存对话框,让用户选择保存位置和文件名 5。
与 showOpenFilePicker 类似,showSaveFilePicker 也接受一个 options 对象,其中 suggestedName 和 types 属性可以极大地改善用户体验 7。
示例:将文本框内容保存为新文件
JavaScript
const saveAsBtn = document.getElementById('save-as-btn');
const textArea = document.getElementById('text-editor');
saveAsBtn.addEventListener('click', async () => {
try {
const saveOptions = {
suggestedName: 'untitled.txt',
types: },
}],
};
const fileHandle = await window.showSaveFilePicker(saveOptions);
// 接下来是写入操作...
} catch (err) {
console.error(err);
}
});
3.2 写入操作剖析:createWritable()、write() 与至关重要的 close()
写入文件的过程分为三个关键步骤,缺一不可:
-
创建可写流 (
createWritable): 首先,在文件句柄上调用handle.createWritable()方法。这个异步方法会返回一个FileSystemWritableFileStream对象 20。这是一个关键步骤,因为浏览器通常会在此阶段创建一个临时文件(或称“交换文件”),所有的写入操作都将先作用于这个临时文件。 -
写入内容 (
write): 接着,调用可写流的writable.write()方法,将数据写入流中。write方法可以接受字符串、Buffer 或 Blob 作为参数 5。 -
关闭流 (
close): 最后,也是最重要的一步,调用writable.close()方法。这个方法会结束写入过程,并将临时文件中的内容原子性地应用到目标文件中(通常是通过重命名或移动操作)5。
忘记调用 close() 是一个常见的初学者错误,这会导致文件内容不会被真正保存。
这种“写入临时文件,关闭时替换”的模式是一个内置的安全机制。它确保了文件操作的原子性。如果写入过程中浏览器崩溃或断电,原始文件将保持不变,避免了文件被损坏或处于“半写入”状态。只有当 close() 成功执行时,文件才会被完整、正确地更新。这个事务性的特性为数据完整性提供了强大的保障,而开发者无需手动实现 20。
完整写入函数示例:
JavaScript
async function saveFile(fileHandle, contents) {
// 1. 创建可写文件流
const writable = await fileHandle.createWritable();
// 2. 写入内容
await writable.write(contents);
// 3. 关闭文件流并写入磁盘
await writable.close();
console.log('File saved successfully!');
}
3.3 就地编辑:精确修改现有文件
File System Access API 最大的优势之一就是能够实现真正的“保存”操作,即就地修改文件。这结合了第二章的读取和本章的写入概念。
完整的“打开 -> 编辑 -> 保存”周期示例:
JavaScript
let currentFileHandle;
const openBtn = document.getElementById('open-btn');
const saveBtn = document.getElementById('save-btn');
const editor = document.getElementById('editor');
// 打开文件
openBtn.addEventListener('click', async () => {
[currentFileHandle] = await window.showOpenFilePicker();
const file = await currentFileHandle.getFile();
editor.value = await file.text();
saveBtn.disabled = false;
});
// 保存文件
saveBtn.addEventListener('click', async () => {
if (!currentFileHandle) return;
const writable = await currentFileHandle.createWritable();
await writable.write(editor.value);
await writable.close();
alert('File saved!');
});
在调用 createWritable 时,可以传入 keepExistingData: true 选项,这在某些需要保留现有文件内容并进行修改的场景中非常有用 20。
3.4 从零构建:以编程方式创建新文件和目录
API 允许在用户选择的目录中以编程方式创建新的文件和子目录。这通过在 get...Handle 方法中传递 { create: true } 选项来实现 22。
示例:在所选目录中创建一个新文件和一个新文件夹
JavaScript
async function createStructure(dirHandle) {
// 创建一个新文件夹 'logs'
const logsDirHandle = await dirHandle.getDirectoryHandle('logs', { create: true });
// 在 'logs' 文件夹中创建一个新文件 'today.log'
const logFileHandle = await logsDirHandle.getFileHandle('today.log', { create: true });
// 向新文件中写入内容
const writable = await logFileHandle.createWritable();
await writable.write(`Log entry at ${new Date().toISOString()}`);
await writable.close();
console.log('Created logs/today.log');
}
值得注意的是,API 的设计是层级化和显式的。您不能通过一次调用 dirHandle.getFileHandle('subdir/myfile.txt', { create: true }) 来创建跨层级的文件。API 规定 name 参数必须是有效的文件名,而不是路径 26。您必须首先获取或创建
subdir 的目录句柄,然后才能在该句柄上创建 myfile.txt 27。这种设计强制开发者进行明确的、分步的创建过程,增强了操作的透明度和安全性,避免了单次调用可能引发的复杂递归创建行为。
3.5 清理工作:删除文件和文件夹
要在目录中删除条目,可以使用 directoryHandle.removeEntry(name) 方法。如果要删除一个非空目录,需要设置 { recursive: true } 选项 28。
JavaScript
// 删除名为 'old-file.txt' 的文件
// 假设 dirHandle 是一个有效的目录句柄
await dirHandle.removeEntry('old-file.txt');
// 递归删除名为 'temp-folder' 的目录及其所有内容
await dirHandle.removeEntry('temp-folder', { recursive: true });
此外,一个更新的、更符合人体工程学的方法是直接在文件或目录句柄本身上调用 handle.remove()。这个方法允许通过条目自身的句柄来删除它,而无需先获取其父目录的句柄。不过,需要注意该方法的浏览器支持度相对有限 29。
第四章:构建健壮应用的高级技巧
本章将引导您从基础用法过渡到构建专业、有状态的应用程序。
4.1 权限模型深度解析:queryPermission() 与 requestPermission()
File System Access API 的安全模型围绕着一个动态的、以用户为中心的权限系统构建。权限状态共有三种:'granted'(已授予)、'denied'(已拒绝)和 'prompt'(需要询问)31。
-
queryPermission(): 这个方法用于查询句柄的当前权限状态,而不会打扰用户。它接受一个可选的{ mode: 'readwrite' }参数来查询读写权限 31。这是一个被动的检查。 -
requestPermission(): 当权限状态为'prompt'时,您必须调用此方法来请求用户授权。这个调用会触发一个浏览器级别的权限提示框,并且必须由用户手势激活 33。
权限并非永久性的。浏览器可能会在会话结束后重置权限,用户也可以随时在浏览器设置中撤销权限 7。
这种权限的生命周期与会话状态和用户交互紧密相关。当一个应用通过 IndexedDB 等方式持久化了一个文件句柄后,下次启动时,浏览器出于安全考虑,通常会将该句柄的权限状态重置为 'prompt' 31。因此,一个健壮的应用在对持久化的句柄进行任何操作前,都必须遵循“先查询,后请求”的模式。
推荐的权限验证流程:
JavaScript
async function verifyPermission(fileHandle, withWrite = false) {
const options = { mode: withWrite? 'readwrite' : 'read' };
// 查询当前权限状态
if (await fileHandle.queryPermission(options) === 'granted') {
return true;
}
// 如果未授予,则请求权限
if (await fileHandle.requestPermission(options) === 'granted') {
return true;
}
// 用户拒绝了权限
return false;
}
在尝试读写之前调用此函数,可以确保应用总是在拥有必要权限的情况下运行,同时为用户提供了清晰的控制权。
4.2 构建持久化体验:在 IndexedDB 中存储文件和目录句柄
FileSystemHandle 对象是可序列化的,这意味着它们可以被存储在 IndexedDB 中 7。这为构建能够“记住”用户工作状态的应用打开了大门,例如实现“最近打开的文件”列表或在应用启动时自动重新打开上次编辑的目录。
以下是使用一个简单的库 idb-keyval 来存储和检索文件句柄的示例 35:
JavaScript
import { get, set } from 'idb-keyval';
const RECENT_FILE_KEY = 'recent-file-handle';
// 保存句柄
async function saveHandle(fileHandle) {
await set(RECENT_FILE_KEY, fileHandle);
}
// 检索句柄
async function getHandle() {
return await get(RECENT_FILE_KEY);
}
// 应用启动逻辑
async function onAppLoad() {
const recentFileHandle = await getHandle();
if (recentFileHandle) {
// 重新加载文件前,必须验证权限!
const hasPermission = await verifyPermission(recentFileHandle, true);
if (hasPermission) {
// 加载文件内容...
} else {
alert('Could not get permission for the last opened file.');
}
}
}
这个例子与上一节的权限模型紧密相连,展示了从 IndexedDB 检索句柄后,必须重新验证权限的完整流程。
4.3 高性能存储:源私有文件系统(OPFS)入门
OPFS 是为需要极致性能的场景设计的。通过 navigator.storage.getDirectory() 可以获取 OPFS 的根目录句柄 6。
OPFS 的最大优势在于,它允许在 Web Worker 中通过 fileHandle.createSyncAccessHandle() 方法进行同步文件 I/O 操作 1。在常规的 Web 开发中,同步操作通常会阻塞主线程,但 Web Worker 的独立线程环境使其成为可能。对于需要处理大量二进制数据、对性能要求极高的应用(如运行在 WebAssembly 上的 SQLite 数据库、视频编辑器或大型游戏),同步 API 能够避免 Promise 带来的开销,从而实现接近原生的性能。
第五章:从开发到部署 - 生产级最佳实践
本章将为您提供构建可靠、生产质量应用的知识。
5.1 防御性编程:常见错误及其优雅处理
与文件系统交互时,错误在所难免。使用 try...catch 块捕获异常,并通过检查 error.name 属性来识别错误类型,是提供良好用户体验的关键 36。
常见的 DOMException 错误包括 9:
-
AbortError: 用户主动关闭了文件选择或保存对话框。这通常不应被视为错误,可以静默处理。 -
NotAllowedError: 用户拒绝了权限请求。应用应向用户解释为何需要权限,并提供替代方案。 -
NotFoundError: 尝试访问的文件或目录不存在。 -
TypeMismatchError: 尝试将一个目录作为文件打开,或反之。 -
InvalidModificationError: 进行了不允许的修改,例如尝试删除一个非空目录但未设置recursive: true。
错误处理示例:
JavaScript
async function saveMyFile() {
try {
const handle = await window.showSaveFilePicker();
//... 写入操作
} catch (err) {
if (err.name === 'AbortError') {
console.log('Save operation was cancelled by the user.');
} else {
alert(`An error occurred: ${err.name} - ${err.message}`);
}
}
}
5.2 确保通用访问:浏览器兼容性与优雅降级
File System Access API 是一项现代技术,并非所有浏览器都完全支持。因此,为保证应用的普适性,必须考虑浏览器兼容性并提供优雅降级方案。
表 2:浏览器兼容性矩阵
下表总结了 API 核心功能在主流桌面浏览器中的支持情况 7。
| 功能 | Chrome | Firefox | Safari | Edge |
showOpenFilePicker | 86+ | 111+ | 15.2+ | 86+ |
showSaveFilePicker | 86+ | ❌ | 15.2+ | 86+ |
showDirectoryPicker | 86+ | 111+ | 15.2+ | 86+ |
createWritable | 86+ | 111+ | 15.2+ | 86+ |
OPFS (getDirectory) | 86+ | 111+ | 15.2+ | 86+ |
createSyncAccessHandle | 102+ | 111+ | 15.2+ | 102+ |
优雅降级(Graceful Fallback)
优雅降级是一种设计策略,即在现代浏览器中提供最佳体验,同时在不支持新特性的旧浏览器中提供基础的核心功能 41。对于 File System Access API,这意味着:
-
特性检测: 在调用 API 之前,先检查它是否存在。
-
提供备用方案: 如果 API 不存在,则回退到传统的文件处理方式。
示例:一个同时支持新旧浏览器的保存函数
JavaScript
async function saveContent(blob, fileName) {
// 1. 特性检测
if ('showSaveFilePicker' in window) {
try {
// 使用新 API
const handle = await window.showSaveFilePicker({ suggestedName: fileName });
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return;
} catch (err) {
if (err.name === 'AbortError') return;
console.error(err);
}
}
// 2. 优雅降级
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fileName;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
}
为了简化这一过程,可以考虑使用 browser-fs-access 这样的库,它封装了特性检测和优雅降级的逻辑,让开发者能用统一的接口编写代码 2。
5.3 融会贯通:从零开始构建一个迷你文本编辑器
作为本指南的总结,我们将构建一个简单的文本编辑器,它将综合运用前面学到的所有核心概念:
-
UI: 包含“打开”、“保存”、“另存为”按钮和一个
<textarea>编辑区。 -
状态管理: 使用一个变量来存储当前文件的
fileHandle。 -
文件操作:
-
“打开”按钮使用
showOpenFilePicker,读取文件内容并填充到文本区。 -
“保存”按钮重用已存储的
fileHandle,并使用createWritable将文本区内容写回文件。 -
“另存为”按钮使用
showSaveFilePicker将内容保存到新文件。
-
-
权限处理: 在每次保存操作前,都使用
verifyPermission辅助函数来检查和请求写权限。 -
持久化: 使用 IndexedDB 存储最后一个打开文件的句柄,并在页面加载时尝试重新打开它。
-
兼容性: 对所有文件操作进行特性检测,为不支持 API 的浏览器提供基于
<input>和<a download>的降级方案。
这个项目将把所有抽象的知识点转化为一个具体、可用的应用程序,为您掌握 File System Access API 打下坚实的基础。
结论:未来在你手中
File System Access API 是一项真正具有变革意义的技术。它打破了长期以来束缚 Web 应用的枷锁,极大地缩小了 Web 应用与原生桌面应用之间的功能差距。通过提供对本地文件系统的直接、安全且持久的访问能力,它为开发者创造前所未有的、功能丰富的在线体验铺平了道路。
从构建一个简单的文本编辑器到开发复杂的、基于云和本地混合存储的专业工具,这项 API 开启了无限可能。掌握它,意味着您不仅学会了一项新技术,更是获得了一把开启下一代 Web 应用大门的钥匙。现在,是时候去探索和创造了。
2809

被折叠的 条评论
为什么被折叠?



