💰 点进来就是赚到知识点!本文带你了解如何用 JS 监控本地文件 ,点赞 、收藏 、评论更能促进消化吸收!
🚀 想解锁更多 Web 文件系统技能吗?快来订阅专栏「Web 玩转文件操作」!
📣 我是 Jax,在畅游 Web 技术海洋的又一年,我仍然是坚定不移的 JavaScript 迷弟,Web 技术带给我太多乐趣。如果你也和我一样,欢迎关注 、私聊!
开门见 demo
先来玩玩这个 demo ------ 在 Chrome 中监控本地文件夹。
在上面的 demo 中,点击按钮选择一个本地文件夹后,无论是在该文件夹中新增、修改还是删除内容,网页端都能够感知到每一步操作的细节,包括操作时间、操作对象是文件还是文件夹、具体执行了什么操作等等。
如果你的感觉是:"哟?有点儿意思!" 那么这篇文章就是专门为你而写的,读下去吧。
在本专栏的前几篇文章中,我们已经知道,Web 应用能对本地文件进行各种花式操作,例如选择文件/文件夹、增/删/改/查文件等等。网页好像能伸出长长的手臂,穿过浏览器触摸到了用户的本地文件。但你可能还不知道,网页也能长出千里眼、顺风耳,本地文件有什么风吹草动,都能被网页端监控到。如此灵通的耳目,它的名字就是 File System Observer API(文件系统观察者)。
API 简介
现在想象我们要开发一个 Web 端相册应用,展示用户本地文件夹中的图片。我们希望这个相册能实时响应用户的操作,例如增加/删掉几张图片后,无需用户手动在 Web 端刷新,就能自动更新到最新状态。
如果请你来实现自动刷新,阁下又该如何应对?
经典思路可能会是以短时间间隔轮询文件夹状态,读取并缓存每个文件的 lastModified
时间戳,如果前后两次轮询的时间戳发生了变化,再把前后差异更新到 Web 视图中。这种实现方式能达到效果,但还是有一些缺点,比如不能真正做到即时响应,且会有很大的性能问题等。
其实咱们都知道,最优雅高效的做法是仅在文件被操作时触发更新。原生操作系统如 WIndows 和 MacOS 都有这样的文件监听机制,但显然目前 Web 端还无法享受其便利性。除了在用户端,Node.js 应用也面临这样的问题。开发者苦此久矣。
直到 2023 年 6 月,来自谷歌的贡献者们开始推进一项 W3C 提案 ------ File System Observer(为方便叙述,下文将简称其为 FSO),旨在从浏览器层面向 Web 应用提供跨平台的文件监听支持。如果这项提案能够顺利进入 ECMAScript 标准,那么 Web 文件系统的又一块重要功能版图将得以补全,Web 生态将会变得更友好、更强大。
解锁尝鲜:加入 Origin Trial
FSO 还是一套崭新的 API,有多新呢?MDN 和 CanIUse 中还没有建立关于它的词条。但这并不意味着我们完全无法用于生产环境 ------ 正如你在本文开头的 demo 中体验到的,我已经用到线上功能中了。只要做一点配置工作,你和你的用户就能成为全球第一批享受到 FSO 的人 😎。
Chrome 已经对 FSO 开启了试用,版本范围是 129 到 134,你可以为你的 Web App 域名注册一个试用 token,你可以跟着我一步一步操作:
首先我们访问 https://developer.chrome.com/origintrials/#/view_trial/59109745109237761 并登录账号。
点击界面下方的「REGISTER」按钮,进入表单页:
按照上图的标注填写信息。每一个域名都需要单独注册一次。例如我本地开发调试时用的是localhost:3000,而线上域名是 rejax.fun,那么就需要给这两个域名分别走一遍 REGISTER 流程。
填写信息后提交表单,你会得到一串字符串 token:
将 token 复制出来,写到 Web App 的 html 文件中,像这样:
html
<meta http-equiv="origin-trial" content="把 token 粘贴到这里" />
或者用 JavaScript 动态插入:
JavaScript
const meta = document.createElement('meta')
meta.httpEquiv = 'origin-trial'
meta.content = token
document.head.appendChild(meta)
最后,在 Chrome 中打开你注册的域名所在的页面,在 Console 中输入 FileSystemObserver
并回车:
如果打印出了「native code」而不是「undefined」,那么恭喜,你已经成功解锁了 FSO 试用!
监听一个文件
有了试用资格,我们来监听一个文件,边调试代码边研究 FSO 的设计和实现。
实例化
上一小节的最后,我们用来测试是否解锁成功的 FileSystemObserver
就是 FSO 的构造函数,它接收一个回调函数作为参数。我们可以像这样实例化一个观察者:
JavaScript
function callback (params) {
console.log(params)
}
const observer = new FileSystemObserver(callback)
callback
函数会在被监听的文件发生变动时被执行,所以我们可以把响应变动的业务处理逻辑放在其中。
绑定目标文件
实例 observer
有一个 observe
方法,它接收两个参数。第二个参数暂且按下不表,我们先专心看第一个参数。
这个参数是一个 FileSystemHandle
格式的对象,代表着本地文件在 JavaScript 运行时中的入口。我们可以通过 showOpenFilePicker
来选择一个文件(假如我们选择了文件 a.js
),并获取到对应的 FileSystemHandle
。
JavaScript
const [fileHandle] = await window.showOpenFilePicker()
observer.observe(fileHandle)
如果你想看
FileSystemHandle
和showOpenFilePicker
的详解,可以移步至本专栏的上一篇文章《谁也别拦我们,网页里直接增删改查本地文件! 》。
调用 observe
方法后,这个文件就算是进入了我们的监控区域 📸 了,直到我们主动解除监听或者网页被关闭/刷新。
监听文件操作
当我们编辑文件 a.js
的内容时,给 observe()
传入的回调函数被调用,并且会接收到两个参数,第一个是本次的变动记录 records
,第二个是实例 observer
本身。我们打印 records
可以看到如下结构:
records
是一个数组,其元素是 FileSystemChangeRecord
类型的对象,我们重点关注以下几个属性:
-
changedHandle
:可以理解为这就是我们绑定的文件。 -
type
:变动类型,可取值及对应含义如下:type 值 含义 appeared 新建文件,或者移入被监听的根目录 disappeared 文件被删除,或者移出被监听的根目录 modified 文件内容被编辑 moved 文件被移动 unknown 未知类型 errored 出现报错
一般情况下,如果我们监听的是单个文件而不是一个目录,那么无论是把文件移走、重命名、删除, record
中的 type
值都会是 disappeared。
监听一个文件夹
监听文件夹的方式和监听文件类似,我们先用 showDirectoryPicker
选择一个文件夹(以文件夹 foo
为例),再把 DirectoryHandle
传入 observe
方法。
为方便描述,我们假设文件夹 foo 的结构如下:
/foo
├── 文件夹 dir1
├── 文件夹 **dir2**
└── 文件 a.js
JavaScript
const dirHandle = await window.showDirectoryPicker()
observer.observe(dirHandle)
与文件有所不同的是,文件夹会有子文件夹和子文件,这是一个树形结构。如果我们只想监听 foo
下面的一级子内容,那么使用像上方代码块那样的调用方式就可以了。但如果我们想密切掌控每一子级的变动,就需要额外的配置参数,也就是前文提到的第二个参数:
JavaScript
observer.observe(dirHandle, {
recursive: true
})
此时你可以在 foo 文件夹里面任意增、删、改子文件或文件夹,一切操作都能在回调函数里以 record
的形式被捕获到。子文件和子文件夹所支持的操作类型,record
值也具有相同结构,因此接下来我们从监听子文件的视角来观察 FSO。
监听子文件
创建和移入、删除和移出 a.js
的情况,record.type
的值分布如下:
文件移入 foo | 在 foo 中创建文件 | 文件从 foo 中移出 | 删除文件 |
---|---|---|---|
appeared | appeared | disappeared | disappeared |
其中移出和删除的表现,与监听单文件的情况是相同的。
我们来试试把 a.js
移到与它同级的文件夹 dir1
中,看看会得到怎样的 record
。
有几个点值得我们注意:
type
的值是 moved,说明只要a.js
还在foo
内,不管处于第几层,都不会触发type: appeared/disappeared
relativePathMovedFrom
是一个单元素数组,它代表移动前a.js
的文件路径relativePathComponents
有两个数组元素,代表被移动文件的新路径是dir1/a.js
但重命名子文件和监听单文件时不同。例如我们将 a.js
更名为 b.js
,会监听到如下 record
:
我们本以为 type
的值是 renamed,但其实是 moved,确实有点反直觉。从 record 上来看,与真正的移动操作相比,重命名的不同之处在于:
changedHandle
指向了重命名后的新文件b.js
relativePathMovedFrom
和relativePathComponents
分别包含的是旧名和新名
FSO 在状态设计上并没有直接定义一个重命名状态,但我们可以自己来区分。重命名的响应数据有这样的特征:
relativePathMovedFrom
和relativePathComponents
这两个数组的 length 一定相等- 且除了最后一个元素,两个数组的其他元素一定是一一对应相等的
因此我们可以这样判断重命名操作:
JavaScript
const { oldList: relativePathMovedFrom, newList: relativePathComponents } = recors[0]
let operation = '是常规的移动操作'
// 重命名前后,文件的目录路径没变,只是文件名变了
if (oldList.length === newList.length) {
const len = newList.length
for (let i = 0; i < len; i++) {
// 相同序号的新旧路径是否一样
const isEqual = newList[i] === oldList[i]
if (i < len - 1) {
if (!isEqual) break
} else if (!isEqual) {
operation = '是重命名操作,不是移动操作'
}
}
}
至此,我们已经摸清了如何监听子文件上的不同操作,除了监听单文件部分已经覆盖到的内容,增量知识点仅有移动和重命名这两块。
监听子文件夹
对子文件夹的操作,也不外乎新建、删除、移动、重命名,和子文件在逻辑上基本一致,我们可以直接复用子文件的监听逻辑,再加上用 record.changedHandle.kind === 'directory'
来判断是否是文件夹即可。
解除监听
当我们想主动解除对文件或文件夹的监听时,只需要调用对应 observer
的 disconnect
即可:
JavaScript
observer.disconnect()
结语
恭喜你读完了本文,你真棒!
这一次,我们勇敢地品尝了一只新鲜生猛的螃蟹,对 File System Observer API 进行了较为深入的理解和实践。如果你之前一直苦于 JS 无法监听文件,无法带给用户完备的功能和极致的体验,那么从现在开始,你可以开始着手准备升级你的 Web App 了!
这套船新版本的 API 有力地补齐了 Web 文件系统 API 的短板,增强了 Web App 的实现能力,提升了开发者和用户的体验。它还在不断修改完善中,非常需要我们开发者积极参与到标准的制定中来,让 Web 技术栈变得更高效、更易用!