注册
web

在我硬盘安监控了?纯 JS 监听本地文件的一举一动


💰 点进来就是赚到知识点!本文带你用 JS 代码监控本地文件点赞收藏评论更能促进消化吸收!


🚀 想解锁更多 Web 文件系统的技能吗?快来订阅专栏「Web 玩转文件操作」!


📣 我是 Jax,在畅游 Web 技术海洋的又一年,我仍然是坚定不移的 JavaScript 迷弟,Web 技术带给我太多乐趣。如果你也和我一样,欢迎关注私聊



开门见 demo


先来玩玩这个 demo —— 在 Chrome 中监控本地文件夹


20241005-220737.gif


在上面的 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,你可以跟着我一步一步操作:


首先我们访问 developer.chrome.com/origintrial… 并登录账号。


20241006-163407.jpeg


点击界面下方的「REGISTER」按钮,进入表单页:


20241006-163919.jpeg


按照上图的标注填写信息。每一个域名都需要单独注册一次。例如我本地开发调试时用的是localhost:3000,而线上域名是 rejax.fun,那么就需要给这两个域名分别走一遍 REGISTER 流程。


填写信息后提交表单,你会得到一串字符串 token:


20241006-164840.jpeg


将 token 复制出来,写到 Web App 的 html 文件中,像这样:


<meta http-equiv="origin-trial" content="把 token 粘贴到这里" />

或者用 JavaScript 动态插入:


const meta = document.createElement('meta')
meta.httpEquiv = 'origin-trial'
meta.content = token
document.head.appendChild(meta)

最后,在 Chrome 中打开你注册的域名所在的页面,在 Console 中输入 FileSystemObserver 并回车:


20241006-165520.jpeg


如果打印出了「native code」而不是「undefined」,那么恭喜,你已经成功解锁了 FSO 试用!


监听一个文件


有了试用资格,我们来监听一个文件,边调试代码边研究 FSO 的设计和实现。


实例化


上一小节的最后,我们用来测试是否解锁成功的 FileSystemObserver 就是 FSO 的构造函数,它接收一个回调函数作为参数。我们可以像这样实例化一个观察者:


function callback (params) {
console.log(params)
}
const observer = new FileSystemObserver(callback)

callback 函数会在被监听的文件发生变动时被执行,所以我们可以把响应变动的业务处理逻辑放在其中。


绑定目标文件


实例 observer 有一个 observe 方法,它接收两个参数。第二个参数暂且按下不表,我们先专心看第一个参数。


这个参数是一个 FileSystemHandle 格式的对象,代表着本地文件在 JavaScript 运行时中的入口。我们可以通过 showOpenFilePicker 来选择一个文件(假如我们选择了文件 a.js),并获取到对应的 FileSystemHandle


const [fileHandle] = await window.showOpenFilePicker()
observer.observe(fileHandle)


如果你想看 FileSystemHandleshowOpenFilePicker 的详解,可以移步至本专栏的上一篇文章谁也别拦我们,网页里直接增删改查本地文件!



调用 observe 方法后,这个文件就算是进入了我们的监控区域 📸 了,直到我们主动解除监听或者网页被关闭/刷新。


监听文件操作


当我们编辑文件 a.js 的内容时,给 observe() 传入的回调函数被调用,并且会接收到两个参数,第一个是本次的变动记录 records,第二个是实例 observer 本身。我们打印 records 可以看到如下结构:


20241006-201248.jpeg


records 是一个数组,其元素是 FileSystemChangeRecord 类型的对象,我们重点关注以下几个属性:



  • changedHandle:可以理解为这就是我们绑定的文件。
  • type:变动类型,可取值及对应含义如下:
    type 值含义
    appeared新建文件,或者移入被监听的根目录
    disappeared文件被删除,或者移出被监听的根目录
    modified文件内容被编辑
    moved文件被移动
    unknown未知类型
    errored出现报错



一般情况下,如果我们监听的是单个文件而不是一个目录,那么无论是把文件移走、重命名、删除, record 中的 type 值都会是 disappeared。


监听一个文件夹


监听文件夹的方式和监听文件类似,我们先用 showDirectoryPicker 选择一个文件夹(以文件夹 foo 为例),再把 DirectoryHandle 传入 observe 方法。



为方便描述,我们假设文件夹 foo 的结构如下:


/foo


├── 文件夹 dir1


├── 文件夹 **dir2**


└── 文件 a.js



const dirHandle = await window.showDirectoryPicker()
observer.observe(dirHandle)

与文件有所不同的是,文件夹会有子文件夹和子文件,这是一个树形结构。如果我们只想监听 foo 下面的一级子内容,那么使用像上方代码块那样的调用方式就可以了。但如果我们想密切掌控每一子级的变动,就需要额外的配置参数,也就是前文提到的第二个参数:


observer.observe(dirHandle, {
recursive: true
})

此时你可以在 foo 文件夹里面任意增、删、改子文件或文件夹,一切操作都能在回调函数里以 record 的形式被捕获到。子文件和子文件夹所支持的操作类型,record 值也具有相同结构,因此接下来我们从监听子文件的视角来观察 FSO。


监听子文件


创建和移入、删除和移出 a.js 的情况,record.type 的值分布如下:


文件移入 foo在 foo 中创建文件文件从 foo 中移出删除文件
appearedappeareddisappeareddisappeared

其中移出和删除的表现,与监听单文件的情况是相同的。


我们来试试把 a.js 移到与它同级的文件夹 dir1 中,看看会得到怎样的 record


20241006-211746.jpeg


有几个点值得我们注意:



  • type 的值是 moved,说明只要 a.js 还在 foo 内,不管处于第几层,都不会触发 type: appeared/disappeared
  • relativePathMovedFrom 是一个单元素数组,它代表移动前 a.js 的文件路径
  • relativePathComponents 有两个数组元素,代表被移动文件的新路径是 dir1/a.js

但重命名子文件和监听单文件时不同。例如我们将 a.js 更名为 b.js,会监听到如下 record


20241006-210550.jpeg


我们本以为 type 的值是 renamed,但其实是 moved,确实有点反直觉。从 record 上来看,与真正的移动操作相比,重命名的不同之处在于:



  • changedHandle 指向了重命名后的新文件 b.js
  • relativePathMovedFromrelativePathComponents 分别包含的是旧名和新名

FSO 在状态设计上并没有直接定义一个重命名状态,但我们可以自己来区分。重命名的响应数据有这样的特征:



  • relativePathMovedFromrelativePathComponents 这两个数组的 length 一定相等
  • 除了最后一个元素,两个数组的其他元素一定是一一对应相等的

因此我们可以这样判断重命名操作:


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’ 来判断是否是文件夹即可。


解除监听


当我们想主动解除对文件或文件夹的监听时,只需要调用对应 observerdisconnect 即可:


observer.disconnect()

结语


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


这一次,我们勇敢地品尝了一只新鲜生猛的螃蟹,对 File System Observer API 进行了较为深入的理解和实践。如果你之前一直苦于 JS 无法监听文件,无法带给用户完备的功能和极致的体验,那么从现在开始,你可以开始着手准备升级你的 Web App 了!


这套船新版本的 API 有力地补齐了 Web 文件系统 API 的短板,增强了 Web App 的实现能力,提升了开发者和用户的体验。它还在不断修改完善中,非常需要我们开发者积极参与到标准的制定中来,让 Web 技术栈变得更高效、更易用!


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

0 个评论

要回复文章请先登录注册