在前端的世界里,除了通过input[file]上传文件外,我们是无法处理文件内容的,处理文件的逻辑都需要依赖于后端完成,现在html5提供了showOpenFilePicker(), showDirectoryPicker(), showSaveFilePicker()等API可以轻松地让我们在浏览器世界里来管理本地文件。所以现在我们来学习一下这个强大的功能。
API介绍
注意:我们访问一个文件或者目录的读写操作所依赖的文件访问权限在刷新或关闭页面并且页面所属的源没有其他标签页保持打开的情况下不会继续保有。
showDirectoryPicker(options)
用于显示一个允许用户选择一个目录的目录选择器。
optionsid可选,通过指定 ID,浏览器可以记住不同 ID 所对应的目录。如果在另一个选择器中使用了相同的 ID,则选择器将在同一目录中打开。mode可选,默认为"read",用于只读访问,或"readwrite",用于读写访问。startIn可选,一个FileSystemHandle对象或者代表某个众所周知的目录的字符串(如:"desktop"、"documents"、"downloads"、"music"、"pictures"、"videos"),用于指定选择器的起始目录。
如果选中的文件夹包含系统文件,将无法打开,并提示选择其他文件夹。
成功选择后,将返回FileSystemDirectoryHandle对象,如果取消(关闭系统弹框或者点击取消)时,将返回一个失败的Promise。
showOpenFilePicker(options)
用于显示一个允许用户选择一个或多个文件的文件选择器,并返回这些文件的句柄。即使单选也是返回数组。
optionsexcludeAcceptAllOption可选,默认为false。默认情况下,选择器应包含一个不应用任何文件类型过滤器的选项(通过下面的类型选项启动)。将此选项设置为true意味着该选项不可用。id可选,通过指定 ID,浏览器可以记住不同 ID 所对应的目录。如果在另一个选择器中使用了相同的 ID,则选择器将在同一目录中打开。(他会记住上次对应id选择的目录位置,在下次使用相同id打开的弹框将定位到对应的目录)multiple可选,默认为false。当设置为true时,可以选择多个文件。startIn可选, 一个FileSystemHandle对象或一个众所周知的目录("desktop"、"documents"、"downloads"、"music"、"pictures"或"videos")以指定打开选择器的起始目录。types可选,允许选择的文件类型的数组。每个项目都是一个具有以下选项的对象:description可选,允许的文件类型类别的可选描述。默认为空字符串。accept一个Object,其键设置为 MIME 类型,值设置为文件扩展名的数组。
const pickerOpts = {
types: [
{
description: "Images",
accept: {
"image/*": [".png", ".gif", ".jpeg", ".jpg"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
};
showSaveFilePicker(options)
用于显示允许用户保存一个文件的文件选择器。用户可以选择一个已有文件覆盖保存,也可以输入名字新建一个文件。
optionsexcludeAcceptAllOption可选,默认为false。默认情况下,选择器应包含一个不应用任何文件类型过滤器的选项(通过下面的类型选项启动)。将此选项设置为true意味着该选项不可用。id可选,通过指定 ID,浏览器可以记住不同 ID 所对应的目录。如果在另一个选择器中使用了相同的 ID,则选择器将在同一目录中打开。startIn可选,一个FileSystemHandle对象或一个众所周知的目录("desktop"、"documents"、"downloads"、"music"、"pictures"或"videos")以指定打开选择器的起始目录。suggestedName可选,一个字符串。建议的文件名。types可选,允许选择的文件类型的数组。每个项目都是一个具有以下选项的对象:description可选,允许的文件类型类别的可选描述。默认为空字符串。accept,一个Object,其键设置为 MIME 类型,值设置为文件扩展名的数组。
async function getNewFileHandle() {
const opts = {
suggestedName: "自定义命名.txt",
types: [
{
description: "Text file",
accept: { "text/plain": [".txt"] },
},
],
};
return await window.showSaveFilePicker(opts);
}
FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle
不管是选择文件还是文件夹, 确认选择后,返回的FileSystemDirectoryHandle,FileSystemFileHandle对象都继承自FileSystemHandle接口,所以我们先来了解下。
FileSystemHandle
代表一个文件或一个目录的对象。
kind条目的类型。如果关联的条目是一个文件,则此值为'file',否则为'directory'。name关联的条目(文件或者文件夹)的名称。
isSameEntry(fileSystemHandle)
用于比对两个句柄以查看两者关联的条目(文件或目录)是否相符。
queryPermission(descriptor)
用于查询当前句柄目前的权限状态。返回值为'granted'、'denied' 或 'prompt'。如果返回prompt,则网站必须先调用 requestPermission(),然后才能对句柄执行任何操作。
descriptor.mode值为read'或'readwrite', 指定需要查询的权限模式
remove(options)
允许你用对应的句柄直接移除一个文件或一个目录。
options.recursive默认为false。当设为true并且条目是一个目录时,目录的内容将会被递归移除。如果目录中有内容那么recursive必须设置为true否则不能删除。
requestPermission(descriptor)
用于为文件句柄请求读取或读写权限。返回值为'granted'、'denied' 或 'prompt'。如果返回prompt,则网站必须先调用 requestPermission(),然后才能对句柄执行任何操作。
descriptor.mode值为read'或'readwrite', 指定需要查询的权限模式
对于requestPermission来说,我如果开始showOpenFilePicker, showDirectoryPickermode未指定readwrite那么我们将可以调用queryPermission来让用户授权。
const dir = await window.showDirectoryPicker({
id: "dir",
mode: "read",
})
dir.requestPermission({
mode: "readwrite"
}).then(async res => {
console.log(res)
console.log(dir, await dir.queryPermission())
console.log("======")
})
FileSystemDirectoryHandle
提供一个指向目录条目的句柄。该对象主要提供一些操作目录的方法,比如删除,创建(文件或目录),遍历等等。
entries()
一个异步迭代器,返回当前选中文件夹下所有直接子内容的键值对。键值对是一个 [key, value] 形式的数组。有了这个就可以递归获取当前文件夹下所有文件及文件夹的句柄对象了。
for await (const entry of dir.entries()) {
console.log(entry)
}
values()
一个异步迭代器,返回当前选中文件夹下所有直接子内容的值(句柄对象)。
for await (const value of dir.values()) {
console.log(value)
}
keys()
一个异步迭代器,返回当前选中文件夹下所有直接子内容的文件名。
for await (const key of dir.keys()) {
console.log(key)
}
removeEntry(name, options)
用于尝试将目录句柄内指定名称的文件或目录移除。
name文件名options.recursive默认为false。当设为true时,条目将会被递归移除。
resolve(possibleDescendant)
一个包含从父目录前往指定子条目中间的目录的名称的数组。数组的最后一项是子条目的名称。
- 要返回其相对路径的
FileSystemHandle对象。
下面这个方法就是一个文件夹中查找一个文件,返回当前文件相对于文件夹的路径。
async function returnPathDirectories(directoryHandle) {
// 通过显示文件选择器来获得一个文件句柄
const [handle] = await self.showOpenFilePicker();
if (!handle) {
// 如果用户取消了选择或者打开文件失败
return;
}
// 检查文件句柄是否存在于目录句柄的目录中
const relativePaths = await directoryHandle.resolve(handle);
return relativePaths
}
getDirectoryHandle(name, options)
返回一个位于调用此方法的目录句柄内带有指定名称的子目录的FileSystemDirectoryHandle。
name子目录名称options.create默认为false。当设为true时,如果没有找到对应的目录,将会创建一个指定名称的目录并将其返回。 此时获取句柄对象的mode必须设置为readwrite。 如果初始化未设置,我们需要通过requestPermission请求对应的权限。
const dir = await window.showDirectoryPicker({
id: "dir",
mode: "read",
})
const res2 = dir.getDirectoryHandle("zh", {
create: true
})
console.log("res2", res2)
getFileHandle()
返回一个位于调用此方法的目录句柄内带有指定名称的文件的 FileSystemFileHandle。
name子文件名称options.create默认为false。当设为true时,如果没有找到对应的文件,将会创建一个指定名称的文件并将其返回。 此时获取句柄对象的mode必须设置为readwrite。 如果初始化未设置,我们需要通过requestPermission请求对应的权限。
FileSystemFileHandle
提供一个指向文件条目的句柄。该对象主要提供一些操作文件的方法,比如写入,获取等等。
move(newName?, options)
允许你移动或重命名用户本地文件系统中的文件。
newName新文件名optionsto: 目标目录的FileSystemDirectoryHandle(用于跨目录移动)overwrite: 默认false,是否覆盖同名文件
createWritable(options)
用于创建一个FileSystemWritableFileStream 对象,可用于写入文件。
任何通过写入流造成的更改在写入流被关闭前都不会反映到文件句柄所代表的文件上。这通常是将数据写入到一个临时文件来实现的,然后只有在写入文件流被关闭后才会用临时文件替换掉文件句柄所代表的文件。
async function writeFile(fileHandle, contents) {
// 创建一个 FileSystemWritableFileStream 用来写入。
const writable = await fileHandle.createWritable();
// 将文件内容写入到流中。
await writable.write(contents);
// 关闭文件并将内容写入磁盘。
await writable.close();
}
keepExistingData默认为false。当设为true时,如果文件存在,则先将现有文件的内容复制到临时文件,否则临时文件初始时内容为空。mode可选, 指定可写文件流的锁定模式的字符串。默认值为"siloed"。"exclusive"只能打开一个FileSystemWritableFileStream写入器。在第一个写入器关闭之前尝试打开后续写入器会导致抛出NoModificationAllowedError异常。"siloed"可以同时打开多个FileSystemWritableFileStream写入器,每个写入器都有自己的交换文件,例如在多个标签页中使用同一个文件时。最后打开的写入器会写入其数据,因为每个写入器关闭时都会刷新数据。
getFile()
返回一个File对象,其表示磁盘上句柄所代表的文件。如果磁盘上的文件在调用了此方法后发生了更改或是被移除,那么返回的File 对象可能会不再可读。
const [file] = await window.showOpenFilePicker({
id: "file"
})
console.log(file)
console.log(await file.getFile())
FileSystemWritableFileStream
获取文件句柄,主要就是为了写入文件的。所以我们再来看看FileSystemWritableFileStream 对象。
locked表示可写流是否已锁定。如果当前可写流调用了getWriter()那么他将被锁定,不能进行任何操作。mode可写流的锁定模式的字符串。("siloed","exclusive")
abort(reason)
用于中止流,表示生产者不能再向流写入数据(会立刻返回一个错误状态),并丢弃所有已入队的数据。一般用于不会流错误时终止。
reason一个字符串,用于提供人类可读的中止原因。
writer.abort("终止写入")
close()
关闭可写流。
getWriter()
返回一个新的 WritableStreamDefaultWriter 实例并且将流锁定到该实例。当流被锁定时,直到这个流被释放之前,不能操作其他 writer。
seek(position)
用于更新文件当前指针的偏移到指定的位置(以字节为单位)。主要改变写入内容插入的位置。
position一个数字,表示从文件开头起的字节位置。 这里我们就可以结合getFile()获取文件大小,然后设置内容插入位置。来实现追加文件内容的效果。但是需要指定createWritable({ keepExistingData: true })才会保留以前的文件内容。
const writableStream = await newHandle.createWritable({
keepExistingData: true
});
const file = await newHandle.getFile()
console.log("file", file.size)
// 默认情况,他只是在这个位置写入,并不会拷贝以前的内容。设置keepExistingData: true后就是追加内容
await writableStream.seek(file.size)
await writableStream.write("追加的内容")
truncate(size)
用于将与流相关联的文件调整为指定字节大小(删除文件内容到对应的字节)。如果指定的大小大于文件当前的大小,文件会被用 0x00(即空格) 字节补充。调用 truncate() 方法同时也会更新文件的指针。和seek()一样
size一个数字,表示要将流调整到的字节数。
write(data/options)
用于在调用此方法的文件上的当前指针偏移处写入内容。传入一个options对象可以是truncate, seek, write的结合体。
data用于写入的文件数据,可以是ArrayBuffer、TypedArray、DataView、Blob或 字符串。optionstype一个字符串,值为"write"、"seek"或"truncate"之一。data用于写入的文件数据,可以是ArrayBuffer、TypedArray、DataView、Blob或 字符串。这个属性在type被设为"write"时是必需的。position当type为"seek"时,表示文件当前指针应该移动到的位置。当type被设为"write"时也可以使用,这种情况下将会在指定的位置开始写入。size一个数字,表示流应当包含的字节数。这个属性在type被设为"truncate"时是必需的。
WritableStreamDefaultWriter
既然可以调整流的控制权,那我们就来了解下WritableStreamDefaultWriter独有的API
closed当前流是否被关闭或者释放锁定(调用releaseLock())desiredSize返回填充满流的内部队列需要的大小。如果无法成功写入流(由于流发生错误或者中止入队),则该值为null,如果流关闭,则该值为 0。ready当流填充内部队列的所需大小从非正数变为正数时兑现,表明它不再应用背压。
desiredSize, ready 可用背压控制。
async function writeWithBackpressure(dataChunks) {
const writer = writableStream.getWriter();
for (const chunk of dataChunks) {
// 检查流的剩余容量
if (writer.desiredSize <= 0) {
console.log('背压:等待流准备就绪');
await writer.ready; // 等待流有空间
}
await writer.write(chunk);
console.log('已写入:', chunk);
}
await writer.close();
}
// 模拟数据块数组
const chunks = ['数据A', '数据B', '数据C'];
writeWithBackpressure(chunks);
releaseLock()
用于释放 writer 对相应流的锁定。释放锁后,writer 将不再处于锁定状态。如果释放锁时关联的流出错,writer 随后也会以同样的方式发生错误;此外,writer 将会关闭。
const [newHandle] = await showOpenFilePicker()
const writableStream = await newHandle.createWritable({
keepExistingData: true
});
const writer = writableStream.getWriter()
writer.write("0000000")
writer.write("111111111111")
writer.releaseLock()
writableStream.write("222222222222")
writableStream.close()
abort(), write(), close()
该方法使用方式同上。
使用WritableStreamDefaultWriter对象操作写入文件的原因
锁机制保证写入安全
如果直接操作流,那么多个页面都可以同时访问一个文件进行操作,导致数据写入混乱。getWriter() 会为流加锁,确保同一时间只有一个写入器活跃。
背压(Backpressure)管理
如果数据生产速度远大于消费速度(如写入大文件),可能导致内存溢出。通过 writer.desiredSize 和 writer.ready 动态控制写入节奏。
如何在开发中使用?
了解了上面的相关API后,我们就来看一下在开发中可以有哪些具体的使用。
分块写入录屏流
遍历当前选中的目录(及子目录)
async function readDir () {
const dirHandle = await window.showDirectoryPicker();
return await recursiveReadDir(dirHandle)
async function recursiveReadDir(dirHandle) {
const entries = [];
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file') {
entries.push({
name: entry.name,
kind: entry.kind,
});
} else if (entry.kind === 'directory') {
entries.push({
name: entry.name,
kind: entry.kind,
children: await recursiveReadDir(entry)
});
}
}
return entries
}
}
递归删除指定目录
async function recursiveRemvoeDir() {
const dir = await window.showDirectoryPicker()
dir.remove({
recursive: true
})
}
批量处理文件
例如批量修改文件名
async function batchRenameImages() {
const dirHandle = await window.showDirectoryPicker();
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && entry.name.endsWith('.jpg')) {
// 修改文件名:image.jpg -> image@zh.jpg
const newName = entry.name.replace('.jpg', '@zh.jpg');
await entry.move(newName);
}
}
}
追加文件内容
默认情况下写入文件都是直接覆盖写入。
async function appendFile(data) {
const [newHandle] = await showOpenFilePicker()
const writableStream = await newHandle.createWritable({
keepExistingData: true // 源文件内容拷贝到临时文件中
});
const {size} = await newHandle.getFile()
await writableStream.seek(size)
await writableStream.write(data)
await writableStream.close()
}
分片写入大文件
async function saveLargeFile(url) {
// 获取文件句柄
const fileHandle = await window.showSaveFilePicker();
const writableStream = await fileHandle.createWritable();
const writer = writableStream.getWriter();
// 分块下载并写入
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
await writer.write(value);
}
await writer.close();
console.log('文件保存完成');
}
saveLargeFile("https://p26-passport.byteacctimg.com/img/user-avatar/b515ec49c88a2c9e23fb5727652cb8f9~90x90.awebp")
往期年度总结
- 在上海的忙碌一年,依旧充满憧憬(2024)
- 四年沿海城市,刚毕业,一年3家公司
- 七月仿佛又回到了那一年(2023年中总结)
- 一位初入职场前端仔的年度终结 <回顾2022,展望2023>
- 大学两年半的前端学习
往期文章
- 前端可以知道的录制浏览器标签页,没有黑魔法
- 一个关联本地页面镜像的功能,我了解到这些
- 啊,原来sessionStorage是这样的
- 如何像掘金编辑器一样粘贴图片即可上传服务器
- Nest装饰器全解析
- Nest世界中的AOP
- Nestjs如何解析http传输的数据
- 如何理解js的DOM事件系统
- 半年没看vue官网,3.5刚刚发布,趁机整理下
- 啊,你还在找一款强大的表格组件吗?
- 前端大量数据层级展示及搜索定位预览
- 如何从0开始认识m3u8(提取,解析及下载)
- 展示大量数据节点(tree),引发的一次性能排查
- ts装饰器的那点东西
- 这是你所知道的ts类型断言和类型守卫吗?
- TypeScript官网内容解读
- 经常使用ts的你,知道这些内容?
- 你有了解过原生css的scope?
- 现在比较常用的移动端调试你知道哪些?
- 众多跨标签页通信方式,你知道哪些?(二)
- 众多跨标签页通信方式,你知道哪些?
- 反调试吗?如何监听devtools的打开与关闭
- 因为原生,选择一家公司(前端如何防笔试作弊)
- 结合开发,带你熟悉package.json与tsconfig.json配置
- 如何优雅的在项目中使用echarts
- 如何优雅的做项目国际化
- 近三个月的排错,原来的憧憬消失喽
- 带你从0开始了解vue3核心(运行时)
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )
专栏文章
🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论, 支持一下博主~
公众号:全栈追逐者,不定期的更新内容,关注不错过哦!