Node.js 现代开发工作流
自诞生以来,Node.js 经历了翻天覆地的变革。如果你已经使用 Node.js 多年,大概率亲身见证了这一演变 —— 从充斥回调函数、由 CommonJS 主导的时代,到如今简洁、基于标准的开发体验。
这些变化绝非表面功夫,而是代表着服务器端 JavaScript 开发方式的根本性转变。现代 Node.js 拥抱 Web 标准、减少外部依赖,并提供更直观的开发体验。让我们深入探讨这些变革,理解它们为何对 2025 年的应用开发至关重要。
1. 模块系统:ESM 成为新标准
模块系统或许是最能体现差异的地方。CommonJS 曾为我们服务多年,但 ES 模块(ESM)已成为无可争议的赢家,它提供了更好的工具支持,并与 Web 标准保持一致。
旧方式(CommonJS)
看看我们过去如何组织模块。这种方式需要显式导出和同步导入:
javascript
// math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// app.js
const { add } = require('./math');
console.log(add(2, 3));
这在当时是可行的,但存在局限性 —— 没有静态分析、不支持树摇(tree-shaking),且与浏览器标准不兼容。
现代方式(带 Node 前缀的 ES 模块)
现代 Node.js 开发采用 ES 模块,并增加了一个关键特性 —— 内置模块的node:
前缀。这种显式命名方式避免了混淆,让依赖关系一目了然:
javascript
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
import { readFile } from 'node:fs/promises'; // 现代node:前缀
import { createServer } from 'node:http';
console.log(add(2, 3));
node:
前缀不仅是一种约定,更是向开发者和工具发出的明确信号:你正在导入 Node.js 内置模块,而非 npm 包。这避免了潜在的冲突,让代码对依赖的描述更清晰。
顶层 await:简化初始化流程
最具颠覆性的特性之一是顶层 await。再也无需为了在模块级别使用 await,而将整个应用包裹在 async 函数中:
javascript
// app.js - 无需包装函数的简洁初始化
import { readFile } from 'node:fs/promises';
const config = JSON.parse(await readFile('config.json', 'utf8'));
const server = createServer(/* ... */);
console.log('应用已启动,配置信息:', config.appName);
这消除了过去随处可见的 “立即执行异步函数表达式(IIFE)” 模式。代码变得更线性,也更易于推理。
2. 内置 Web API:减少外部依赖
Node.js 大幅拥抱 Web 标准,将 Web 开发者早已熟悉的 API 直接引入运行时。这意味着更少的依赖,以及跨环境的更高一致性。
Fetch API:告别 HTTP 库依赖
还记得每个项目都需要 axios、node-fetch 等库来处理 HTTP 请求的时代吗?那些日子已经过去了。Node.js 现在原生支持 Fetch API:
javascript
// 旧方式 - 需要外部依赖
const axios = require('axios');
const response = await axios.get('https://api.example.com/data');
// 现代方式 - 带增强功能的内置fetch
const response = await fetch('https://api.example.com/data');
const data = await response.json();
但现代方式不仅是替换 HTTP 库这么简单。你还能获得内置的复杂超时和取消支持:
javascript
async function fetchData(url) {
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(5000) // 内置超时支持
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
throw new Error('请求超时');
}
throw error;
}
}
这种方式无需超时库,并提供一致的错误处理体验。AbortSignal.timeout()
方法尤其巧妙 —— 它会创建一个在指定时间后自动中止的信号。
AbortController:优雅的操作取消机制
现代应用需要优雅地处理取消操作,无论是用户触发还是超时导致。AbortController 提供了标准化的操作取消方式:
javascript
// 干净地取消长时间运行的操作
const controller = new AbortController();
// 设置自动取消
setTim