Skip to content

[译] Node.js 2025年的现代模式

原文地址:https://kashw1n.com/blog/nodejs-2025/

Node.js 自诞生以来经历了惊人的蜕变。若你已使用 Node.js 编程多年,想必亲历了这场变革——从充斥回调函数、CommonJS 主导的开发环境,到如今基于标准的简洁开发体验。

这些变化绝非表面功夫,而是我们处理服务器端 JavaScript 开发方式的根本性转变。现代 Node.js 拥抱 Web 标准,减少外部依赖,并提供更直观的开发者体验。让我们深入探讨这些变革,并了解它们为何对 2025 年的应用程序至关重要。

1. 模块系统:ESM 成为新的标准

模块系统或许是你能感受到最大变化的地方。CommonJS 曾很好地服务于我们,但 ES Modules(ESM)已成为不争的赢家,它提供了更完善的工具支持,并与 Web 标准更紧密地契合。

过去方式(CommonJS)

让我们回顾过去如何组织模块。这种方式需要显式导出和同步导入:

js
// 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 优化,且不符合浏览器标准。

现代方式(ES Modules 搭配 node: 前缀)

现代 Node.js 开发采用 ES Modules,并加入关键特性——为内置模块添加 node: 前缀。这种显式命名避免了混淆,使依赖关系清晰明确:

js
// math.js
export function add(a, b) {
  return a + b;
}

// app.js
import { add } from './math.js';
import { readFile } from 'node:fs/promises';  // Modern node: prefix
import { createServer } from 'node:http';

console.log(add(2, 3));

node: 不仅是一种约定,更是向开发者和工具传递的明确信号:你导入的是 Node.js 内置模块而非 npm 包。这既能避免潜在冲突,又能让代码更清晰地表明其依赖关系。

顶层 await:简化初始化

最具颠覆性的特性之一是顶层 await。无需再将整个应用程序包裹在异步函数中,即可在模块级别使用 await:

js
// app.js - 无需包装函数的简洁初始化
import { readFile } from 'node:fs/promises';

const config = JSON.parse(await readFile('config.json', 'utf8'));
const server = createServer(/* ... */);

console.log('App started with config:', config.appName);

这消除了我们曾经随处可见的立即调用异步函数表达式(IIFE)的常见模式。你的代码变得更线性,更易于理解。

2. 内置 Web API:减少外部依赖

Node.js 大力拥抱了 Web 标准,将 Web 开发者熟悉的 API 直接引入运行时环境。这意味着更少的依赖项和跨环境更高的兼容性。

Fetch API:告别 HTTP 库依赖

还记得每个项目都需要 axios、node-fetch 或类似库来处理 HTTP 请求的日子吗?那些日子已经一去不复返了。Node.js 现在原生支持 Fetch API:

js
// 过去方式:需要引入外部依赖
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();

但 Fetch 这种现代方式不仅限于替换 HTTP 库,它还内置了复杂的超时和取消支持:

js
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('Request timed out');
    }
    throw error;
  }
}

此方法消除了对超时库的需求,并提供了统一的错误处理体验。其中 AbortSignal.timeout() 方法尤为优雅——它创建的信号会在指定时间后自动中止。

AbortController:优雅的操作取消

现代应用程序需要优雅地处理操作取消,无论是用户主动取消还是因超时导致。AbortController 提供了一种标准化的操作取消方式:

js
// 长时间运行操作取消的简洁方式
const controller = new AbortController();

// 建立自动的取消
setTimeout(() => controller.abort(), 10000);

try {
  const data = await fetch('https://slow-api.com/data', {
    signal: controller.signal
  });
  console.log('Data received:', data);
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('Request was cancelled - this is expected behavior');
  } else {
    console.error('Unexpected error:', error);
  }
}

该模式适用于多种 Node.js API,不仅限于 fetch。你可以在文件操作、数据库查询以及任何支持取消的异步操作中使用同样的 AbortController。

3. 内置测试:无需外部依赖的专业测试

测试曾需要在 Jest、Mocha、Ava 等框架中做出选择。如今 Node.js 内置了功能完整的测试运行器,无需任何外部依赖即可满足大多数测试需求。

基于 Node.js 内置测试运行器的现代测试

内置测试运行器提供简洁、熟悉的API,兼具现代感与完整性:

