谁也别拦我们,网页里直接增删改查本地文件!

欢迎来到 Jax 的专栏「Web 玩转文件操作」,快来了解 Web 端关于文件操作的方方面面!

转载请联系作者 Jax。

先来玩玩这个 Demo ------ 一个网页端的本地文件管理器

在上面的 Demo 中,点击「打开文件夹」按钮后,你可以选择电脑本地的某个文件夹来打开。然后就能对其中的文件做各种操作了,包括新建、右键打开、编辑、删除等等。你会发现,在网页上的操作会立刻在本地文件上实际生效。

如果你感觉是:"哟?有点儿意思!",那么这篇文章就是专门为你而写的,读下去吧。

正所谓「边牧是边牧、狗是狗」,Web 应用早已不是当初的简陋网页了,它进化出了令人眼花缭乱的新能力,同时也在迅速追齐系统原生能力。如 Demo 所示,我们甚至可以把文件管理系统搬进网页,并且简单的 JavaScript 代码就能实现。Web 应用能有这样的威力,得益于浏览器对文件系统 API 的实现。

文件系统(File System Access)API 目前还处于社区草稿状态,但早在 2016 年,WHATWG 社区就已经开始着手设计这一标准,并且八年来一直在持续不断地打磨,迄今已经被各大主流浏览器所实现。

这套 API 足够简单易用,你很快就能掌握它,并且让它成为你的得力助手。但由于涉及到文件管理这个课题,它有很多方面都可以挖掘得很深很深,比如读写流、OPFS、安全策略等等。咱们这个专栏本着少吃多餐的原则,不会一次性囊括那么多内容。咱们的这第一铲,就先浅浅地挖一个「憨豆」 ------ FileSystemHandle

FileSystemHandle

在文件系统中,文件和文件夹是一个个实体,是所有状态和交互的核心。相对应地,在文件系统 API 中,我们用「FileSystemHandle」这个对象来对实体进行抽象。为了能够简洁地描述,我们在下文中将称之为「憨豆」。我们用对象属性来保存实体的名称、类型,通过执行对象方法来操作实体。

那么 FileSystemHandle 从何而来呢?一般是从用户操作而来。以文件为例,当用户通过文件窗口选择了某个本地文件之后,我们就能从代码层面获取到通向这个文件的入口,从而可以对文件进行管理操作。除此之外,拖拽文件进入也能得到憨豆

属性:name 和 kind

name:无论是文件还是文件夹,必然都有一个名字。

kind :实体的类型,值为 'file' 代表文件;值为 'directory' 代表文件夹。

校验方法 isSameEntry()

用于判断两个憨豆是否代表相同的实体。例如用户上传图片时,先后两次选择的是同一张图片,那么两个憨豆指向的其实是同一个文件。

JavaScript 复制代码
const [handle1] = await showOpenFilePicker() // 第一次上传
const [handle2] = await showOpenFilePicker() // 第二次选择了同一个文件

const isSame = await handle1.isSameEntry(handle2)
console.log(isSame) // true

该方法也同样适用于文件夹校验。

我们可以借此来检测重复性。

删除方法 remove()

用于删除实体。比如,执行下列代码会现场抽取一个幸运文件来删除:

JavaScript 复制代码
const [handle] = await showOpenFilePicker()
handle.remove()

但如果我们想要选中文件夹来删除,像上面那样直接调用是会报错并删除失败的。我们需要额外传入配置参数:

JavaScript 复制代码
handle.remove({ recursive: true })

传参后的删除,是对文件夹里面的嵌套结构进行递归删除,这个设计理念旨在让开发者和用户在删除前知悉操作后果。

权限方法 queryPermission() 和 requestPermission()

用于向用户查询和获取读写权限,可以传入一个参数对象来细分权限类型。

JavaScript 复制代码
const permission = await handle.queryPermission({ mode: 'read' }) // mode 的值还可以是 readwrite
console.log(permission)// 若值为 'granted',则代表有足够权限

我们在对实体进行操作前,最好总是先查询权限。因此一个最佳实践是把这两个方法封装进通用操作逻辑中。

其他特性

