自 Node.js 推出以来,它已经经历了显著的演变。如果你已经使用 Node.js 编写代码多年,很可能亲身见证了这一变化的全过程——从早期充斥着回调函数、以 CommonJS 为主的开发模式,逐步发展为如今基于标准、结构清晰的现代开发体验。
这些变化并不只是表面上的改进,而是代表了我们在服务端 JavaScript 开发方式上的一次根本性转变。现代 Node.js 更加拥抱 Web 标准,减少对外部依赖的依赖,同时提供了更直观的开发者体验。下面我们将深入探讨这些转变,并理解它们为何在 2025 年对你的应用至关重要。
1. 模块系统:ESM 成为新标准
模块系统或许是变化最显著的地方。CommonJS 曾经发挥了重要作用,但现在 ES Modules(ESM)已经成为明显的赢家,提供了更好的工具链支持,并与 Web 标准保持一致。
旧方式(CommonJS)
来看一下我们过去是如何组织模块的。这种方式需要显式导出,并使用同步导入:
// math.jsfunction add(a, b) {return a + b;}module.exports = { add };// app.jsconst { add } = require('./math');console.log(add(2, 3));
这种方式虽然能正常工作,但也存在一些局限性——无法进行静态分析、不支持 tree-shaking,而且与浏览器的标准不兼容。
现代方式(使用 node: 前缀的 ES Modules)
现代 Node.js 开发全面采用 ES Modules,并加入了一个关键改进——对内置模块使用 node: 前缀。这个显式的命名方式可以避免混淆,让依赖关系更加清晰明确:
// math.jsexport function add(a, b) {return a + b;}// app.jsimport { add } from './math.js';import { readFile } from 'node:fs/promises'; // Modern node: prefiximport { createServer } from 'node:http';console.log(add(2, 3));
node: 前缀不仅仅是一种规范,它向开发者和工具清晰地表明你导入的是 Node.js 内置模块,而不是来自 npm 的包。这样可以避免潜在的命名冲突,并让代码在依赖声明上更加明确。
顶层 await:简化初始化流程
顶层 await 是最具变革性的特性之一。现在你无需再为了在模块层级使用 await 而将整个应用包裹在一个 async 函数中:
// app.js - 无需包装函数的简洁初始化import { readFile } from 'node:fs/promises';const config = JSON.parse(await readFile('config.json', 'utf8'));const server = createServer(/* ... */);console.log('应用已按配置启动:', config.appName);
这消除了过去随处可见的立即执行 async 函数表达式(IIFE)的常见写法。你的代码会变得更加线性、简洁,也更容易理解。
2. 内置 Web API:减少外部依赖
Node.js 大幅度拥抱了 Web 标准,把前端开发者早已熟悉的 API 直接引入到运行时。这意味着更少的外部依赖,以及跨环境的一致性。
Fetch API:告别 HTTP 库依赖
还记得过去每个项目都需要 axios、node-fetch 或类似的库来发送 HTTP 请求吗?那些日子已经结束了。现在 Node.js 原生就支持 Fetch API:
// 旧方式 —— 需要依赖外部库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 库,它还内置了更完善的超时和取消请求支持:
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 提供了一种标准化的操作取消方式:
// 干净地取消长时间运行的操作const controller = new AbortController();// 设置自动取消setTimeout(() => controller.abort(), 10000);try {const data = await fetch('https://slow-api.com/data', {signal: controller.signal});console.log('数据接收成功:', data);} catch (error) {if (error.name === 'AbortError') {console.log('请求已被取消 —— 这是预期行为');} else {console.error('意外错误:', error);}}
这种模式不仅适用于 fetch,还能应用到许多 Node.js API。你可以在文件操作、数据库查询,以及任何支持取消的异步操作中使用同一个 AbortController。
3. 内置测试:无需外部依赖的专业测试
过去做测试时需要在 Jest、Mocha、Ava 等框架之间做选择。而现在,Node.js 已经内置了功能完善的测试运行器,能够覆盖大多数测试需求,无需任何外部依赖。
使用 Node.js 内置测试运行器的现代测试
内置的测试运行器提供了一个简洁、熟悉的 API,看起来现代且功能完整:
// test/math.test.jsimport { test, describe } from 'node:test';import assert from 'node:assert';import { add, multiply } from '../math.js';describe('数学函数', () => {test('正确相加', () => {assert.strictEqual(add(2, 3), 5);});test('处理异步操作', async () => {const result = await multiply(2, 3);assert.strictEqual(result, 6);});test('遇到无效输入时抛出错误', () => {assert.throws(() => add('a', 'b'), /Invalid input/);});});
这一特性之所以特别强大,是因为它能够与 Node.js 的开发流程无缝集成:
# 使用内置运行器运行所有测试node --test# 开发时的 watch 模式node --test --watch# 代码覆盖率报告(Node.js 20+)node --test --experimental-test-coverage
watch 模式在开发过程中尤其有价值——当你修改代码时,测试会自动重新运行,无需额外配置就能立即获得反馈。
4. 更成熟的异步模式
虽然 async/await 并不是新特性,但围绕它的使用模式已经显著成熟。现代 Node.js 开发能够更高效地利用这些模式,并与更新的 API 结合使用。
async/await 与增强的错误处理
现代的错误处理方式将 async/await 与更完善的错误恢复机制和并行执行模式结合起来:
import { readFile, writeFile } from 'node:fs/promises';async function processData() {try {// 并行执行互不依赖的操作const [config, userData] = await Promise.all([readFile('config.json', 'utf8'),fetch('/api/user').then(r => r.json())]);const processed = processUserData(userData, JSON.parse(config));await writeFile('output.json', JSON.stringify(processed, null, 2));return processed;} catch (error) {// 带上下文的结构化错误日志console.error('处理失败:', {error: error.message,stack: error.stack,timestamp: new Date().toISOString()});throw error;}}
这种模式将并行执行与全面的错误处理结合在一起。Promise.all() 确保互不依赖的操作能够并发运行,而 try/catch 则提供了一个集中化的错误处理点,并能记录丰富的上下文信息。
使用 AsyncIterator 的现代事件处理
事件驱动编程已经不再局限于简单的事件监听器。AsyncIterator 提供了一种更强大的方式来处理事件流:
import { EventEmitter } from 'node:events';class DataProcessor extends EventEmitter {async *processStream() {for (let i = 0; i < 10; i++) {this.emit('data', `chunk-${i}`);yield `processed-${i}`;// 模拟异步处理时间await new Promise(resolve => setTimeout(resolve, 100));}this.emit('end');}}// 以 AsyncIterator 的方式消费事件const processor = new DataProcessor();for await (const result of processor.processStream()) {console.log('已处理:', result);}
这种方式的强大之处在于,它将事件的灵活性与异步迭代的控制流结合在一起。你可以顺序处理事件,自然地应对背压问题,并且能够干净地跳出处理循环。
5. 高级流处理与 Web 标准集成
流依然是 Node.js 最强大的特性之一,但它已经演进为更加符合 Web 标准,并提供了更好的互操作性。
现代流处理
随着更直观的 API 和更清晰的模式,流处理变得更加易用:
import { Readable, Transform } from 'node:stream';import { pipeline } from 'node:stream/promises';import { createReadStream, createWriteStream } from 'node:fs';// 使用简洁且专注的逻辑创建转换流const upperCaseTransform = new Transform({objectMode: true,transform(chunk, encoding, callback) {this.push(chunk.toString().toUpperCase());callback();}});// 使用健壮的错误处理来处理文件async function processFile(inputFile, outputFile) {try {await pipeline(createReadStream(inputFile),upperCaseTransform,createWriteStream(outputFile));console.log('文件处理成功');} catch (error) {console.error('管道处理失败:', error);throw error;}}
pipeline 函数结合 Promise 提供了自动清理和错误处理机制,消除了传统流处理中的许多痛点。
Web Streams 互操作性
现代 Node.js 可以与 Web Streams 无缝协作,从而实现与浏览器代码和边缘运行时环境的更好兼容性:
// 创建一个 Web Stream(兼容浏览器)const webReadable = new ReadableStream({start(controller) {controller.enqueue('Hello ');controller.enqueue('World!');controller.close();}});// 在 Web Streams 和 Node.js streams 之间进行转换const nodeStream = Readable.fromWeb(webReadable);const backToWeb = Readable.toWeb(nodeStream);
这种互操作性对需要在多个环境中运行或在服务端与客户端之间共享代码的应用来说至关重要。
6. Worker Threads:CPU 密集型任务的真正并行
JavaScript 的单线程特性并不总是适合 CPU 密集型任务。Worker threads 提供了一种有效利用多核的方式,同时保持了 JavaScript 的简洁性。
无阻塞的后台处理
Worker threads 非常适合那些计算开销巨大的任务,否则这些任务可能会阻塞主事件循环:
// worker.js - 隔离的计算环境import { parentPort, workerData } from 'node:worker_threads';function fibonacci(n) {if (n < 2) return n;return fibonacci(n - 1) + fibonacci(n - 2);}const result = fibonacci(workerData.number);parentPort.postMessage(result);
主应用可以将繁重的计算委托给工作线程,而不会阻塞其他操作:
// main.js - 非阻塞的委托import { Worker } from 'node:worker_threads';import { fileURLToPath } from 'node:url';async function calculateFibonacci(number) {return new Promise((resolve, reject) => {const worker = new Worker(fileURLToPath(new URL('./worker.js', import.meta.url)),{ workerData: { number } });worker.on('message', resolve);worker.on('error', reject);worker.on('exit', (code) => {if (code !== 0) {reject(new Error(`Worker 停止,退出码为 ${code}`));}});});}// 主应用保持响应console.log('开始计算...');const result = await calculateFibonacci(40);console.log('Fibonacci 结果:', result);console.log('整个过程中应用始终保持响应!');
这种模式让应用能够利用多核 CPU 的性能,同时依然保持熟悉的 async/await 编程模型。
7. 增强的开发体验
现代 Node.js 更加重视开发者体验,许多过去需要依赖外部包或复杂配置才能实现的功能,如今已经内置。
Watch 模式与环境管理
借助内置的 watch 模式和环境文件支持,开发工作流得到了显著简化:
{"name": "modern-node-app","type": "module","engines": {"node": ">=20.0.0"},"scripts": {"dev": "node --watch --env-file=.env app.js","test": "node --test --watch","start": "node app.js"}}
--watch 标志消除了对 nodemon 的需求,而 --env-file 则让你无需依赖 dotenv。这样一来,开发环境变得更加简洁高效:
// 使用 --env-file 自动加载的 .env 文件// DATABASE_URL=postgres://localhost:5432/mydb// API_KEY=secret123// app.js - 环境变量可立即使用console.log('连接到:', process.env.DATABASE_URL);console.log('API Key 已加载:', process.env.API_KEY ? 'Yes' : 'No');
这些特性通过减少配置开销和避免频繁重启,让开发体验更加轻松。
8. 现代化的安全与性能监控
安全性与性能如今已成为一等公民,Node.js 内置了用于监控和控制应用行为的工具。
权限模型:增强安全性
实验性的权限模型允许你限制应用的访问范围,遵循最小权限原则:
# 以受限的文件系统访问权限运行node --experimental-permission --allow-fs-read=./data --allow-fs-write=./logs app.js# 网络访问限制node --experimental-permission --allow-net=api.example.com app.js# 上面的 allow-net 功能目前尚不可用,该 PR 已合并到 node.js 仓库,# 将在未来的版本中提供
这对于需要处理不受信任代码或需要满足安全合规要求的应用来说尤其有价值。
内置性能监控
性能监控现在已经集成到平台中,对于基础监控无需再依赖外部 APM 工具:
import { PerformanceObserver, performance } from 'node:perf_hooks';// 设置自动性能监控const obs = new PerformanceObserver((list) => {for (const entry of list.getEntries()) {if (entry.duration > 100) { // 记录慢操作console.log(`检测到慢操作: ${entry.name} 耗时 ${entry.duration}ms`);}}});obs.observe({ entryTypes: ['function', 'http', 'dns'] });// 为自定义操作打点async function processLargeDataset(data) {performance.mark('processing-start');const result = await heavyProcessing(data);performance.mark('processing-end');performance.measure('data-processing', 'processing-start', 'processing-end');return result;}
这样一来,你无需依赖外部工具就能洞察应用的性能,从而在开发早期就发现瓶颈。
9. 应用分发与部署
现代 Node.js 通过单文件可执行应用和改进的打包方式,让应用分发变得更加简单。
单文件可执行应用
你现在可以将 Node.js 应用打包成一个单独的可执行文件,从而简化部署与分发:
# 创建一个自包含的可执行文件node --experimental-sea-config sea-config.json
配置文件定义了应用的打包方式:
{"main": "app.js","output": "my-app-bundle.blob","disableExperimentalSEAWarning": true}
这对 CLI 工具、桌面应用,或任何希望分发时无需用户单独安装 Node.js 的场景来说都特别有价值。
10. 现代化的错误处理与诊断
错误处理已经从简单的 try/catch 块发展为结构化的错误处理和全面的诊断能力。
结构化错误处理
现代应用能够从结构化、带上下文的错误处理方式中获益,从而提供更好的调试信息:
class AppError extends Error {constructor(message, code, statusCode = 500, context = {}) {super(message);this.name = 'AppError';this.code = code;this.statusCode = statusCode;this.context = context;this.timestamp = new Date().toISOString();}toJSON() {return {name: this.name,message: this.message,code: this.code,statusCode: this.statusCode,context: this.context,timestamp: this.timestamp,stack: this.stack};}}// 带丰富上下文的使用示例throw new AppError('数据库连接失败','DB_CONNECTION_ERROR',503,{ host: 'localhost', port: 5432, retryAttempt: 3 });
这种方式为调试和监控提供了更丰富的错误信息,同时还能在整个应用中保持一致的错误接口。
高级诊断
Node.js 内置了强大的诊断功能,帮助你深入了解应用内部的运行情况:
import diagnostics_channel from 'node:diagnostics_channel';// 创建自定义诊断通道const dbChannel = diagnostics_channel.channel('app:database');const httpChannel = diagnostics_channel.channel('app:http');// 订阅诊断事件dbChannel.subscribe((message) => {console.log('数据库操作:', {operation: message.operation,duration: message.duration,query: message.query});});// 发布诊断信息async function queryDatabase(sql, params) {const start = performance.now();try {const result = await db.query(sql, params);dbChannel.publish({operation: 'query',sql,params,duration: performance.now() - start,success: true});return result;} catch (error) {dbChannel.publish({operation: 'query',sql,params,duration: performance.now() - start,success: false,error: error.message});throw error;}}
这些诊断信息可以被监控工具消费、记录下来进行分析,或者用于触发自动化修复操作。
11. 现代化的包管理与模块解析
包管理和模块解析变得更加智能,支持更好的 monorepo 管理、内部包处理,以及更灵活的模块解析方式。
Import Maps 与内部包解析
现代 Node.js 支持 import maps,使你能够创建简洁的内部模块引用:
{"imports": {"#config": "./src/config/index.js","#utils/*": "./src/utils/*.js","#db": "./src/database/connection.js"}}
这样可以为内部模块创建一个简洁且稳定的接口:
// 简洁的内部导入,即使重构项目结构也不会出问题import config from '#config';import { logger, validator } from '#utils/common';import db from '#db';
这些内部导入让重构更加轻松,同时清晰地区分了内部依赖与外部依赖。
动态导入:灵活的加载方式
动态导入支持更复杂的加载模式,包括条件加载和代码分割:
// 根据配置或环境加载功能async function loadDatabaseAdapter() {const dbType = process.env.DATABASE_TYPE || 'sqlite';try {const adapter = await import(`#db/adapters/${dbType}`);return adapter.default;} catch (error) {console.warn(`数据库适配器 ${dbType} 不可用,回退到 sqlite`);const fallback = await import('#db/adapters/sqlite');return fallback.default;}}// 条件加载功能模块async function loadOptionalFeatures() {const features = [];if (process.env.ENABLE_ANALYTICS === 'true') {const analytics = await import('#features/analytics');features.push(analytics.default);}if (process.env.ENABLE_MONITORING === 'true') {const monitoring = await import('#features/monitoring');features.push(monitoring.default);}return features;}
这种模式使应用能够根据所处环境进行自适应,并且只加载真正需要的代码。
前行之路:现代 Node.js 的关键要点(2025)
回顾当前的 Node.js 开发现状,可以总结出以下几个核心原则:
拥抱 Web 标准:使用
node:前缀、Fetch API、AbortController 和 Web Streams,以获得更好的兼容性并减少依赖。充分利用内置工具:测试运行器、watch 模式和环境文件支持能减少外部依赖与配置复杂度。
采用现代异步模式思维:顶层 await、结构化错误处理和 async iterator 让代码更具可读性和可维护性。
战略性地使用 Worker Threads:对于 CPU 密集型任务,Worker Threads 提供真正的并行能力,而不会阻塞主线程。
拥抱渐进增强:通过权限模型、诊断通道和性能监控构建健壮、可观测的应用。
优化开发者体验:watch 模式、内置测试和 import maps 打造更高效、更愉快的开发流程。
规划分发方式:单文件可执行应用和现代打包方式让部署更加简单。
Node.js 从一个简单的 JavaScript 运行时演变为一个全面的开发平台,这一转变令人瞩目。采用这些现代模式,你不仅是在编写现代化代码,更是在构建更具可维护性、更高性能,并且更契合 JavaScript 生态的应用。
现代 Node.js 的美妙之处在于,它在不断进化的同时,依然保持了向后兼容性。你可以逐步采用这些模式,它们能够与现有代码共存。无论是开启新项目,还是对现有项目进行现代化改造,这些模式都能为你提供清晰的路径,助力构建更健壮、更愉快的 Node.js 开发体验。
随着 2025 的推进,Node.js 仍在不断演进,但本文探讨的这些基础模式,为构建长期保持现代化和可维护性的应用奠定了坚实的基础。
原文地址:https://kashw1n.com/blog/nodejs-2025/
翻译:谢杰
审校:谢杰
---END---
推荐阅读:
【收藏必备】8 张脑图快速了解 Vue 组件
新搭建一个 Vue 项目后,我有了这 15 点思考 从 0 到 1 搭建一个企业级前端开发规范
![]()
最近面试BAT,整理一份面试资料《前端面试BAT通关手册》,覆盖了前端技术、CSS、JavaScript、框架、 数据库、数据结构等等。 获取方式:关注公众号并回复 前端 领取,更多内容陆续奉上。 明天见(。・ω・。)ノ♡

