本帖最后由 LinLin00 于 2023-12-23 01:23 编辑
此前,我们已经有了 LinLin00000000/usbuild: 一个基于 esbuild 的油猴脚本(UserScript)构建工具 (github.com) 这样一个开发工具,它拥有开发模式实时热重载的功能。
这个实时热重载就是监听你脚本源文件的变化,一有变化就会通知浏览器,然后浏览器进行页面刷新,免去了手动刷新的麻烦。
但是,即使有了 autoReloadDelay 这样的控制自动刷新间隔的参数,在元素繁多的页面进行频繁刷新也会产生一点不好的体验,因为刷新慢加载慢,再加上一般需要开着浏览器调试工具,会感觉到明显的卡。
我去找了另一个比较有名的构建工具 vite-plugin-monkey,它在搭配一些前端框架如 Vue、React 使用时可以对里面的前端页面实现 HMR,但一般我开发脚本都是使用 document.querySelector 获取页面上的元素进行一些修改,而不是直接塞一个前端框架在旁边。当我在它的 dev 模式中使用 document.querySelector 对页面元素做修改时,无法享受到 vite 的 hmr 功能,也是只能把整个网页重新加载。
于是我给我的工具加了一个实验性功能(尚未发布),可以在监听到文件变化时不直接刷新浏览器,而是进行动态地重新安装脚本,再加上一点小小的魔法(后面会讲到),就可以实现 Selector 级别的 HMR,下面直接看效果
(竟然只支持 1MB 以下的图片,只能压缩得非常糊)
原理其实很简单,虽然每次都会重新安装脚本,但是只要在 unsafeWindow 上存储一个 cache 就行了,每次运行的时候先找有没有 cache,有就先恢复元素最初的样子,然后再执行用户自定义的 callback。其实就是简单封装一下 dynamicQuery
有一些局限性,需要做好缓存和恢复的工作,还要处理好副作用,可以考虑抽象出一个通用的插件 {save(), load(), update()} 就可以适用更多场合。
const selectorHMR = (selector, callback, options) => {
const CACHE_NAME = '_usbuild_selectorHMR'
if (!unsafeWindow[CACHE_NAME]) {
unsafeWindow[CACHE_NAME] = new Map()
}
const cacheMap = unsafeWindow[CACHE_NAME]
const uuid = selector
let cache
if (cacheMap.has(uuid)) {
console.log(`cache | ${uuid} | load`)
cache = cacheMap.get(uuid)
cache.remove()
dynamicQuery(selector, e => (e.outerHTML = cache.originHTML), options)()
cache.remove = dynamicQuery(selector, callback, options)
} else {
console.log(`cache | ${uuid} | save`)
cache = {}
cacheMap.set(uuid, cache)
cache.remove = dynamicQuery(
selector,
e => {
cache.originHTML = e.outerHTML
callback?.(e)
},
options
)
}
return cache.remove
}