前言

前端文件系统api(The File System Access API)让web应用可以读写用户本地的文件或文件夹。使我们可以开发出能够和用户本地文件交互的web应用,比如IDE,图片和视频编辑器,文字编辑器等等。

在用户开启web应用权限后,这些api可以直接在用户本地读写文件或文件夹,打开一个目录并显示里面的内容,在用户本地创建或删除文件夹和文件。

打开

比如页面中有个打开按钮:

1
<div id="open">打开</div>

点击后,调用showOpenFilePicker就可以弹出文件选择窗,我们选择文件,再调用getFile即可获取文件的file数据,这个file数据和获取的file数据一样。

1
2
3
4
5
6
const openElm = document.getElementById('open')
let fileHandle
openElm.addEventListener('click', async () => {
[fileHandle] = await window.showOpenFilePicker()
const file = await fileHandle.getFile()
})

上面的例子中,我们选择了一个文件,showOpenFilePicker返回了FileSystemFileHandle类型的数组:

fileHandle

这里的fileHandle将会很有用,后面的保存等操作都需要它。

区分 file pickers

有时应用会有多个不同的picker,比如富文本编辑器可以打开文本,也可以打开图片,默认情况下,每个file picker会记住上次的路径,我们可以通过id来区分不同的file picker,让它们记住不同的最近一次打开的路径。

1
2
3
4
5
6
7
const fileHandle1 = await window.showSaveFilePicker({
id: 'openText',
})

const fileHandle2 = await window.showSaveFilePicker({
id: 'importImage',
})

不过这个功能,我在windows上Chrome版本 96.0.4664.45(正式版本) (64 位)上试验失败了,file picker没有记住上次打开的路径。

保存

保存会重写原文件。

页面中,我们放一个打开和保存按钮,还有一个文本框:

1
2
3
<div id="open">打开</div>
<div id="save">保存</div>
<textarea id="textArea"></textarea>

点击打开按钮,我们选择文件,比如test.txt,并把文本内容显示到textArea:

1
2
3
4
5
6
openElm.addEventListener('click', async () => {
[fileHandle] = await window.showOpenFilePicker()
const file = await fileHandle.getFile()
const contents = await file.text()
textArea.value = contents
})

点击保存时调用writeFile:

1
2
3
saveElm.addEventListener('click', () => {
writeFile(fileHandle, textArea.value)
})

writeFile函数中创建可写数据流,把textArea的内容写进去:

1
2
3
4
5
6
7
8
async function writeFile(fileHandle, contents) {
// 创建一个FileSystemWritableFileStream用来写数据
const writable = await fileHandle.createWritable()
// 把file的数据写到流中
await writable.write(contents)
// 关闭文件并将内容写入磁盘
await writable.close()
}

写数据用到FileSystemWritableFileStream对象,它本质上是一个可写的流,调用fileHandle的createWritable就可以创建,调用createWritable时,浏览器会先检查是否有写的权限,没有的话浏览器就会弹对话框,让用户选择是否开启写权限:

permission_save

用户拒绝时createWritable会抛出DOMException的错误:

DOMException

这样,应用就不会保存更改。

上面的writeFile方法在写数据时用的contents是字符串,我们也可以用其它格式的数据,比如BufferSource,或者Blob:

1
2
3
4
5
6
async function writeURLToFile(fileHandle, url) {
const writable = await fileHandle.createWritable()
const response = await fetch(url)
// 让响应的数据流入文件中,pipeTo默认会关闭管道,不需要手动关闭
await response.body.pipeTo(writable)
}

我们还可以在打开时手动申请写的权限,用户打开文件时看到一个对话框,然后我们对打开的文件就有了读写权限,在保存时就不会再弹对话框。

通过下面的verifyPermission函数来判断fileHandle是否有读写权限,结果是true则开启了,false则是用户拒绝了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function verifyPermission(fileHandle, readWrite) {
const options = {}
if (readWrite) {
options.mode = 'readwrite'
}
// 是否开启权限,是就返回true
if ((await fileHandle.queryPermission(options)) === 'granted') {
return true
}
// 请求开启权限,是就返回true
if ((await fileHandle.requestPermission(options)) === 'granted') {
return true
}
// 用户拒绝开启权限,返回false
return false
}

另存

另存会创建一个新的文件。
调用showSaveFilePicker会弹出保存弹窗:

1
await window.showSaveFilePicker()

保存类型

types参数控制保存类型:

1
2
3
4
5
6
7
8
9
10
await window.showSaveFilePicker({
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt'],
},
},
],
})

save_as_text

默认目录

同样的,我们也可以设置启动目录。如果你是编辑文本,会希望打开或保存时的文件夹路径是文档,如果是编辑图片,则默认图片文件夹,这个路径可以通过配置startIn来实现:

1
startIn: 'pictures'

