node_modules 太胖?用 Node.js 原生功能给依赖做一次大扫除
写 Node.js 项目的时候,package.json 里的依赖列表是不是越来越长?
装个 node-fetch 发请求,装个 uuid 生成 ID,装个 dotenv 读环境变量,装个 chalk 输出彩色日志......每个包都不大,但加起来就是一堆。
问题是:
- 依赖多了,供应链风险就大了(还记得 left-pad 事件吗?)
node_modules越来越臃肿- 版本冲突和兼容性问题时不时冒出来
好消息是,Node.js 这几年一直在把常用功能收编进核心模块。很多以前必须装包才能用的东西,现在原生就有了。
这篇文章整理了 15 个可以被 Node.js 原生功能替代的 npm 包,按使用频率排序,看看哪些依赖可以从你的项目里删掉。
1. fetch() 替代 node-fetch
这个应该是用得最多的了。
以前 Node.js 没有 fetch,想发 HTTP 请求要么用 http 模块自己封装,要么装 node-fetch。现在不用了:
javascript
// Node.js 18+ 原生支持
const res = await fetch('https://api.github.com/repos/nodejs/node');
const data = await res.json();
console.log(data.stargazers_count);
- v17.5.0 实验性引入
- v18.0.0 稳定
API 和浏览器的 fetch 一样,代码可以前后端通用。
2. crypto.randomUUID() 替代 uuid
生成 UUID 是个很常见的需求,以前都用 uuid 包:
javascript
// 以前
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// 现在(Node.js 14.17.0+)
import { randomUUID } from 'node:crypto';
const id = randomUUID();
// 输出类似:'1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
少装一个包,还不用担心 uuid 的版本兼容问题。
3. --env-file 替代 dotenv
读 .env 文件是几乎每个项目都要做的事,以前必须装 dotenv:
javascript
// 以前
import 'dotenv/config';
console.log(process.env.API_KEY);
现在可以用命令行参数:
bash
# Node.js 20.10.0+
node --env-file=.env app.js
不用写任何代码,环境变量直接加载到 process.env。
要注意的是,--env-file 功能比 dotenv 简单,不支持变量展开(${VAR})和多行值。如果你的 .env 文件比较复杂,可能还是得用 dotenv。
4. fs.rm({ recursive: true }) 替代 rimraf
删除目录及其所有内容,以前用 rimraf:
javascript
// 以前
import rimraf from 'rimraf';
rimraf.sync('dist');
// 现在(Node.js 12.10.0+)
import { rm } from 'node:fs/promises';
await rm('dist', { recursive: true, force: true });
force: true 表示目录不存在时不报错,和 rm -rf 行为一致。
5. fs.mkdir({ recursive: true }) 替代 mkdirp
创建嵌套目录:
javascript
// 以前
import mkdirp from 'mkdirp';
mkdirp.sync('path/to/nested/dir');
// 现在(Node.js 10.12.0+)
import { mkdir } from 'node:fs/promises';
await mkdir('path/to/nested/dir', { recursive: true });
这个功能加得比较早,很多项目可能已经在用了。
6. node:test 替代测试框架
Node.js 现在有内置的测试模块:
javascript
// test.js
import { test } from 'node:test';
import assert from 'node:assert';
test('加法测试', () => {
assert.strictEqual(1 + 2, 3);
});
test('异步测试', async () => {
const result = await Promise.resolve(42);
assert.strictEqual(result, 42);
});
运行:
bash
node --test
- v18.0.0 实验性引入
- v20.0.0 稳定
对于模块级别的单元测试够用了。但如果是大型应用,需要 mock、覆盖率报告、并行执行等功能,Jest 或 Vitest 还是更合适。
7. util.styleText() 替代 chalk
终端彩色输出:
javascript
// 以前
import chalk from 'chalk';
console.log(chalk.red('Error!'));
console.log(chalk.green.bold('Success!'));
// 现在(Node.js 20.12.0+,v22.17.0 稳定)
import { styleText } from 'node:util';
console.log(styleText('red', 'Error!'));
console.log(styleText(['green', 'bold'], 'Success!'));
支持的样式包括:red、green、yellow、blue、bold、italic、underline 等。
8. util.stripVTControlCharacters() 替代 strip-ansi
去除 ANSI 转义字符(彩色输出的控制码):
javascript
import { stripVTControlCharacters } from 'node:util';
const colored = '\x1b[31mError\x1b[0m';
const plain = stripVTControlCharacters(colored);
// plain === 'Error'
在写日志到文件或者做字符串比较时很有用。
9. fs.glob() 替代 glob
文件匹配:
javascript
// 以前
import { glob } from 'glob';
const files = await glob('**/*.js');
// 现在(Node.js 22.0.0+)
import { glob } from 'node:fs/promises';
const files = await glob('**/*.js');
API 基本一致,迁移成本很低。
10. atob / btoa 替代 Base64 polyfill
Base64 编解码:
javascript
// Node.js 20+ 全局可用
const encoded = btoa('hello world');
// 'aGVsbG8gd29ybGQ='
const decoded = atob(encoded);
// 'hello world'
和浏览器 API 一致,写同构代码更方便了。
11. WebSocket 替代 ws(客户端场景)
javascript
// Node.js 21.0.0+(实验性)
const ws = new WebSocket('wss://echo.websocket.org');
ws.addEventListener('open', () => {
ws.send('Hello');
});
ws.addEventListener('message', (event) => {
console.log('Received:', event.data);
});
目前还是实验性的,而且只适合客户端场景。写 WebSocket 服务端,ws 包还是标准选择。
12. node:sqlite 替代 sqlite3 / better-sqlite3
javascript
// 实验性功能
import { open } from 'node:sqlite';
const db = await open(':memory:');
await db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
await db.run('INSERT INTO users (name) VALUES (?)', 'Alice');
const row = await db.get('SELECT * FROM users WHERE id = ?', 1);
最大的好处是不用编译原生模块了。sqlite3 和 better-sqlite3 都需要 node-gyp 编译,在某些环境下会遇到各种问题。
目前还是实验性的,生产环境建议观望。
13. EventTarget 替代 event-target-shim
javascript
// Node.js 15.0.0+ 全局可用
const target = new EventTarget();
target.addEventListener('message', (event) => {
console.log(event.detail);
});
target.dispatchEvent(new CustomEvent('message', { detail: 'hello' }));
和浏览器 API 一致。
14. URLPattern 替代 url-pattern
路由匹配:
javascript
// Node.js 20.0.0+(实验性)
const pattern = new URLPattern({ pathname: '/users/:id' });
const result = pattern.exec('https://example.com/users/123');
console.log(result.pathname.groups.id); // '123'
对于简单的路由匹配场景够用,但目前还是实验性的。
15. --experimental-strip-types 运行 TypeScript
bash
# Node.js 21.0.0+(实验性)
node --experimental-strip-types app.ts
这个功能只是去掉类型注解,不做类型检查,也不支持一些 TypeScript 特有的语法(如 enum、namespace)。
适合快速运行 .ts 文件做原型或测试,但不能替代完整的 TypeScript 工具链。
哪些可以直接用,哪些再等等?
| 功能 | 状态 | 建议 |
|---|---|---|
fetch() |
稳定 | 直接用 |
crypto.randomUUID() |
稳定 | 直接用 |
--env-file |
实验性 | 简单场景可用 |
fs.rm/mkdir |
稳定 | 直接用 |
node:test |
稳定 | 模块测试可用 |
util.styleText() |
稳定 | 直接用 |
fs.glob() |
稳定 | 直接用 |
atob/btoa |
稳定 | 直接用 |
WebSocket |
实验性 | 客户端可用 |
node:sqlite |
实验性 | 观望 |
URLPattern |
实验性 | 观望 |
| TypeScript 支持 | 实验性 | 原型/测试可用 |
迁移建议
-
先查版本 :确认你的 Node.js 版本支持这些功能。
node -v看一下。 -
渐进式迁移:不用一次全换,哪个包用得多、问题多,先换哪个。
-
实验性功能谨慎使用:标记为实验性的功能,API 可能会变,生产环境慎用。
-
保留复杂场景的包 :如果你需要
dotenv的变量展开、chalk的链式调用、Jest 的完整测试能力,继续用包也没问题。原生功能是多了个选择,不是必须替换。
依赖少了,项目就干净了。供应链安全、启动速度、维护成本,都能受益。
参考
如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:
Claude Code Skills (按需加载,意图自动识别,不浪费 token,介绍文章):
- code-review-skill - 代码审查技能,覆盖 React 19、Vue 3、TypeScript、Rust 等约 9000 行规则(详细介绍)
- 5-whys-skill - 5 Whys 根因分析,说"找根因"自动激活
- first-principles-skill - 第一性原理思考,适合架构设计和技术选型
全栈项目(适合学习现代技术栈):
- prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
- chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB