Node.js 22 已进入 LTS 阶段,代号 Jod。从 22.0.0 到当前的 22.23.0,这条线累积了不少让日常开发更顺手的能力——require() 终于能直接加载 ESM 模块,WebSocket 客户端不再需要第三方库,node --run 替代了 npm run 的部分场景,TypeScript 文件也能直接跑起来。这些不是实验玩具,而是逐步稳定到可以上生产的特性。下面挑几个最影响日常工作的改动,逐个拆开看。
require(esm):CJS 与 ESM 的墙终于开了个门
过去 CJS 项目想引用 ESM 包,只能走动态 import(),异步传染整个调用链。Node.js 22 把 require(esm) 稳定化,同步引用 ESM 导出终于可行了。
实际效果:你的老 CJS 服务可以直接 require() 一个只提供 ESM 入口的 npm 包,不用把整个文件改成 async。
// old-service.js — CommonJS 项目引用纯 ESM 包
const { parse } = require('yaml'); // yaml 包已只提供 ESM 导出
const config = parse(fs.readFileSync('config.yaml', 'utf8'));
console.log(config.database.host);
注意几个边界:
- 只支持 named exports,不支持 ESM 模块的 default 导出(需包作者显式 module.exports = { default })。实际遇到 default 不可用时,用 const mod = require('pkg'); mod.default 兜底。
- Top-level await 的 ESM 模块仍然不能被 require() 加载,会抛 ERR_REQUIRE_ASYNC_MODULE。
- 这是 Node.js 22 的稳定特性,不需要加 --experimental-require-module。
内置 WebSocket 客户端:告别 ws 依赖
Node.js 22 把 WebSocket 加入全局对象,协议实现符合浏览器标准。对大多数后端场景,你不再需要安装 ws 或 websocket。
// ws-client.js — 内置 WebSocket,零依赖
const ws = new WebSocket('ws://localhost:8080/echo');
ws.addEventListener('open', () => {
ws.send('hello from node 22');
});
ws.addEventListener('message', (event) => {
console.log('received:', event.data);
ws.close();
});
ws.addEventListener('error', (err) => {
console.error('connection failed:', err.message);
});
与 ws 库的差异:内置版本不支持自定义 headers(暂无 WebSocket(url, protocols, options) 的 headers 字段),也不支持 permessage-deflate 压缩扩展。如果你的服务端依赖压缩或需要传认证 header,暂时还得保留 ws。
node --run:少一层 npm 间接调用
node --run <script> 直接执行 package.json 中定义的 script,跳过 npm 的脚本解析层。好处:更快、更干净,CI 环境里不用装 npm。
// package.json
{
"scripts": {
"build": "tsc && node build-post.js",
"test": "node --test test/**/*.test.js",
"lint": "eslint src/"
}
}
# 直接用 node 执行,不依赖 npm
node --run build
node --run test
# CI Docker 镜像里只用 node,不装 npm
docker run node:22 node --run test
限制:不支持 npm 的 pre/post 钩子,不解析 npm_config_* 环境变量。复杂脚本链还是走 npm run。
--experimental-strip-types:TypeScript 直接跑
Node.js 22.6.0 引入 --experimental-strip-types,Node.js 会剥离类型注解后直接执行,不做类型检查。22.x 后续版本逐步稳定了这个能力。适合开发阶段的快速验证,不适合替代 tsc 编译。
// server.ts — 直接用 node 运行
import http from 'node:http';
const port: number = Number(process.env.PORT ?? 3000);
const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('running on node 22\n');
});
server.listen(port, () => {
console.log(`listening on :${port}`);
});
# 开发阶段快速启动
node --experimental-strip-types server.ts
关键认知:这是类型剥离,不是类型编译。enum、namespace、parameter properties 等需要转换的 TS 特性不支持,遇到会报错。正式发布前仍应跑 tsc --noEmit 做类型检查。
fs.glob:文件搜索不再依赖 globby
fs.glob 和 fs.globSync 进入稳定 API,基于 Rust 库 fast-glob 的性能水准。
// find-sources.js
import { glob } from 'node:fs/promises';
const files = await glob('src/**/*.ts', {
cwd: process.cwd(),
exclude: ['**/*.d.ts', '**/*.test.ts'],
});
console.log('source files:', files);
升级到 22 LTS 的实操清单
- 版本选择:生产用
22.xLTS 镜像,别用22.0.0,直接拉最新的22.23.0,累积了半年多的 bug fix。 require(esm)迁移:先在测试环境验证你依赖的 ESM 包是否全部支持 named exports,遇到 default-only 包加一层适配。- WebSocket 替换:新项目直接用内置
WebSocket;老项目如果用了压缩或自定义 header,暂不替换。 - TypeScript 开发流:开发用
--experimental-strip-types加速,CI 保留tsc --noEmit守门。 node --run进 CI:Dockerfile 里把npm run替换为node --run,镜像可以不装 npm,体积更小。- 监控兼容性:APM 和 tracing 工具确认支持 Node.js 22 的 V8 12.4+,部分老版本 SDK 可能需要升级。
Node.js 22 LTS 不是激进重构,而是把过去两年实验的特性逐步收口到稳定态。对大多数项目,升级风险可控,收益具体——少装依赖、少写适配代码、CI 更轻量。现在是从 20 LTS 切过来的合理时机。