js
// test/math.test.js
import { test, describe } from 'node:test';
import assert from 'node:assert';
import { add, multiply } from '../math.js';

describe('Math functions', () => {
  test('adds numbers correctly', () => {
    assert.strictEqual(add(2, 3), 5);
  });

  test('handles async operations', async () => {
    const result = await multiply(2, 3);
    assert.strictEqual(result, 6);
  });

  test('throws on invalid input', () => {
    assert.throws(() => add('a', 'b'), /Invalid input/);
  });
});

其强大之处在于能与 Node.js 开发工作流无缝集成:

bash
# 使用内置运行器执行所有测试
node --test

# 开发监听模式
node --test --watch

# 覆盖率报告(Node.js 20+)
node --test --experimental-test-coverage

监听模式在开发过程中尤为重要——当你修改代码时,测试会自动重新运行,无需额外配置即可获得即时反馈。

4. 复杂的异步模式

虽然 async/await 并非新概念,但围绕它的编程模式已显著成熟。现代 Node.js 开发更高效地运用这些模式,并将其与新型 API 相结合。

增强型错误处理下的 async/await

现代错误处理将 async/await 与复杂的错误恢复机制及并行执行模式相结合:

js
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('Processing failed:', {
      error: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString()
    });
    throw error;
  }
}

该模式将并行执行与全面的错误处理相结合。Promise.all() 确保独立操作并行运行,而 try/catch 则提供了一个具备丰富上下文的集中式错误处理点。

基于异步迭代器的现代事件处理

事件驱动编程已超越简单的事件监听器。异步迭代器为处理事件流提供了更强大的方式:

js
import { EventEmitter, once } 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');
  }
}

// 消费事件异步迭代器
const processor = new DataProcessor();
for await (const result of processor.processStream()) {
  console.log('Processed:', result);
}

这种方法之所以特别强大,是因为它将事件的灵活性与异步迭代的控制流相结合。你可以顺序处理事件,自然地处理背压,并干净利落地跳出处理循环。

5. 基于Web标准集成的先进流技术

流仍是 Node.js 最强大的特性之一,但它们已进化以兼容 Web 标准并提供更佳的互操作性。

现代流处理

流处理通过更完善的 API 和更清晰的模式变得更直观:

js
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('File processed successfully');
  } catch (error) {
    console.error('Pipeline failed:', error);
    throw error;
  }
}

基于 Promise 的 pipeline 函数提供了自动清理和错误处理功能,消除了流处理中许多传统痛点。

Web Streams 互操作性

现代 Node.js 可与 Web Streams 无缝协作,从而实现与浏览器代码及边缘运行时环境更佳的兼容性:

js
// 创建 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. 工作线程:CPU密集型任务的真正并行性

JavaScript 的单线程特性并不总是适用于 CPU 密集型工作。工作线程提供了一种在保持 JavaScript 简单性的同时有效利用多核处理器的方式。

无阻塞的后台处理

对于那些会阻塞主事件循环的计算密集型任务,工作线程堪称完美解决方案:

js
// 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);

主应用程序可将繁重的计算任务委托出去,同时不阻塞其他操作:

js
// 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 stopped with exit code ${code}`));
      }
    });
  });
}

// 主程序保持正常响应
console.log('Starting calculation...');
const result = await calculateFibonacci(40);
console.log('Fibonacci result:', result);
console.log('Application remained responsive throughout!');

该模式使你的应用程序能够利用多个CPU核心,同时保持熟悉的 async/await 编程模型。

7. 增强的开发体验

现代 Node.js 通过内置工具将开发者的体验放在首位,这些工具此前需要外部包或复杂配置才能实现。

监听模式与环境管理

内置的监听模式和环境文件支持显著简化了开发工作流:

json
{
  "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 的依赖。你的开发环境因此变得更简单、更快速:

js
// .env 文件会通过 --env-file 标志自动加载进来
// DATABASE_URL=postgres://localhost:5432/mydb
// API_KEY=secret123

// app.js - 环境变量即时可用
console.log('Connecting to:', process.env.DATABASE_URL);
console.log('API Key loaded:', process.env.API_KEY ? 'Yes' : 'No');

这些特性通过减少配置开销并消除重启循环,使开发过程更加愉快。

8. 现代安全性和性能监控

安全性与性能已成为首要关注点,内置工具可用于监控和控制应用程序行为。

增强安全性的权限模型

实验性权限模型遵循最小权限原则,允许你限制应用程序的访问权限:

bash
# 以受限的文件系统访问权限运行
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 仓库,将在未来版本中提供

这对于处理不可信代码或需要证明安全合规性的应用程序尤为重要。

内置性能监控

性能监控现已集成到平台中,简单的监控无需外部应用性能管理工具:

js
import { PerformanceObserver, performance } from 'node:perf_hooks';

// 创建自动化的性能监控
const obs = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 100) { // 打印缓慢操作
      console.log(`Slow operation detected: ${entry.name} took ${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 应用程序打包成单个可执行文件,从而简化部署和分发流程:

bash
# 创建一个独立的可执行文件
node --experimental-sea-config sea-config.json

配置文件定义了应用程序的打包方式:

json
{
  "main": "app.js",
  "output": "my-app-bundle.blob",
  "disableExperimentalSEAWarning": true
}

这对于命令行工具、桌面应用程序或任何需要分发应用程序而无需用户单独安装 Node.js 的场景尤为重要。

10. 现代错误处理与诊断

错误处理已超越简单的 try/catch 代码块,发展为包含结构化错误处理和全面诊断机制的体系。

结构化错误处理

现代应用程序受益于结构化、基于上下文的错误处理机制,该机制能提供更优质的调试信息:

js
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(
  'Database connection failed',
  'DB_CONNECTION_ERROR',
  503,
  { host: 'localhost', port: 5432, retryAttempt: 3 }
);

此方法在为调试和监控提供更丰富的错误信息的同时,还能在整个应用程序中保持一致的错误接口。

高级诊断功能

Node.js 包含强大的诊断功能,可帮助你了解应用程序内部的运行状况:

js
import diagnostics_channel from 'node:diagnostics_channel';

// 创建自定义诊断 channel
const dbChannel = diagnostics_channel.channel('app:database');
const httpChannel = diagnostics_channel.channel('app:http');

// 订阅诊断事件
dbChannel.subscribe((message) => {
  console.log('Database operation:', {
    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. 现代包管理与模块解析

包管理和模块解析功能日益完善,对单仓库、内部包以及灵活模块解析的支持也更加强大。

导入映射与内部包解析

现代 Node.js 支持导入映射,可创建简洁的内部模块引用:

json
{
  "imports": {
    "#config": "./src/config/index.js",
    "#utils/*": "./src/utils/*.js",
    "#db": "./src/database/connection.js"
  }
}

这些内部导入使重构更容易,并清晰区分了内部与外部依赖关系。

动态导入实现灵活加载

动态导入支持复杂的加载模式,包括条件加载和代码拆分:

js
// 基于配置或环境加载功能
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(`Database adapter ${dbType} not available, falling back to 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 的发展现状,有若干关键原则需要留意下:

  1. 拥抱 Web 标准:采用 node: 前缀、fetch API、AbortController 和 Web Streams 提升兼容性并减少依赖
  2. 善用内置工具:测试运行器、监听模式及环境文件支持可降低外部依赖与配置复杂度
  3. 采用现代异步模式:顶层 await、结构化错误处理和异步迭代器使代码更易读易维护
  4. 策略性使用工作线程:对于 CPU 密集型任务,工作线程提供真正并行处理能力且不阻塞主线程
  5. 践行渐进增强:运用权限模型、诊断通道和性能监控构建健壮可观测的应用程序
  6. 优化开发者体验:监听模式、内置测试与导入映射(import maps)构建了更愉悦的开发流程
  7. 部署规划:单一可执行应用与现代打包方案简化了部署流程

Node.js 从简单 JavaScript 运行时蜕变为综合开发平台的历程令人惊叹。采用这些现代模式不仅意味着编写符合时代需求的代码,更意味着构建出更具可维护性、更高性能且与 JavaScript 生态系统深度融合的应用。

现代 Node.js 的精妙之处在于其在保持向后兼容性基础上实现的进化。这些模式可逐步采用,并与现有代码无缝共存。无论你是启动新项目还是改造现有项目,这些模式都为实现更稳健、更愉悦的 Node.js 开发提供了清晰路径。

迈入2025年,Node.js 仍在持续进化,但本文探讨的基础模式已为构建应用奠定坚实根基——这些应用将在未来多年保持现代化与可维护性。