前言
在 HTML5 标准的File API出现之前,前端对于文件的操作是非常有局限性的,大多需要配合后端实现。出于安全角度考虑,从本地上传文件时,代码不可能获取文件在用户本地的地址,所以纯前端不可能完成一些类似图片预览的功能。但是 File API 的出现,让这一切变成了可能。
然而出于安全性考虑,File API存在很多局限性,例如不能直接读取本地文件,创建文件需要通过下载的方式,以及读取文件后不能即时保存修改等等。
本文要介绍的是由 [WICG(Web Incubator Community Group) ](Web Incubator Community Group (WICG)) 制定的 File System Access API,其基于现有FIle 接口提供了更强大的方法来处理本地文件。
来自官方文档的描述:
这个接口所提供的一系列方法能够让网页直接读取文件并保存修改到本地,以及访问目录。相比于HTML5 所提供的File、FileReader 确实是提供了更强大的能力。不过目前该接口只在Chrome及chromium系的Edge浏览器上使用。
这里介绍下Web Incubator Community Group (WICG),中文全称翻译叫web平台孵化器社区小组,是w3c组织的一个分支,负责设计下一代的web标准,因此它们会时不时发布一些接口提案,有兴趣的可以看下这篇介绍 WICG: Evolving the Web from the ground up | W3C Blog。
在WICG官网,有许多正在孵化的API实现,这里我们主要关注File System Access,这个接口经历了初始草案,迭代并发布,下面就让我们来使用它。

开始
我们可以新建一个页面文件或者直接在控制台来使用File System Access的相关方法。
打开文件
打开文件我们需要调用一个全局方法showOpenFilePicker(),该方法是异步的,调用后会弹出一个文件选择对话框,选择文件后会返回一个存有文件句柄的数组,之所以是数组,因为它允许我们选择多个文件,只需在调用时传入相应的参数即可,包括默认打开的文件目录、文件类型、文件数量等等。
给打开文件按钮绑定一个事件,点击后调用showOpenFilePicker函数,并将文件句柄保存起来。
文件句柄通常是一个系统标识符,可以用来描述窗体、文件等,在C++中,文件句柄就是指向各类对象的指针。
const BtnOpenFile = document.getElementById('btn-choose-file')
const editor = document.getElementById('editor')
let fileHandle;
BtnOpenFile.addEventListener('click', async () => {
[fileHandle] = await window.showOpenFilePicker()
});
从fileHandle对象上,我们能获得文件的内容,属性等信息,

调用它的getFile方法,得到一个[Blob](Blob() - Web API 接口参考 | MDN (mozilla.org))对象,再调用text()获得文件的内容,对于Blob的其他方法如slice,常用的就是用来作文件切片,这里就不多加介绍。
const BtnOpenFile = document.getElementById('btn-choose-file')
const editor = document.getElementById('editor')
const fileHandle;
BtnOpenFile.addEventListener('click', async () => {
[fileHandle] = await window.showOpenFilePicker()
const fileBlob = await fileHandle.getFile()
editor.value = await fileBlob.text()
});
打开文件并获取内容input标签也能实现。
getFile()获取的文件句柄可能会失效。当获取了句柄后,删除或移动源文件会造成文件句柄对象失效,这时只能重新调用getFile()获得新的句柄。
新建文件并保存
要新建文件,我们调用showSaveFilePicker()方法,它会打开一个保存文件对话框,同时我们还能传入参数来设置文件的名称、类型等,调用后返回新创建文件的句柄。
const BtnSaveAs = document.getElementById('btn-save-as')
async function fileSaveAs(description) {
const options = {
types: [
{
description,
accept: {
'text/plain': ['.txt'],
},
},
],
};
return await window.showSaveFilePicker(options);
}
BtnSaveAs.addEventListener('click',async ()=>{
const handle = await fileSaveAs("Hello File Access Api")
})
点击按钮并选择保存位置后,新的文件便被创建和保存,但此时文件是空的,下面我们就往文件内写入数据。
以前要保存文件我们通常生成标签<a download="file_name">,然后模拟点击把文件下载下来,如果它是基于现有文件修改的,并不会覆盖原文件。
写入数据
要往文件里写入内容,需要调用文件句柄对象上的createWritable()来创建流,它是一个FileSystemWritableFileStream对象。由于从web写入文件到系统是一种不安全的行为,所以浏览器会询问我们是否授权。
async function writeFile(fileHandle, contents) {
// createWritable()创建一个可写流对象WritableStream
const writable = await fileHandle.createWritable();
// 通过管道将数据传输到文件
await writable.write(contents);
// 管道使用完毕后需要关闭
await writable.close();
}
BtnSaveAs.addEventListener('click',async ()=>{
const handle = await fileSaveAs("Hello File Access Api")
await writeFile(handle,editor.value)
})
写入文件的过程是流操作,使用了Stream相关的API,这里使用的FileSystemWritableFileStream实际上是一个WritableStream实例。
write()方法负责写入数据,可以是字符串,Blob对象,也可以是流。
举个栗子,fetch请求响应的response.body就是一个readableStream实例,因此可以直接通过write()写入文件中:
async function writeURLToFile(fileHandle, url) {
// 创建要写入的FileSystemWritableFileStream实例
const writable = await fileHandle.createWritable();
// 请求资源
const response = await fetch(url);
// response.body是一个readableStream实例,使用pipeTo建立管道进行数据传输
await response.body.pipeTo(writable);
// pipeTo()创建的管道会自动关闭
}
然而就像在node中使用管道一样,数据传输完成后,如果管道不会自动关闭,那么一定要手动关闭。
使用默认文件名和默认目录
有时我们希望保存文件时有一个默认文件名,就像Typora的默认文件名是Untitled那样,又或者我们希望能设置打开文件时所处的默认目录,这些都能通过suggestedName、startIn这两个属性来实现,只需掉调用类似showXxxPicker方法时传入即可。
const fileHandle = await self.showSaveFilePicker({
// 默认文件名
suggestedName: 'Untitled',
// 默认打开桌面目录
startIn: 'desktop'
types: [{
description: 'Text documents',
accept: {
'text/plain': ['.txt'],
},
}],
});
这是常用的系统目录列表:
desktop: 桌面documents: 文档downloads: 下载music: 音乐pictures: 图像videos: 视频
除了常用的系统目录,我们也能传入自定义目录的句柄,自定义目录需要我们调用showDirectoryPicker()获得。
// 选择文件要保存的目录
const directoryHandle = await showDirectoryPicker()
const fileHandle = await showOpenFilePicker({
// 作为文件保存的起始目录
startIn: directoryHandle
});
打开目录并获取其内容
调用showDirectoryPicker(),我们可以选择要查看的目录,然后返回对应的目录句柄,它是一个FileSystemDirectoryHandle对象实例,使用它的values()方法我们就能查看目录中有哪些文件了。
const btnOpenDirectory = document.getElementById('btn-open-dir');
btnOpenDirectory.addEventListener('click', async () => {
const dirHandle = await window.showDirectoryPicker();
for await (const item of dirHandle.values()) {
console.log(item)
}
});
在打开之前,浏览器会询问我们是否允许,允许后会在地址栏内出现一个目录标志。


