Node.js 新特性
问题
Node.js 近年来有哪些重要的新特性?
答案
Node.js 持续演进,引入了原生 ESM 支持、内置 fetch、测试运行器、权限模型等重要特性。
ESM 原生支持
启用 ESM
// package.json
{
"type": "module"
}
// 或使用 .mjs 扩展名
// app.mjs
// ESM 导入
import express from 'express';
import { readFile } from 'fs/promises';
import config from './config.js'; // 必须带扩展名
// 动态导入
const module = await import('./dynamic.js');
ESM 与 CommonJS 互操作
// ESM 中导入 CommonJS
import pkg from 'cjs-package'; // 默认导出
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjsModule = require('./legacy.cjs');
// 获取 __dirname 和 __filename
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// import.meta
console.log(import.meta.url); // file:///path/to/file.js
console.log(import.meta.resolve); // 解析模块路径
内置 fetch(Node 18+)
// 无需安装 node-fetch
const response = await fetch('https://api.example.com/users');
const data = await response.json();
// POST 请求
const result = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Alice' })
});
// AbortController 支持
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { signal: controller.signal });
const data = await response.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Request timed out');
}
} finally {
clearTimeout(timeout);
}
内置测试运行器(Node 18+)
// test/user.test.ts
import { describe, it, before, after, mock } from 'node:test';
import assert from 'node:assert/strict';
describe('User Service', () => {
before(() => {
// 测试前准备
});
after(() => {
// 测试后清理
});
it('should create user', async () => {
const user = await createUser({ name: 'Alice' });
assert.equal(user.name, 'Alice');
assert.ok(user.id);
});
it('should throw on invalid input', async () => {
await assert.rejects(
() => createUser({ name: '' }),
{ message: 'Name is required' }
);
});
});
// Mock
import { mock } from 'node:test';
it('should mock function', () => {
const fn = mock.fn((x: number) => x * 2);
assert.equal(fn(2), 4);
assert.equal(fn.mock.calls.length, 1);
assert.deepEqual(fn.mock.calls[0].arguments, [2]);
});
# 运行测试
node --test
node --test --watch # 监听模式
node --test --coverage # 覆盖率
node --test --test-reporter spec # 报告格式
权限模型(Node 20+ 实验性)
# 启用权限模型
node --experimental-permission app.js
# 文件系统权限
node --experimental-permission \
--allow-fs-read=/app \
--allow-fs-write=/tmp \
app.js
# 网络权限
node --experimental-permission \
--allow-worker \
--allow-child-process \
app.js
// 检查权限
import { permission } from 'node:process';
if (permission.has('fs.read', '/path')) {
// 有读取权限
}
单文件可执行(Node 20+)
# 创建 SEA (Single Executable Application)
# 1. 创建配置
echo '{ "main": "app.js", "output": "sea-prep.blob" }' > sea-config.json
# 2. 生成 blob
node --experimental-sea-config sea-config.json
# 3. 复制 Node 可执行文件
cp $(which node) my-app
# 4. 注入 blob
npx postject my-app NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
# 5. 运行
./my-app
watch 模式(Node 18+)
# 内置 watch 模式,无需 nodemon
node --watch app.js
# 指定监听路径
node --watch-path=./src --watch-path=./config app.js
# 忽略文件
node --watch --watch-preserve-output app.js
新增 API
Web Streams API
import { ReadableStream, WritableStream, TransformStream } from 'stream/web';
// Web 标准 Stream
const readable = new ReadableStream({
start(controller) {
controller.enqueue('Hello');
controller.enqueue('World');
controller.close();
}
});
const transform = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
});
const writable = new WritableStream({
write(chunk) {
console.log(chunk);
}
});
await readable.pipeThrough(transform).pipeTo(writable);
结构化克隆
import { structuredClone } from 'util';
const original = {
date: new Date(),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
regex: /pattern/g,
buffer: new ArrayBuffer(8)
};
const clone = structuredClone(original);
// 正确复制所有类型
自定义 ESM Loader(已更名为 Hooks)
// loader.mjs
export async function resolve(specifier, context, nextResolve) {
// 自定义模块解析
if (specifier.startsWith('@/')) {
specifier = specifier.replace('@/', './src/');
}
return nextResolve(specifier, context);
}
export async function load(url, context, nextLoad) {
// 自定义加载行为
return nextLoad(url, context);
}
node --loader ./loader.mjs app.js
# Node 20+
node --import ./register.mjs app.js
常见面试问题
Q1: Node.js ESM 和 CommonJS 有什么区别?
答案:
| 特性 | ESM | CommonJS |
|---|---|---|
| 语法 | import/export | require/module.exports |
| 加载 | 异步 | 同步 |
| 解析 | 静态分析 | 运行时 |
| this | undefined | module.exports |
| 扩展名 | 必须 | 可选 |
| 顶层 await | 支持 | 不支持 |
| Tree Shaking | 支持 | 不支持 |
// ESM
import { readFile } from 'fs/promises';
export const helper = () => {};
// CommonJS
const { readFile } = require('fs').promises;
module.exports = { helper: () => {} };
Q2: Node.js 内置 fetch 和 node-fetch 有什么区别?
答案:
| 特性 | 内置 fetch | node-fetch |
|---|---|---|
| 安装 | 无需安装 | 需要安装 |
| API | Web 标准 | 接近标准 |
| 依赖 | 内置 | 外部依赖 |
| Node 版本 | 18+ | 任意版本 |
| 底层 | Undici | http 模块 |
// 内置 fetch(Node 18+)
const res = await fetch(url);
// 向后兼容
const fetch = globalThis.fetch || (await import('node-fetch')).default;
Q3: Node.js 内置测试运行器有什么优势?
答案:
| 优势 | 说明 |
|---|---|
| 零依赖 | 无需安装 Jest/Mocha |
| 原生支持 | 官方维护 |
| TypeScript | 配合 ts-node 使用 |
| 覆盖率 | 内置覆盖率报告 |
| 隔离 | 每个测试文件独立进程 |
# 基本使用
node --test
# 配合 TypeScript
node --import tsx --test test/*.test.ts
# 覆盖率
node --test --experimental-test-coverage
Q4: 如何在 ESM 中获取 __dirname?
答案:
// CommonJS 中
console.log(__dirname);
console.log(__filename);
// ESM 中
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 或者直接使用 import.meta
console.log(import.meta.url); // file:///path/to/file.js
// Node 20.11+ 新增
console.log(import.meta.dirname);
console.log(import.meta.filename);
Q5: Node.js 各主要版本有哪些重要特性?
答案:
| 版本 | 重要特性 |
|---|---|
| Node 16 | V8 9.0、Apple Silicon、npm 7 |
| Node 18 | 内置 fetch、测试运行器、watch 模式 |
| Node 20 | 权限模型、WebSocket、稳定测试运行器 |
| Node 22 | WebSocket 客户端、require(esm) |
// Node 18: 内置 fetch
const data = await fetch(url);
// Node 18: 内置测试
import { test } from 'node:test';
// Node 20: import.meta 增强
console.log(import.meta.dirname);
// Node 22: CommonJS 可以 require ESM
const esm = require('./esm-module.mjs');