Node.js 22+ 特性实战:用内置能力写微服务与 CLI,能少装一堆依赖

Node.js 22+ 特性实战:用内置能力写微服务与 CLI,能少装一堆依赖

Node.js 从 22 开始(22 LTS → 24 当前主线)明显在走一条路:把前端/浏览器生态里已经标准化的 API 收编进运行时,同时把测试、TS 执行、权限、env 这些"每个项目都要装一遍"的周边工具内置化 。对写微服务和 CLI 的人来说,这意味着很多小工具可以做到 package.jsondependencies 接近空,镜像更小、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-nodetsx、先 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/promisesglob + 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.jsonscripts,用 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-sqlite3zodpino)。


落地建议

  • CLI 工具 :目标引擎设 >=22.6,用 parseArgs + node:test + 原生 WebSocket + --env-file,做到单文件可执行、零运行时依赖;
  • 微服务 :服务间事件订阅用原生 WS 客户端,健康检查/管理面用 node --test 覆盖核心纯函数,TS 源码直接 strip 跑,容器里加 --permission 缩权限;
  • 别迷信全内置:WS 服务端、复杂路由、React 组件测试、类型安全门禁,该用库还是用库,内置能力是"降本"不是"取代一切"。

Node 22+ 这套组合拳的本质,是把"写一个能发布的小工具"的脚手架成本压到极低------你省下的不是某一次 npm install 的时间,而是每次新建仓库都要重新决策"测试框架选啥、WS 库选啥、env 怎么加载"的认知开销。

相关推荐
苍何3 小时前
飞书,成了豆包工作的“超级上下文”
后端
苍何3 小时前
会用 AI 做图,已经不够了(分享 3 个牛逼设计 skill)
后端
明月_清风3 小时前
发现一个超系统的 AI Agent 学习地图 —— Agent Atlas 推荐
人工智能·后端·agent
大勇前进4 小时前
设计模式在 JS 中的应用:用 TS 重写经典模式,结合 React / Vue 真实业务场景
后端
MacroZheng4 小时前
同事问我:"Claude Code经常失忆,不怕它把项目搞炸?",我:"怕,三个Markdown文件给它装个永不丢失的外置大脑!"
java·人工智能·后端
用户29975997127074 小时前
深分页的四种解法,以及为什么说它多半是个伪需求
后端
苏三说技术5 小时前
为什么越来越多人用 OpenSearch?
后端
大白805 小时前
JS 内存泄漏排查:用真实案例教你如何用 Chrome DevTools 定位并解决,顺带搞懂闭包和 GC
后端
泡海椒5 小时前
Lambda 极简调用:不定义接口直接执行 curl 命令,JQuick-Curl 让 Java HTTP 调用更短更快
后端