还有其他目录可以配置:

  • desktop: 桌面
  • documents: 文档
  • downloads: 下载
  • music: 音乐
  • pictures: 图片
  • videos: 视频
    windows中对应文件夹的这些目录:

startIn

除了上面这些通用的目录,你还可以设置为存在的文件或目录地址:

1
2
3
4
5
6
7
8
9
10
11
const openElm = document.getElementById('open')
const saveAsElm = document.getElementById('saveAs')
let fileHandle
openElm.addEventListener('click', async () => {
[fileHandle] = await window.showOpenFilePicker()
})
saveAsElm.addEventListener('click', async () => {
window.showSaveFilePicker({
startIn: fileHandle
})
})

上面的例子中,我们showOpenFilePicker打开了文件,在showSaveFilePicker保存时startIn传打开的fileHandle,即可将保存弹窗的路径设置为和打开时选择的文件路径一致。

文件夹

打开文件夹

showDirectoryPicker可以打开文件夹并获取其中的内容:

1
2
3
4
5
6
7
openElm.addEventListener('click', async () => {
const dirHandle = await window.showDirectoryPicker()

for await (const entry of dirHandle.values()) {
console.log(entry)
}
})

如果没有权限,浏览器会弹对话框:

permission_openDir

创建文件和文件夹

在文件夹中,你可以用getFileHandle读取文件,用getDirectoryHandle读取文件夹,在可选参数中传create来控制当新文件和文件夹不存在时是否需要创建。

1
2
3
4
5
6
7
// 打开文件夹
const dirHandle = await window.showDirectoryPicker()
// 在打开的文件夹中新建名为 "My Documents"的文件夹
const newDirectoryHandle = await dirHandle.getDirectoryHandle('My Documents', { create: true })
// 在"My Documents"文件夹中新建名为 "My Notes.txt" 的文件
const newFileHandle = await newDirectoryHandle.getFileHandle('My Notes.txt', { create: true })

解析路径

上面的例子在打开的文件夹中创建文件夹并在新文件夹中新建文件,我们可以解析新建文件的路径:

1
2
const path = await dirHandle.resolve(newFileHandle)
// path 的值是 ["My Documents", "My Notes.txt"]

删除

删除上面新建的My Notes.txt文件:

1
await newDirectoryHandle.removeEntry('My Notes.txt')

删除上面的My Documents文件夹:

1
await dirHandle.removeEntry('My Documents', { recursive: true })

拖拽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
window.addEventListener('dragover', async (e) => {
e.preventDefault()
})
window.addEventListener('drop', async (e) => {
e.preventDefault()
// 遍历多有的item
for (const item of e.dataTransfer.items) {
// 注意:文件和文件夹的kind都是file
if (item.kind === 'file') {
const entry = await item.getAsFileSystemHandle()
if (entry.kind === 'directory') {
// 处理文件夹
handleDirectoryEntry(entry)
} else {
// 处理文件
handleFileEntry(entry)
}
}
}
})

补丁

还不能给File System Access API打完整的补丁。

  • showOpenFilePicker可以用<input type="file">代替
  • showSaveFilePicker可以用<a download="file_name">代替,尽管这能触发下载,但不能覆盖现有文件
  • showDirectoryPicker可以用<input type="file" webkitdirectory>替代
    browser-fs-access封装了前端文件系统api,会尽量使用File System Access API,不支持的浏览器使用其他方案。

安全

Chrome团队设计并实现了文件系统访问API,使用了控制访问强大Web平台功能的核心原则。

打开或另存文件

打开文件时,用户通过 file picker 提供读取文件的权限,用来打开的file picker只能用户手动点开,如果用户改变主意了,可以点取消,然后应用不会得到任何用户数据,表现和一样。

同样的,当应用要另存时,浏览器会弹出保存窗口,让用户指定文件名和位置。

文件夹限制

为了保护用户及其数据,浏览器不允许用户保存到一些文件夹,比如核心操作系统文件夹。如果要保存到这些位置,浏览器会弹窗让用户选择其他路径。

保存

保存时会覆盖源文件,web应用必须得到用户明确同意后才能保存。

如果用户要保存对开启了读取权限文件的更改,浏览器就会弹对话框,问用户是否要保存。

另外,可以编辑多个文件的web应用程序(比如IDE)也可以在打开时请求保存更改的权限。

如果在对话框中用户点击取消,不给写的权限,那么应用就不能保存更改。这时就需要提供保存的替代方案,让用户可以保存数据,比如提供下载途径,或者保存到云端等等。

透明

用户开启应用保存权限后,浏览器会在地址栏显示一个图标,点击图标可以显示开启权限了的文件列表,也可以很方便的取消保存权限。

有效期

同一个域名下所有的页面都关闭后,保存权限就没了,用户下一次访问时,会再次弹对话框来询问是否开启权限。

参考