除此之外,FileSystemHandle 还具有一些其他特性,比如可以转化为 indexDB 实例、可以通过 postMessage 传输到 Web Workers 里进行操作等等。我们会在后续的专栏文章中继续顺藤摸瓜地了解它们。

两个子类

到目前为止,FileSystemHandle 所具有的属性和方法都在上面了。你可能也意识到了,单靠这三招两式是不可能实现像 Demo 里那么丰富完备的文件操作的。

没错,这个憨豆只是一个抽象父类,它还有两个子类 FileSystemFileHandleFileSystemDirectoryHandle,这两位才是文件和文件夹实体的真正代言人。对实体的常规操作,几乎都是通过这两个紫憨豆来执行的。

除了从父类继承而来的基因,两个紫憨豆都各自具有独特的属性和方法,满足了开发者对文件和文件夹两种不同实体的不同操作需求。

FileSystemFileHandle

在本专栏的上一篇文章《绕过中间商,不用 input 标签也能搞定文件选择》中,我们曾与文件憨豆有过一面之缘。那时候,我们通过 showOpenFilePicker 获取了文件憨豆,并调用它的 getFile 方法拿到了 文件 Blob

此外,文件憨豆还具有的方法如下:

  • createSyncAccessHandle():用于同步读写文件,但是仅限于在 Web Workers 中。
  • createWritable:创建一个写入流对象,用于向文件写入数据。

FileSystemDirectoryHandle

文件夹憨豆的特有方法如下:

  • getDirectoryHandle():按名称查找子文件夹。
  • getFileHandle():按名称查找子文件。
  • removeEntry():按名称移除子实体。
  • resovle():返回指向子实体的路径。

经过上述罗列,我们对两种憨豆的能力有了一个大概的印象。下面,我们将回到 Demo 的交互场景,从具体操作上手,看看文件系统 API 是如何在 JavaScript 中落地的。

操作 & 用法

载入文件夹

我们首先需要载入一个文件夹,然后才能对其中的子实体进行操作。

如果你碰巧读过上一篇文章,知道如何用 showOpenFilePicker() 选择文件,那你一定能举一反三推断出,选择文件夹的方法是 showDirectoryPicker()

JavaScript 复制代码
const dirHandle = await showDirectoryPicker()

showDirectoryPicker 方法也接收一些参数,其中 idstartIn 这两个参数与 showOpenFilePicker 方法 的同名参数完全对应。另外还支持一个参数 mode ,其值可以是 readreadwrite,用于指定所需的权限。

用户选择文件夹后得到的 dirHandle,就是一个 FileSystemDirectoryHandle 格式的对象。我们可以遍历出它的子实体:

JavaScript 复制代码
for await (const sub of dirHandle.values()) {
  const { name, kind } = sub
  console.log(name, kind)
}

从子实体中取出名称和类别属性值,就可以对文件夹内容一目了然了。

读取文件内容

在上一步中,我们已经读取到了子实体的名字和类型,那么对于文件类型,我们可以先按名称检索到对应的憨豆:

JavaScript 复制代码
// if sub.kind === 'file'
const fileHandle = await dirHandle.getFileHandle(sub.name)

再从文件憨豆中掏出文件 Blob,进一步读取到文件的内容:

JavaScript 复制代码
const file = await fileHandle.getFile()
const content = file.text()

如果你用来调试的文件是文本内容的文件,那么打印 content 的值,你就可以看到内容文本了。

同理,获取子文件夹的方式是 dirHandle.getDirectoryHandle(sub.name)

新建文件、文件夹

除了指定名称参数,getFileHandlegetDirectoryHandle 这两个方法还支持第二个参数,是一个一个配置对象 { create: true/false },用于应对指定名称的实体不存在的情况。

例如,我们对一个文件夹实体执行 dirHandle.getFileHandle('fileA'),但该文件夹中并没有名为 fileA 的文件。此时第二个参数为空,等同于 create 的默认值为 false,那么此时会抛出一个 NotFoundError 错误,提示我们文件不存在。

