Node.js 22+ 特性实战:用内置能力写微服务与 CLI,能少装一堆依赖
Node.js 从 22 开始(22 LTS → 24 当前主线)明显在走一条路:把前端/浏览器生态里已经标准化的 API 收编进运行时,同时把测试、TS 执行、权限、env 这些"每个项目都要装一遍"的周边工具内置化 。对写微服务和 CLI 的人来说,这意味着很多小工具可以做到 package.json 里 dependencies 接近空,镜像更小、CVE 面更小、冷启动更快。
下面挑 5 个对"微服务 + CLI"最实用的内置特性,每个都给可抄的代码片段。
1. 原生 WebSocket 客户端(v22.4 稳定,全局 WebSocket)
以前 Node 想连 WebSocket 服务端,必装 ws。现在全局 WebSocket 基于 Undici 实现,客户端场景零依赖 ,事件模型和浏览器完全一致(EventTarget 风格,addEventListener('message'))。
注意:它是客户端 实现,不是服务端。要起 WS 服务端仍用
ws/uWebSockets.js等;但微服务之间"订单服务连通知服务的 WS 推送""CLI 订阅后端实时日志"这类客户端活,已经不用装包。
微服务里订阅上游事件流的实战写法:
javascript
// subscriber.mjs ------ Node 22.4+ 直接跑:node subscriber.mjs
const ws = new WebSocket('wss://events.internal/orders');
ws.addEventListener('open', () => {
ws.send(JSON.stringify({ type: 'subscribe', topic: 'order.created' }));
});
ws.addEventListener('message', (ev) => {
const msg = JSON.parse(ev.data);
if (msg.type === 'order.created') {
// 这里接你的领域逻辑,比如写本地 SQLite / 发内部 HTTP
console.log('new order', msg.payload.id);
}
});
ws.addEventListener('close', (ev) => {
console.warn(`ws closed ${ev.code}/${ev.reason}, reconnect in 1s`);
setTimeout(() => connect(), 1000); // 生产里用指数退避
});
CLI 场景同理:比如 mycli logs --follow 用原生 WS 连后端 tail 通道,比塞一个 ws 依赖轻得多。
踩坑点:
- 标准
WebSocket构造器不能传自定义 Header (如Authorization),鉴权走 URL token 或 cookie 或连接后第一条消息; - 二进制用
binaryType = 'arraybuffer'收,不要假设拿到 Buffer; - 心跳自己用
setInterval发应用层 ping,原生 API 不替你做。
2. 内置测试运行器 node:test(v22 稳定,v24 自动等子测试)
node:test + node:assert/strict + mock 三件套,覆盖单元测试 90% 场景:不用 Jest/Vitest,CLI 工具的单测直接 node --test 跑。
微服务里测一个纯函数 + mock 定时器:
javascript
// ttl-cache.test.mjs
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
class TTLCache {
constructor(ttl) { this.ttl = ttl; this.store = new Map(); }
set(k, v) { this.store.set(k, { v, at: Date.now() }); }
get(k) {
const item = this.store.get(k);
if (!item) return undefined;
if (Date.now() - item.at > this.ttl) { this.store.delete(k); return undefined; }
return item.v;
}
}
test('TTL 过期失效', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
const cache = new TTLCache(1000);
cache.set('k', 'v');
assert.equal(cache.get('k'), 'v');
// 把时间拨过 1s
t.mock.timers.tick(1100);
assert.equal(cache.get('k'), undefined);
});
CLI 工具测命令行出口:
javascript
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
test('cli --version 输出 semver', (_, done) => {
execFile('node', ['./bin/cli.mjs', '--version'], (err, stdout) => {
assert.ifError(err);
assert.match(stdout, /\d+.\d+.\d+/);
done();
});
});
Node 24 起子测试不再用手动 await ,嵌套 t.test() 自动等完,少一个 flaky 来源。 覆盖率用 node --test --experimental-test-coverage,snapshot 用 t.assert.snapshot() + --test-update-snapshots,TAP/JUnit 报告都有。
3. 原生跑 TS:--experimental-strip-types(22 引入,25+ 趋稳)
简单脚本/CLI 不再需要 ts-node、tsx、先 tsc 编译。Node 22 起:
css
node --experimental-strip-types cli.ts
只做类型剥离(type stripping) ,不做类型检查------CI 里另跑 tsc --noEmit 兜底。
对 CLI 特别香:发布时不用发编译后的 dist/,bin 直接指 .ts 入口(配合 engines.node >=22.6),用户装包即用,包体积少一半。
限制:不支持
enum、命名空间、参数属性等需要运行时语义的 TS 特性,只认纯类型注解。
4. 权限模型 + 内置 .env + util.parseArgs:CLI 安全与解析三件套
权限模型 (22 experimental,24 简化成 --permission)让 CLI 可以声明"我只读配置目录、只连这个域名",用户能审计你没偷摸扫他文件系统:
ini
node --permission \
--allow-fs-read=./config \
--allow-net=api.internal:443 \
cli.mjs sync
代码里还能自查:
javascript
import { permission } from 'node:process';
if (!permission.has('fs.read', './secrets')) {
// 已经在受限模式,别尝试读
}
.env 原生加载 ,告别 dotenv:
ini
node --env-file=.env --env-file=.env.local cli.mjs
# 或代码里 process.loadEnvFile('.env')
参数解析 用内置 util.parseArgs(18.3+ 就有,22+ CLI 标配),零依赖替代 commander 处理简单场景:
php
import { parseArgs } from 'node:util';
const { values, positionals } = parseArgs({
options: {
version: { type: 'boolean', short: 'v' },
env: { type: 'string', short: 'e', default: 'prod' },
},
allowPositionals: true,
});
复杂多子命令再考虑 commander,简单 DevOps 小工具到这就够了。
5. node:fs/promises 的 glob + node --run:微服务脚本化与文件遍历
Node 22 起 fs.glob / fs.globSync 内置,CLI 批量扫文件不再装 glob:
javascript
import { glob } from 'node:fs/promises';
const files = await glob('src/**/*.handler.mjs');
node --run(22+)替代 npm run,容器/CI 里少一层 npm 启动开销:
arduino
node --run build
node --run migrate
微服务单仓库里把"启服务 / 跑迁移 / 清缓存"写成 package.json 的 scripts,用 node --run 直接调,镜像里甚至可以不装 npm 生产依赖之外的东西。
微服务 / CLI 场景的依赖消减对照
| 过去必装 | Node 22+ 内置替代 | 适用边界 |
|---|---|---|
ws |
全局 WebSocket(客户端) |
仅客户端,不起 WS 服务端 |
jest / vitest |
node:test + mock |
单测/集成测够用,组件测仍用 Vitest |
ts-node / tsx |
--experimental-strip-types |
纯类型剥离,不查类型 |
dotenv |
--env-file / loadEnvFile |
基础场景 |
glob |
fs.glob |
简单通配 |
commander |
util.parseArgs |
单命令/少量 flag |
node-fetch/axios |
全局 fetch + AbortController |
已在 22 前稳定 |
一个内部 CLI 或小微服务,把这些换掉后 dependencies 可以从十几个降到 2~3 个(剩下的大概率是真正的领域库,如 better-sqlite3、zod、pino)。
落地建议
- CLI 工具 :目标引擎设
>=22.6,用parseArgs+node:test+ 原生WebSocket+--env-file,做到单文件可执行、零运行时依赖; - 微服务 :服务间事件订阅用原生 WS 客户端,健康检查/管理面用
node --test覆盖核心纯函数,TS 源码直接 strip 跑,容器里加--permission缩权限; - 别迷信全内置:WS 服务端、复杂路由、React 组件测试、类型安全门禁,该用库还是用库,内置能力是"降本"不是"取代一切"。
Node 22+ 这套组合拳的本质,是把"写一个能发布的小工具"的脚手架成本压到极低------你省下的不是某一次 npm install 的时间,而是每次新建仓库都要重新决策"测试框架选啥、WS 库选啥、env 怎么加载"的认知开销。