前言
首先思考一下如下需求:
- 能不能在浏览器上开发一个文本编辑器,编辑本地文本文件?
- 一个 Web IDE ,能不能直接打开硬盘上的某一项目目录进行开发,而非编辑云端文件呢?
- 一个浏览器版的视频编辑器,能不能编辑电脑上内的视频、音频、图片,将合成后的影片直接存储在用户指定的目录呢?
- 实现一个浏览器版的文件管理器?
前端们但凡听到运营产品提出类似需求,会第一时间提出疑问。产品们在描述需求时,前端们就仿佛在等着这么一句话 “点击按钮把文件保存在 D 盘” 。没有这句话万事大吉,一听到这句话,前端们仿佛一直承受了莫大的委屈,而这委屈终于被人注意到了。哭诉 JS 只是个“脚本语言”,运行在浏览器沙箱里,哪能跟 “原生语言” 比 。
在之前这是不可能的。但是,时代变了。Chrome 68 版本新增了文件系统 API,让这些有了可能 。现在,在获得用户的许可后,JS 可以直接操作文件了。
好吧,本文并不会实现一个文件管理器,但是会向大家介绍实现它所需的必要知识。
文件系统 API 介绍
这一批 API 的兼容性如下,目前还是非标准状态,尝试之前请更新自己的 Edge、Chrome。
引用 Can I Use 上的一句话:
This feature is non-standard and should not be used without careful consideration.
那么开始吧。
我这里写了一个示例(codesandbox.io/s/g3lv1)。由于… API 不支持在 iframe 内调用,所以请单独打开页面浏览。
对文件(夹)的操作主要涉及三个对象主体:【文件夹操作句柄 FileSystemDirectoryHandle 】、【文件操作句柄 FileSystemFileHandle 】和【可写文件流对象 FileSystemWritableFileStream 】。
所以这里按照如何获得这几个对象,以及这些对象提供什么方法,拿到后能做些什么来分类介绍。
window.showDirectoryPicker() 得到文件夹句柄 FileSystemDirectoryHandle
作用:打开系统的文件夹选择器,确认选择后获得目标文件夹操作句柄,没有权限的会提示用户授权。
有如下代码:
const rootDirHandle = await window.showDirectoryPicker();
复制代码
执行后效果如图,弹出一个系统的文件夹选择器。
点击选择后会出现一个确认弹窗。
选择同意后浏览器地址栏右侧会多出一个图标,可以点击查看详情。可以看到此时网页有 github 目录的读取权限。
这一行代码执行完,可以获得一个文件夹操作句柄,即 rootDirHandle 变量 。
FileSystemDirectoryHandle.entries() 得到文件夹下一级目录的内容文件(夹)列表
有如下代码:
for await (const [key, value] of rootDirHandle.entries()) {
console.log(value)
}
复制代码
执行后即可看到目录下有哪些目录和文件,这只能看到目录下直接子级内容。可通过递归等方法获得所有内容。
key 和 value.name 是文件(夹)名,kind 则告诉我们是文件夹还是文件。
window.showOpenFilePicker({}) 得到文件句柄数组 [FileSystemFileHandle]
作用:打开系统的文件选择器,确认选择后获得目标文件操作句柄数组,没有权限的会提示用户授权。
有如下代码:
const fileHandle = await window.showOpenFilePicker();
// 多选
const fileHandle = await window.showOpenFilePicker({ multiple: true });
// 选择特定格式
const fileHandle = await window.showOpenFilePicker({
types: [
{
accept: {
// MIME 类型: [文件扩展名]
'image/png': [],
// or
'image/png': [ '.jpg', '.mp4' ],
// or
'image/*': ['.png', '.gif', '.jpeg', '.jpg']
}
},
],
// 是否允许用户把文件选择器的筛选类型切换到到所有类型(.*)
// 为 true 时 types 必须提供, [] 也行
excludeAcceptAllOption: true
});
复制代码
excludeAcceptAllOption: false 的情况
excludeAcceptAllOption: true 的情况
‘image/png’: [ ‘.jpg’, ‘.mp4’ ] accept 里只有这个将可以选择什么文件呢?
从实际表现可以看出是可以选 key 和 value 定义类型的并集。
这个格式限制只能一定程度限制,并不能杜绝。在上面文件全灰色不能选的情况下,使用 Tab 按钮切换,是可以切到灰色不能选的文件上的,按下 Enter 机选择成功。所以程序还是需要判断实际获得的文件类型是否符合期望。
这里的其他表现跟 window.showDirectoryPicker 一致,不再贴图。
window.showSaveFilePicker({}) 保存一个文件并得到文件句柄 FileSystemFileHandle
作用:打开系统的文件选择器,保存文件到指定位置,保存之前可以重命名。保存后获得此文件的操作句柄。
有如下代码:
const options = {
types: [{
accept: { 'text/plain': ['.txt'] },
}],
};
const fileHandle = await window.showSaveFilePicker(options);
复制代码
此处的 options 配置内容 和 window.showOpenFilePicker 里面的一致,不再重复。
执行后效果如下:
执行完地址栏点击小图标有如下提示:
可以看到,此方法的好处是不会出现需要用户授权的提示,因为用户只要保存了,页面即获得了此文件的修改权限。
FileSystemDirectoryHandle.getDirectoryHandle(x, {}) 得到文件夹句柄 FileSystemDirectoryHandle
作用:在现有目录下获取或创建新的文件夹,并得到其文件夹句柄。
有以下代码:
const testDirHandle = await rootDirHandle.getDirectoryHandle("testDir", {
create: true
});
复制代码
在此之前我们选择了 github 文件夹,即此处 rootDirHandle 是 github 文件夹的句柄。执行后如果 github 文件夹下有 testDir 文件夹,则 testDirHandle 将是 testDir 文件夹的句柄;如果 testDir 文件夹不存在,则会先创建 testDir 文件夹,然后 testDirHandle 则是新创建的 testDir 文件夹的句柄。
FileSystemDirectoryHandle.getFileHandle(x, {}) 得到文件句柄 FileSystemFileHandle
作用:在现有目录下获取或创建新的文件,并得到其文件句柄。
有如下代码:
// 获得一个文件句柄
let txtFileHandle = await rootDirHandle.getFileHandle("txt.txt", {
create: true
});
复制代码
获取 github 目录下 txt.txt 文件的句柄,由于 create: true ,所以如果文件不存在,则会先创建 txt.txt 文件。
总之:有了文件夹句柄就可以获得或创建新的文件夹或文件。
FileSystemFileHandle.getFile() 得到文件 File
有如下代码:
// 获取 txt 文件句柄
const txtFileHandle = await rootDirHandle.getFileHandle("txt.txt");
// 获取 txt 文件
const txtFile = await txtFileHandle.getFile();
console.log(await txtFile.text());
复制代码
首先获得 github 文件夹下的 txt.txt 文件句柄,然后获取到 txt 的 File 类型对象。File 是啥不是本文重点,不做介绍。
FileSystemFileHandle.createWritable() 得到一个可写文件流 FileSystemWritableFileStream
有以下代码:
// 获取 txt 文件句柄
const txtFileHandle = await rootDirHandle.getFileHandle("txt.txt");
// 创建一个 txt 文件的可写文件流
const txtFileWritable = await txtFileHandle.createWritable();
复制代码
createWritable() 执行时,如果原来没有文件夹/文件的修改权限,浏览器此时会提示授权。
通过后浏览器地址栏右侧小图标又会发生变化,点击查看有以下提示:
FileSystemWritableFileStream.write(x) 写入文件内容
在获得了文件的可写文件流以后,便可以更改文件内容了。其实写入什么内容都可以,取决于你。这里以文本和图片为示例。
示例1: 普通文本文件
有如下代码:
// 这里是文本文件,可以使用换行符换行,ps: 不同操作系统换行符可能不同
await txtFileWritable.write("haha\nhaha");
// 在内容末尾再追加内容
await txtFileWritable.write("?");
复制代码
此时我们便在 txt 文件写入了内容,但如果此时你打开 txt 文件,发现里面并没有刚刚写入的内容,不仅如此文件夹里还多出了一个记录着改动的临时文件。这是因为没有按 Ctrl + S 啊。在哪按往下看。
此时如果获取下 github 目录下的文件(夹)列表,如下:
可以看到 chrome 的这个临时文件是可以获取到的。所以对文件操作时请注意规避对此临时文件的操作,防止 chrome 和程序出现异常。
示例2: 图片文件
代码如下:
// 获取一个图片文件的 blob
const imgBlob = await fetch(
"https://unsplash.it/1600/900?random"
).then((res) => res.blob());
// 创建图片文件
const imgFileName = "img." + imgBlob.type.split("/")[1];
imgFileHandle = await rootDirHandle.getFileHandle(imgFileName, {
create: true
});
// 创建一个可写文件流
const imgFileWritable = await imgFileHandle.createWritable();
// 写入图片 blob
await imgFileWritable.write(imgBlob);
// 结束写入,保存图片
await imgFileWritable.close();
// 再次读取一下看看,也可以打开该文件夹自行查看
imgFileHandle = await rootDirHandle.getFileHandle(imgFileName);
// 获取文件
const imgFile = await imgFileHandle.getFile();
// 创建 blob url
const url = URL.createObjectURL(imgFile);
const img = document.createElement("img");
img.src = url;
// 移除 blob url 映射,回收内存
img.onload = () => URL.revokeObjectURL(url);
document.body.appendChild(img);
复制代码
FileSystemWritableFileStream.close() 保存可写文件流对文件做的修改
有如下代码:
// 接上一步代码
// 保存修改到文件
await txtFileWritable.close();
复制代码
close() 方法类似于常用的 Ctrl + S 快捷键。以上执行后,可以看到那个临时文件不见了,我们的 txt 文件也能看到刚刚写入的内容了。同时,刚刚写入的图片文件也看到了。
FileSystemDirectoryHandle.removeEntry(x, {}) 删除文件(夹)
只能创建可不行。
这是我现在 testDir 目录的文件结构。
本例是删除你所选文件夹下的 testDir 文件夹。ps: 学习资料被删了可不要找我 ?。
有如下代码:
// recursive 为递归删除内部文件(夹)
await rootDirHandle.removeEntry(testDir, { recursive: true });
复制代码
recursive: true 代表递归删除 testDir 目录下所有内容。难道还有只删除文件夹,缺保留该文件夹下内容的操作?
那我先执行下以 recursive: false 执行下。不出所料,除了一个报错,什么都没有改变。
不过稍微思考下,这种行为是有用的。比如只想删除空文件夹,有内容就跳过的情况。
那按 recursive: true 的情况执行代码。文件夹及以内文件(夹)全部被删除:
可能有用的资源:
This module allows you to easily use the File System Access API on supporting browsers, with a transparent fallback to the and legacy methods. This library is a ponyfill.
虽然做了两种方式的封装,由于文件系统 API 提供的功能更多,所以用时还是要区分。不过这是有用的,因为可以帮助我们将更少的代码放在 if else 里面。
Google Chrome Labs 开发人员写的一个文本编辑器示例。
一个手绘风格的画图应用。使用文件系统 API 提升了用户体验。
在浏览器上跑 node,Web IDE 却有软件 IDE 般的编码体验。
最后
好吧,本文并没有实现一个文件管理器,但是大家现在应该自己可以做出来一个了。
参考文献
- MDN Docs. developer.mozilla.org/en-US/docs/…
- The File System Access API: simplifying access to local files. web.dev/file-system…
- Introducing WebContainers: Run Node.js natively in your browser. blog.stackblitz.com/posts/intro…
- Can I Use. caniuse.com/?search=sho…
- Browser-FS-Access lib. github.com/GoogleChrom…
文|无忌
关注得物技术,携手走向技术的云端