而如果我们这样执行:dirHandle.getFileHandle('fileA', { create: true }),那么就会在当前文件夹中新建一个名为 fileA 的空文件。

同理,我们也可以用 dirHandle.getDirectoryHandle('dirA', { create: true }) 新建一个名为 dirA 的空文件夹。

在 Demo 中,我们实现了让用户在新建文件时自定义文件名,这其实是使用了 prompt 方法:

JavaScript 复制代码
const fileName = prompt('请输入文件名')
await dirHandle.getFileHandle(fileName, { create: true })

在当下这个 AI 时代,Prompt 更多是以「LLM 提示词」被大家初识。在更早时候,它是浏览器实现的一个组件。其实这两种形态的含义是一致的,都是在人机交互中,从一端到另一端的输入输出流程。

编辑文件内容

刚刚我们读取了文件的内容,现在我们来对文件内容进行修改,然后再存回去。

我们已经能够通过 getFile() 方法拿到文本内容,那应该把内容放到哪里进行编辑呢?你有很多种选择:富文本编辑器、给 div 设置 contenteditable、唤起 VS Code...... 但本着最(能)小(用)原(就)则(行),我们还有更便捷的选项 ------ prompt!

prompt() 方法也支持第二个参数,我们把文本内容传入,弹出弹窗后,你就会看到内容已经填在了输入框中,现在就可以随意编辑里面的字符了。

JavaScript 复制代码
const file = await fileHandle.getFile()
const fileContent = await file.text()
const newContent = prompt('', fileContent) // newContent 即为修改后的文件内容

但是点击 Prompt 的确认按钮并不会让新内容自动写入文件,这里就需要用到上面提到的 createWritable 了。下面是一个完整的写入流流程:

JavaScript 复制代码
const writable = await fileHandle.createWritable() // 对待修改文件开启写入流
await writable.write(newContent) // 把新内容写入到文件
await writable.close() // 关闭写入流

至此,新的内容就已经被保存到文件中了。你可以在 Demo 中再次右键、打开文件查看内容,你会发现确实读取到了修改后的内容。

文件重命名

修改文件名也是文件管理中的常规操作,文件系统 API 也提供了对应的方法来实现。可能手快的同学已经在尝试 fileHandle.rename() 方法了。但 API 中还真没有这个方法,我们其实是要用一个 move() 方法。惊不惊喜意不意外?

因为从底层视角看,重命名文件和移动文件的处理过程类型,都是需要先新建一个文件(使用新的命名或者放到新的位置),再把源文件的数据复制过去,最后把源文件删除。目前在 Web 端还没有更高效的处理方式。

我们只需从 Prompt 获取新名称,再传给 move() 方法即可:

JavaScript 复制代码
const newName = prompt('请输入新的文件名')
await fileHandle.move(newName)

这样,文件重命名就搞定了。

删除文件、文件夹

删除实体就没有那么多幺蛾子了,我们甚至不用对文件和文件夹做逻辑上的区分,简单直接地调用 currentHandle.remove({ recursive: true }) 就行了。

但越是方便,就越要谨慎,因为涉及到删除用户的文件,所以如果要用于生产环境,最好给用户提供二次确认的机会。

写在结尾

恭喜你读完了本文,你真棒!

这次我们通过实现一个 Web 版文件管理器 demo,对文件管理 API 进行了较为深入的理解和实践。我再嘱咐两句:

  1. 涉及到操作用户文件,请务必谨慎。
  2. 为了保障安全性,文件系统 API 仅支持 https。

我是 Jax,在畅游 Web 技术领域的第 7 年,我仍然是坚定不移的 JavaScript 迷弟,Web 开发带给我太多乐趣。如果你也喜欢 Web 技术,或者想讨论本文内容,欢迎来聊!你可以通过下列方式找到我:

掘金:juejin.cn/user/113435...

GitHub:github.com/JaxNext

微信:JaxNext

相关推荐
Myli_ing20 分钟前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
dr李四维37 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
ifanatic38 分钟前
[面试]-golang基础面试题总结
面试·职场和发展·golang
I_Am_Me_1 小时前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
雯0609~1 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ1 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
前端百草阁2 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜2 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript