是时候抛弃 chokidar 了?Node.js 原生支持 HMR 热更新!

boyanx17小时前技术教程2

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

1. 为什么需要模块热加载

提高开发效率的关键因素之一是尽可能少地丢弃状态。

node --watch -r ts-node/register src/index.ts

这意味着在 Node.js 中新的 --watch 标志位用处并不大,因为其会丢弃所有状态。理想的做法是,当模块或其依赖的模块发生变更时,简单地使其失效。这样,所有 import 和数据 data 都能始终保持最新,但只有部分模块树会被重新执行。

import {FileTree, hooks} from "immaculata";
import {registerHooks} from "module";
// 在 “./src” 下保留文件树的内存版本
const tree = new FileTree("src", import.meta.dirname);
// 当 src 目录文件变化后使其失效
registerHooks(hooks.useTree(tree));
// 保持最新
tree.watch().on("filesUpdated", doStuff);
doStuff();
// 现在导入 “src” 下的模块会重新执行
async function doStuff() {
  const {stuff} = await import("src/dostuff.js");
  // "stuff" is never stale
}

之前,immaculata 提供的功能与 Vite 相同,即使用内置的 node:vm 功能创建一个位于 Node 之上的临时模块系统,并使用自定义逻辑粘合各个系统,相当于创建了二等模块。

该方式的主要的缺点是逻辑重复且相互独立。

  • 重复:体现在查找、加载、解析文件、执行和存储模块对象,以及将所有模块相互粘合,还要考虑与 Node 自身的模块系统粘合
  • 独立:这些系统本质上是独立的,因此原生的 Node 模块钩子不会对临时系统产生影响

2. 使用 Node 内置的 node:module 热加载原理

现在,通过使用 Node 内置的 node:module 模块添加模块钩子 (Hooks),可以原生实现 “热模块 (HMR)” 功能。

  • 首先,从磁盘加载源文件并将其保存在内存中,且不会影响大多数项目和开发机器的内存限制。为了满足这一需求,创建了一个 FileTree 类,其除了将文件树加载到内存之外不执行其他操作,并可选地通过 .watch() 保持文件树的更新,该函数返回一个带有 filesUpdated 事件的 EventEmitter。Node 的原生文件监听器 ( File Watcher) 可以节省磁盘空间,并返回需要的所有信息,因此开发者不再需要 chokidar 。
  • 提供了 useTree 双钩子 (dual-hook),其执行两项关键操作,包括:一个加载器钩子,使用 tree.files.get 而不是 fs.readFileSync 返回源字符串。同时添加了一个解析器钩子,将 ?ver=${file.version} 附加到任何给定模块的 URL 之后。
  • 使用 FileTree 构造函数和 watch 方法将每个文件的版本设置为 Date.now(),因为 Node 内部使用 URL 来表示所有模块。

实际上,这意味着开发者可以先导入一个模块文件,然后在 filesUpdated 事件之后再次导入同一文件。此时,要么返回缓存的模块对象,要么重新执行该文件。

该难题唯一缺少的就是依赖关系树。由于模块钩子会在导入期间调用,因此可以使用此信息来注册依赖关系,由 FileTree 内部完成。每次模块的依赖关系发生变化时,父模块本身的版本也会更新。而且该过程是递归的,因此即使单个深层依赖关系发生变化,模块也始终是最新的。

3. 如何使用钩子

3.1 基础用法

import {FileTree} from "immaculata";
import {useTree} from "immaculata/hooks.js";
import {registerHooks} from "node:module";

const tree = new FileTree("src", import.meta.dirname);
registerHooks(useTree(tree));
const myModule = await import("src/myModule.js");
// src/myModule.js 被执行
const myModule2 = await import("src/myModule.js");
// src/myModule.js  第二次不会再执行
tree.watch().on("filesUpdated", async () => {
  const myModule = await import("src/myModule.js");
  // src/myModule.js 如果失效会自动再次执行
});

由于有了依赖关系树,开发者可以在更新其版本的同时轻松地抛出 moduleInvalidated 事件。而且由于依赖关系树本身就是对象,因此可以从需要在失效时清理资源的模块中导入。

import * as ShikiMd from "@shikijs/markdown-it";
import type MarkdownIt from "markdown-it";
import * as Shiki from "shiki";
import {tree} from "../../static.ts";
const highlighter = await Shiki.createHighlighter({
  themes: ["dark-plus"],
  langs: ["tsx", "typescript", "json", "yaml", "bash"],
});
export function highlightCode(md: MarkdownIt) {
  md.use(ShikiMd.fromHighlighter(highlighter, { theme: "dark-plus"}));
}
// moduleInvalidated 事件
tree.onModuleInvalidated(import.meta.url, () => {
  highlighter.dispose();
});

3.2 Node.js 中启用 JSX

默认情况下,原生 Node.js 模块系统:

  • 拒绝将 .jsx 或 .tsx 文件视为可导入模块
  • 无法将 JSX 语法转换为 JavaScript

而使用 immaculata,开发者可以:

  • 使 Node.js 将 .jsx 和 .tsx 文件识别为有效模块
  • 告诉 Node.js 如何将 JSX/TSX 转换为有效的 JavaScript
  • 将默认的 react/jsx-runtime 重新映射到其他模块
import {compileJsx} from "immaculata/hooks.js"
import {registerHooks} from "module"
import ts from 'typescript'
import {fileURLToPath} from "url"
// 当 Node.js 加载时会自动将 tsx 转化为 JavaScript
registerHooks(compileJsx((str, url) =>
  ts.transpileModule(str, {
    fileName: fileURLToPath(url),
    compilerOptions: {
      target: ts.ScriptTarget.ESNext,
      module: ts.ModuleKind.ESNext,
      jsx: ts.JsxEmit.ReactJSX,
      sourceMap: true,
      inlineSourceMap: true,
      inlineSources: true,
    }
  }).outputText
))

3.3 MD 文件支持 SSG

按照用 markdown 编写每个站点的悠久传统,可以借助于下面代码实现了简单构建工具指南中引用的 processSite。

import { Pipeline, type FileTree } from 'immaculata'
import { md } from "./markdown.ts"
import { template } from "./template.tsx"

export function processSite(tree: FileTree) {
  const files = Pipeline.from(tree.files)
  // make `site/public/` be the file tree
  files.without('/public/').remove()
  files.do(f => f.path = f.path.slice('/public'.length))
  // find all .md files and render in a jsx template
  files.with(/\.md$/).do(f => {
    f.path = f.path.replace('.md', '.html')
    f.text = template(md.render(f.text))
  })

  return files.results()
}

参考资料

https://immaculata.dev/blog/native-nodejs-hmr.html

https://github.com/sdegutis/immaculata

https://medium.com/@jircdev/hmr-in-node-with-beyondjs-4869060c9d83

标签: tree.js

相关文章

从 “卡顿” 到 “秒开”:外投首屏性能优化的实战锦囊|得物技术

一、背景在互联网时代,网站性能的好坏直接影响用户体验和转化率。对投放的广告页面而言,如何在保证视觉效果和功能的同时提升加载速度,成为了开发者必须面对的挑战。本文将探讨几种有效的外投页面性能优化策略,包...

Sonda 横空出世,JS 与 CSS 通用可视化和分析神器?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。什么是 Son...

browser-use:AI 驱动的浏览器自动化神器——DOM识别与交互详解

browser-use 可以识别网页中可交互DOM内容,并能与之进行交互。本文将详细介绍 browser-use 实现这一核心功能的技术细节。一、可交互元素识别browser-use 是通过 DOMS...

尤雨溪力荐 Vorms!代码最少!功能最强!

在前端开发中,表单验证一直是个让人头疼的问题。但今天要给大家介绍一个尤雨溪都点赞的表单验证工具 —— Vorms。它只有 12KB,曾获得尤雨溪的强烈推荐,绝对是 Vue 3 开发者的福音!Vorms...

我的世界手机版JS资源下载(我的世界手机版资源下载软件)

我的世界手机版JS资源下载攻略带给玩家,希望对玩家们有帮助,看看我的世界手机版JS资源下载攻略。首先我们来了解一下我的世界这个游戏中什么是JS。我的世界手机版js详解:我的世界手机版js详解,手机js...

来了!JavaScript 最强大的 8 个 DOM API

作为前端开发者,我们每天都在操作 DOM,但 DOM API 中隐藏着许多鲜为人知却极其实用的方法。本文将介绍一些「冷门但能显著提升开发效率」的DOM操作技巧。1. Element.checkVisi...

发表评论    

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。