注册
web

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


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


转载请联系作者 Jax。



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


demo.gif


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


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


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


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


venders.jpeg


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


FileSystemHandle


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


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


属性:name 和 kind


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


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


校验方法 isSameEntry()


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


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

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

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


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


删除方法 remove()


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


const [handle] = await showOpenFilePicker()
handle.remove()

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


handle.remove({ recursive: true })

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


权限方法 queryPermission() 和 requestPermission()


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


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()


const dirHandle = await showDirectoryPicker()

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


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


for await (const sub of dirHandle.values()) {
const { name, kind } = sub
console.log(name, kind)
}

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


读取文件内容


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


// if sub.kind === 'file'
const fileHandle = await dirHandle.getFileHandle(sub.name)

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


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 方法:


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

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


编辑文件内容


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


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


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


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

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


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

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


文件重命名


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


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


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


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



作者:JaxNext
来源:juejin.cn/post/7416933490136252452

0 个评论

要回复文章请先登录注册