控制台输出了目录中有哪些文件。

但是处于安全着想,浏览器不允许我们访问一些敏感目录,例如包含系统文件的目录或者组策略不允许访问的目录。

创建或访问目录中的文件与文件夹
在目录中,可以调用目录句柄的getFileHandle()和getDirectoryHandle()方法来访问文件和子目录,同时可以传入一个{create}对象参数,表示在当文件或目录不存在时创建它。
// 当MyDocuments不存在时将会创建它
const newDirectoryHandle = await existingDirectoryHandle.getDirectoryHandle('MyDocuments', {
create: true,
});
// 当text.txt在当前目录不存在则新建
const newFileHandle = await newDirectoryHandle.getFileHandle('text.txt', { create: true });
删除文件或目录
当已经获得了一个目录的句柄时,要想删除该目录或目录下的某个文件,可以调用removeEntry()方法。
// 删除文件
await directoryHandle.removeEntry('text.txt');
默认条件下,只能删除空目录,否则会报错,但是可以传入一个对象表示要递归删除目录下的所有子目录和文件。
// 递归删除目录下的所有文件和子目录
await directoryHandle.removeEntry('oldDir', { recursive: true });
还要注意的是,如果删除不存的目录或文件,返回的操作结果也是成功的。
解析目录下文件的路径
通过目录句柄的resolve()方法,我们能获得一个表示文件所处路径的数组,文件必须位于目录内,可以是目录的子目录。
const path = await directoryHandle.resolve(fileHandle);
// `path` is now ["desktop", "text.txt"]
浏览器支持
通过[Can I use](caniuse.com/?search=Fil… Access)网站看,除了谷歌、Edge、Opera外,其他浏览器一律不支持。

总结
对比现有的File API,File System Access API确实更加强大,但由于没有成为标准,甚至未来某一天被废弃或者更改,因此用于生产环境前需要谨慎考虑,但对于个人开发者来说,我们可以利用它实现一个网页端的文本编辑器,比如说基于PWN的低配版Typora,或者将它作为web应用安装到桌面。Typora基于Electron 和 node拥有操控系统文件的能力,现有的File API 不能打开一个目录,虽然<input type="file" webkitdirectory>可以实现,但它不是标准,File System Access API能帮我们实现这个功能。综上,使用File System Access做一个小项目也是个不错的主意。
推荐一个库browser-fs-access,它提供了对File Access System API的封装,当浏览器支持时会优先使用,当浏览器不支持会进行降级使用File API.
相关Demo:Browser-FS-Access.js Demo
参考
- File System Access (wicg.github.io)
- Streams API 概念 - Web API 接口参考 | MDN (mozilla.org)
- File - Web API 接口参考 | MDN (mozilla.org)
- WICG/file-system-access: Expose the file system on the user’s device, so Web apps can interoperate with the user’s native applications. (github.com)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!