2025年Node.js现代模式

自 Node.js 问世以来,它经历了令人瞩目的变革。如果你已经使用 Node.js 开发了数年,想必亲眼见证了这一演进过程——从早期以回调函数为主、CommonJS 规范主导的开发环境,到如今以标准规范为基础、代码风格更加简洁的开发体验。

这些变化不仅仅是表面上的,它们代表了我们对待服务器端 JavaScript 开发方式的根本性转变。现代 Node.js 拥抱网络标准,减少外部依赖,并提供更直观的开发者体验。让我们探索这些转变,并理解它们为何对你在 2025 年的应用程序至关重要。

1. 模块系统:ESM 是新标准

模块系统或许是您会注意到最大变化的地方。CommonJS曾为我们提供了良好的服务,但ES模块(ESM)已成为明确的赢家,提供了更好的工具支持并符合网络标准。

元素周期表

旧方式(CommonJS)

让我们看看过去如何结构化模块。这种方法需要显式导出和同步导入:

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

// app.js
const { add } = require('./math');
console.log(add(2, 3));

Copy

这种方式虽然可行,但存在局限性——无法进行静态分析、无法进行树摇动优化,且与浏览器标准不兼容。

现代方式(ES 模块搭配 Node: 前缀)

现代 Node.js 开发采用 ES 模块,并添加了一个关键特性——为内置模块添加 node: 前缀。这种显式命名可避免混淆,并让依赖关系一目了然:

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

Copy

node: 前缀不仅仅是一种约定——它是向开发者和工具明确信号,表明你正在导入 Node.js 内置模块而非 npm 包。这避免了潜在冲突,并使代码更明确地说明其依赖关系。

顶级 await:简化初始化

最具变革性的功能之一是顶级 await。无需再将整个应用程序包裹在异步函数中仅为在模块级别使用 await:

// app.js - Clean initialization without wrapper functions
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);

Copy

这消除了我们过去随处可见的立即调用异步函数表达式(IIFE)模式。您的代码变得更加线性且易于理解。

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

Node.js 大力拥抱 Web 标准,将 Web 开发者已熟悉的 API 直接集成到运行时环境中。这意味着减少依赖项并实现跨环境的一致性。

Fetch API:无需 HTTP 库依赖

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

// Old way - external dependencies required
const axios = require('axios');
const response = await axios.get('https://api.example.com/data');

// Modern way - built-in fetch with enhanced features
const response = await fetch('https://api.example.com/data');
const data = await response.json();

Copy

但现代方法不仅限于替换 HTTP 库。它还内置了高级超时和取消支持:

async function fetchData(url) {
  try {
    const response = await fetch(url, {
      signal: AbortSignal.timeout(5000) // Built-in timeout support
    });

    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;
  }
}

Copy

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

AbortController:优雅的操作取消

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

// Cancel long-running operations cleanly
const controller = new AbortController();

// Set up automatic cancellation
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);
  }
}

Copy

这种模式适用于许多 Node.js API,而不仅仅是 fetch。你可以使用相同的 AbortController 进行文件操作、数据库查询以及任何支持取消的异步操作。

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

过去进行测试需要在 Jest、Mocha、Ava 等框架中进行选择。Node.js 现已内置功能齐全的测试运行器,可满足大多数测试需求且无需任何外部依赖。

借助 Node.js 内置测试运行器进行现代测试

内置测试运行器提供了一个干净、熟悉的 API,既现代又完整:

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

Copy

其特别强大的地方在于与 Node.js 开发工作流的无缝集成:

# Run all tests with built-in runner
node --test

# Watch mode for development
node --test --watch

# Coverage reporting (Node.js 20+)
node --test --experimental-test-coverage

Copy

监听模式在开发过程中尤为有用——当您修改代码时,测试会自动重新运行,提供即时反馈,无需额外配置。

4. 复杂的异步模式

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

带增强错误处理的 async/await

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

import { readFile, writeFile } from 'node:fs/promises';

async function processData() {
  try {
    // Parallel execution of independent operations
    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) {
    // Structured error logging with context
    console.error('Processing failed:', {
      error: error.message,
      stack: error.stack,
      timestamp: new Date().toISOString()
    });
    throw error;
  }
}

Copy

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

基于 AsyncIterators 的现代事件处理

事件驱动编程已超越简单的事件监听器。AsyncIterators 提供了一种更强大的方式来处理事件流:

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}`;
      // Simulate async processing time
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    this.emit('end');
  }
}

// Consume events as an async iterator
const processor = new DataProcessor();
for await (const result of processor.processStream()) {
  console.log('Processed:', result);
}

Copy

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

5. 支持 Web 标准的高级流

流仍然是 Node.js 最强大的功能之一,但它们已经演进以支持 Web 标准并提供更好的互操作性。

现代流处理

流处理通过更直观的 API 和清晰的模式变得更加易用:

import { Readable, Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';

// Create transform streams with clean, focused logic
const upperCaseTransform = new Transform({
  objectMode: true,
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
});

// Process files with robust error handling
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;
  }
}

Copy

pipeline 函数通过承诺机制提供自动清理和错误处理,消除了传统流处理中的许多痛点。

Web 流互操作性

现代 Node.js 可无缝与 Web 流配合使用,实现与浏览器代码及边缘运行时环境的更好兼容性:

// Create a Web Stream (compatible with browsers)
const webReadable = new ReadableStream({
  start(controller) {
    controller.enqueue('Hello ');
    controller.enqueue('World!');
    controller.close();
  }
});

// Convert between Web Streams and Node.js streams
const nodeStream = Readable.fromWeb(webReadable);
const backToWeb = Readable.toWeb(nodeStream);

Copy

这种互操作性对于需要在多个环境中运行或在服务器和客户端之间共享代码的应用程序至关重要。

6. 工作线程:CPU 密集型任务的真正并行处理

JavaScript 的单线程特性并不总是适合 CPU 密集型工作。工作线程提供了一种有效利用多个核心的方法,同时保持 JavaScript 的简单性。

背景处理而不阻塞

工作者线程非常适合那些否则会阻塞主事件循环的计算密集型任务:

// worker.js - Isolated computation environment
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);

Copy

主应用程序可以将重计算任务委托给工作者线程,而不会阻塞其他操作:

// main.js - Non-blocking delegation
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}`));
      }
    });
  });
}

// Your main application remains responsive
console.log('Starting calculation...');
const result = await calculateFibonacci(40);
console.log('Fibonacci result:', result);
console.log('Application remained responsive throughout!');

Copy

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

7. 增强的开发体验

现代 Node.js 通过内置工具优先考虑开发者体验,这些工具此前需要外部包或复杂配置。

监视模式与环境管理

开发工作流程通过内置监视模式和环境文件支持得到显著简化:

{
  "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"
  }
}

Copy

--watch 标志消除了对 nodemon 的需求,而 --env-file 消除了对 dotenv 的依赖。您的开发环境变得更加简单和快速:

// .env file automatically loaded with --env-file
// DATABASE_URL=postgres://localhost:5432/mydb
// API_KEY=secret123

// app.js - Environment variables available immediately
console.log('Connecting to:', process.env.DATABASE_URL);
console.log('API Key loaded:', process.env.API_KEY ? 'Yes' : 'No');

Copy

这些功能通过减少配置开销和消除重启循环,使开发更加愉快。

8. 现代安全与性能监控

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

增强安全性的权限模型

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

# Run with restricted file system access
node --experimental-permission --allow-fs-read=./data --allow-fs-write=./logs app.js

# Network restrictions 
node --experimental-permission --allow-net=api.example.com app.js
# Above allow-net feature not avaiable yet, PR merged in node.js repo, will be available in future release

Copy

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

内置性能监控

性能监控现已集成到平台中,无需外部 APM 工具即可进行基本监控:

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

// Set up automatic performance monitoring
const obs = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 100) { // Log slow operations
      console.log(`Slow operation detected: ${entry.name} took ${entry.duration}ms`);
    }
  }
});
obs.observe({ entryTypes: ['function', 'http', 'dns'] });

// Instrument your own operations
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;
}

Copy

这提供了应用程序性能的可视性,无需外部依赖,帮助您在开发早期识别瓶颈。

9. 应用程序分发与部署

现代 Node.js 通过单一可执行应用程序和改进的打包功能,使应用程序分发更加简单。

单一可执行应用程序

您现在可以将 Node.js 应用程序打包为单一可执行文件,简化部署和分发:

# Create a self-contained executable
node --experimental-sea-config sea-config.json

Copy

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

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

Copy

这对于命令行工具、桌面应用程序或任何无需用户单独安装 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
    };
  }
}

// Usage with rich context
throw new AppError(
  'Database connection failed',
  'DB_CONNECTION_ERROR',
  503,
  { host: 'localhost', port: 5432, retryAttempt: 3 }
);

Copy

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

高级诊断

Node.js 包含高级诊断功能,帮助您了解应用程序内部的运行情况:

import diagnostics_channel from 'node:diagnostics_channel';

// Create custom diagnostic channels
const dbChannel = diagnostics_channel.channel('app:database');
const httpChannel = diagnostics_channel.channel('app:http');

// Subscribe to diagnostic events
dbChannel.subscribe((message) => {
  console.log('Database operation:', {
    operation: message.operation,
    duration: message.duration,
    query: message.query
  });
});

// Publish diagnostic information
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;
  }
}

Copy

这些诊断信息可被监控工具消费、记录以供分析,或用于触发自动修复操作。

11. 现代包管理与模块解析

包管理和模块解析已变得更加复杂,对单仓库、内部包和灵活的模块解析提供了更好的支持。

导入映射和内部包解析

现代 Node.js 支持导入映射,允许您创建干净的内部模块引用:

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

Copy

这为内部模块创建了一个干净、稳定的接口:

// Clean internal imports that don't break when you reorganize
import config from '#config';
import { logger, validator } from '#utils/common';
import db from '#db';

Copy

这些内部导入使重构更加容易,并明确区分了内部和外部依赖关系。

动态导入实现灵活加载

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

// Load features based on configuration or environment
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;
  }
}

// Conditional feature loading
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;
}

Copy

此模式允许您构建能够适应环境并仅加载实际所需代码的应用程序。

未来方向:现代 Node.js 的关键要点(2025)

在审视当前 Node.js 开发状态时,以下关键原则浮现:

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

Node.js 从一个简单的 JavaScript 运行时演变为一个全面的开发平台,这一转变令人惊叹。通过采用这些现代模式,您不仅在编写当代代码,更在构建更易于维护、性能更优且与更广泛的 JavaScript 生态系统相兼容的应用程序。

现代 Node.js 的魅力在于其在保持向后兼容性的同时不断演进。您可以逐步采用这些模式,它们可与现有代码无缝协作。无论您是启动新项目还是现代化现有项目,这些模式都为实现更健壮、更愉悦的 Node.js 开发提供了明确路径。

随着我们迈向 2025 年,Node.js 持续演进,但本文探讨的基础模式为构建未来数年仍能保持现代性和可维护性的应用程序奠定了坚实基础。

本文文字及图片出自 Modern Node.js Patterns for 2025

共有 411 条评论

  1. 哇,我不知道有这个:

      # 以受限文件系统访问权限运行
      node --experimental-permission 
        --allow-fs-read=./data --allow-fs-write=./logs app.js
      
      # 网络限制
      node --experimental-permission 
        --allow-net=api.example.com app.js
    

    看来他们受到了Deno的启发。这是一个很棒的功能。https://docs.deno.com/runtime/fundamentals/security/#permiss

    • 我非常不喜欢在运行时或应用程序中看到这样的功能。

      解决这个问题“正确”的地方是在操作系统中。那里已经解决了这个问题,包括所有不可避免的边角案例。

      为什么要重新发明这个轮子,给你的项目增加复杂性、错误表面、维护负担等等?它解决了什么问题,而其他人还没有解决?

      • 多年来,我听说使用cron更好,因为这个问题已经以正确的方式(tm)解决了。我使用cron的经历是,在生产环境中遇到大约十几次难以解决的问题,比如cron没有运行/没有正确的权限/错误丢失而没有被记录/等等。更改/升级操作系统成为了一个问题。我后来切换到一个带有基本调度器的简单Node脚本,7年来我没有遇到任何问题。我的开发人员可以愉快地在调度程序中添加条目,而无需打扰我。我们甚至添加了一致性检查、断言、计划一次性执行任务,… 以及现在多服务器调度。

        需要以特定方式配置操作系统的部署非常困难(Docker、Kubernetes、Snap 的存在就是这种困难的症状)。这样做需要较高的权限。升级和回滚操作极具挑战性,甚至难以实现。当跨多台硬件时,操作系统有时无法提供解决方案。

        如果“npm start”能将权限限制在代码版本所需的范围内,我将使用它并感到满意。

        • 如果 cron 对你来说有问题,那么逻辑上的解决方案是用其他能正常工作的东西替换它。但要在正确的位置和抽象层级上进行替换。这几乎从来不是在运行时或应用程序中。

          专注于一件事(并做好它)。

          一个专门的领域特定调度微服务?众多 cron 替代方案之一?众多“SaaS cron”之一?Systemd?

          这个问题已经解决。边角案例已优化。可自由使用。

          同样适用于环境变量作为配置(而非发明又一个配置方案)、文件权限、监控、网络、沙箱、chroot 等。我不得不绕过的那些在操作系统中处理的、破损的、不安全的或只是低效的 DIY 版本的数量令人震惊。这会造成双重损失:构建它所需的时间。这段时间本可以用于业务领域,而未来十五年维护和调试它所需的时间。

          • 我更倾向于将配置集中化。与其配置两件事,这让我只需配置一件事。我愿意在这里做出这种权衡。

      • 这是一个好主意,但当操作系统工具不够完善时该怎么办?macOS就是一个例子,它有操作系统级别的沙箱功能[0],但文档几乎不存在,唯一能弄清楚的方法就是阅读那些在你之前与之抗争的人写的博客文章。将它集成到Node中意味着至少在理论上,你在每个操作系统上都能得到相同的结果。

        [0] https://www.karltarvas.com/macos-app-sandboxing-via-sandbox-

      • 不过,操作系统实际上并未解决这个问题。任何可运行的程序都能访问你的任意文件,而要控制这种访问权限非常困难,即使你想限制自己软件的影响范围。说真的——你使用的软件是如何工作的?去编写eBPF作为一个小型临时hypervisor,通过seLinux强制执行难以编写的策略?这只有在你作为机器的管理员时才有效,而管理员并不一定是编写软件的人,他们希望以防御性方式编写代码。

        此外,现代软件安全正致力于加强软件对抗供应链漏洞的能力。这更像是一种能力模型,而非传统操作系统模型。在该模型中,你从一组有限权限开始,即使在同一地址空间内,也难以获得新权限,除非你明确获得该权限的句柄(从某种意义上说,所有权限都应遵循这种自上而下的工作方式)。

        • “任何你可以运行的程序”。但你不应以你自己的身份运行任何程序。程序可以以独立的受限用户身份运行。

        • 你需要在生活中使用FreeBSD的Capsicum。它就像你描述的那样。

        • 这就是进程挂载命名空间的作用。各种容器实现都会使用它。在现代 Linux 系统中,你甚至不需要第三方容器管理器,systemd-nspawn 随系统自带,应该能够实现这一功能。

          Node.js 中所谓的“解决方案”存在的问题在于,Node.js 无法决定例如域名解析的方式。因此,很容易欺骗它允许或拒绝访问作者未预期的内容。

          从历史上看,我们(计算机用户)决定由操作系统负责域名解析。虽然今天操作系统可能做得不够好,但从原则上讲,我们希望世界是这样的:操作系统负责 DNS,而不是每个程序。从管理员的角度来看,这可以让管理员无需学习每个想要做类似事情的程序的能力、限制和语法。

          日志处理也是类似的情况。从管理员的角度来看,日志应始终输出到标准错误(stderr)。那些试图绕过此规则、将日志写入单独文件或发送到套接字等操作,会让任何有过管理经验的管理员感到头疼。

          命名空间也是如此。让Linux自行处理即可。无需在单个程序或运行时环境中重复实现。

      • 如何以原生方式实现这一点?我的意思是,我相信你(应该是chroot监狱吧?),但并非所有人都运行在*nix系统上,更重要的是,并非所有Node开发者都了解或希望了解底层操作系统。当然,这对他们不利,但很多人被困在自己的生态系统中。在Java生态系统中,这种情况可能更糟,但这被视为卖点(在JVM上“写一次,到处运行”等)。

        • > 你会如何以原生方式实现这一点?

          我不知道GP会如何做,但我会将一个服务(用Go编写的Web应用)在特定用户下运行,并限制该用户在文件系统上的读写权限。

          至于网络方面,那又是另一个问题。

        • > 但并非所有人都使用*nix系统

          指的是Windows吗?它在操作系统层面也有经过充分测试且可靠的文件系统权限机制。

          > 并非所有Node开发者都了解或愿意深入了解底层操作系统

          问题是,他们可能也不愿意去理解这一特性,更不会编写与之兼容的代码。

          如果他们某天想认真对待系统权限,会发现直接与操作系统协作要容易得多。

          • 所以每个应用程序都需要单独的用户账户?

            仅在本地环境下,这似乎是个巨大的麻烦……至少可以建议使用容器,其界面通常更友好。

          • 我不知道Windows有这个功能,有人能解释一下吗?

      • > 解决这个问题“正确”的地方,是在操作系统层面。

        这是我对使用 dotenv 库的看法。应用程序不应该加载环境变量,只需要读取它们。使用类似 omz 中的 dotenv 函数/插件要好得多。

        • 完全同意。

          不过常被提及的反对意见是“但Windows系统”。若Windows缺乏环境变量支持(或Cron、chroot等功能),解决方案应是迁移至支持这些功能的环境,或为Windows用户专门引入工具。

          而非构建一个复杂的分层目录扫描器,用于查找并合并各类.env、.env.local等文件。

          在开发环境中,我确实会使用 .ENV 文件,但会通过 zenv 或 loadenv 工具/脚本(位于项目代码库之外)将这些文件加载到环境中。

          • 我使用 .env 文件,并有一个小型的 Bash 脚本在开发时将它们加载到环境中,或在部署时加载到 K8s 配置映射中。

        • 在团队环境中,将环境/配置加载逻辑直接集成到仓库中非常有用。这并不意味着必须由应用程序进程加载,但可以作为代码库中周边工具的一部分。

          • 是的,我认为这是正确的位置:利用或简化操作系统功能的临时工具。

            例如 xenv、一个小型 Bash 脚本、一个 Makefile 等工具,开发人员可以根据需要替换为自己的版本(Windows 用户可能需要与我的 Zsh 内置命令不同的内容)。这些工具在生产环境中不存在,也不会在本地运行 Kubernetes 或 Docker Compose 时出现。

            几年前,我在一个集成的.env加载器中发现了一个安全漏洞,该加载器部分依赖于一个库,部分是自定义/DIY代码。一位开发者构建了一个能够在文件层次结构中上下遍历以搜索.env.*文件的工具,并在运行时合并这些文件,如果发现新的或修改过的文件,则重新加载应用程序。这对开发者来说很有用。但在生产环境中,上传一个.env.png文件会导致该文件被临时目录中的自制工具拾取。是的,任何互联网用户都可以将大部分配置注入我们的生产应用程序。

            因为一位开发者为一个早已解决的问题构建了解决方案,如果他能再多花点时间研究问题的话。

            我们通过移除数千行代码、一个依赖项(及其依赖项),并在 READMe 中添加一行说明来“修复”它:使用类似的环境加载器。结果发现,这不仅是一个安全问题,还是一个 inotify 资源占用者、内存占用者,以及启动时的 I/O 瓶颈。之后我们能够缩减部分生产环境基础设施。

            是的,开发人员构建了糟糕的软件。但问题不在于质量,而在于它本就不该被构建。

      • > 它解决了什么问题,而这些问题尚未被其他人解决?

        没什么。除了“可移植性”的论点或许可以提及。

        Java内置了安全管理器和访问限制,但它从未真正有效(且在实际使用中相当繁琐)。多年来出现了大量绕过机制,以及补丁修复等。

        坦白说,操作系统才是唯一值得信赖的安全保障,因为它处于应用程序通常能达到的最低级别(除非你进入驱动程序/内核空间,比如那些杀毒软件/反作弊软件/CrowdStrike应用)。

        但平台供应商总是喜欢“自己动手”(NIH),希望让自己的平台稍显便捷,同时仍保持类似的安全级别。

      • 如何在 Linux、macOS 和 Windows 系统中通过操作系统层面解决这个问题?

        我已经尝试了几年,试图为我的 Python 项目找到一个好的解决方案。但我目前还不信任自己想出的任何方案——它们彼此不一致,而且由于其固有的复杂性和缺乏我信任的文档,我感觉自己很容易犯错。

        • 如果某个问题在操作系统层面得到解决,它很可能需要根据操作系统进行调整。就像解析数据的应用层解决方案在 nodeJS 和 Java 之间会略有不同一样。

          要让解决方案真正适用于所有操作系统,可能需要在网络层面实现。例如,通过代理服务器仅允许流量访问特定的白名单/黑名单目的地。

          • 代理方案解决了网络访问问题,但无法解决文件系统访问问题。

            使用代理时,挑战在于如何确保编程语言中的不可信代码仅通过代理访问网络。除了容器和iptables,我尚未看到其他实现方式。

            • 我想我的意思是,我们拥有不同的操作系统,正是因为人们希望以不同的方式完成任务。因此,我们无法采用通用方法来实现这些功能。

              操作系统通用的文件系统权限就像操作系统通用的用户界面框架一样,本质上非常困难且最终受限。

              另外,我完全理解你对操作系统在网络和文件系统权限方面的解决方案感到头疼。尽管我对rwx权限还算熟悉,但我绝不会允许未受信任的代码运行在存有敏感文件的机器上。但我认为我们应该通过改进操作系统工具来解决这个问题,而不是将问题转移到应用层。

        • 为什么桌面程序需要此类限制?

      • > 为什么要重新发明轮子,给项目增加复杂性、漏洞风险、维护负担等等?它解决了什么问题,而这些问题没有被其他人解决过?

        虽然这(实际上)是一种权威论证,但你为什么假设 Node 团队没有考虑过这一点?他们以保守的态度著称,不轻易添加间接层或额外层。而且他们非常注重 *nix 系统。

        我敢肯定他们考虑过“我只需在不同用户下运行这个脚本”

        (我推测这是因为权限 API 涵盖了大量资源和副作用,其中一些在不同操作系统上难以复现,但我没有原始提案可供查阅和验证)

      • 操作系统级别的检查在不同操作系统和不同版本上必然会有所不同。在应用程序二进制文件本身中包含此类检查意味着,无论运行应用程序的操作系统是什么,都可以实现标准化的实现。

        我经常听到关于数据库级安全规则的类似论点。例如,行级安全(RLS)是一个非常强大的功能,我认为在条件允许的情况下值得使用。不过,使用RLS并不意味着可以跳过API层的授权规则检查,你需要在业务逻辑中和数据库中都进行授权验证。

        • 好吧,我来试试。你认为Node.js实现是否了解DNS搜索路径?(我估计它有90%的概率不知道。)

          如果你不知道什么是DNS搜索路径,这里是我非正式的解释:你的应用程序可能请求连接到foo.bar.com或bar.com,如果你的/etc/resolv.conf文件中包含“search foo”,那么这两个请求实际上是同一个请求。

          这是企业网络的重要特性,因为它允许进行宏管理操作、临时故障转移解决方案等。但如果一个程序在不理解这一特性的情况下配置了 Node.js,那么这些操作将无法实现。

          从我的角度来看,作为需要执行运维/管理任务的人,我非常讨厌有人使用这些 Node.js 特性。它们会碍事并引发问题,因为它们只是玩具,而非真正的解决方案。应用程序无法以非玩具的方式处理 DNS。这是系统的工作。

          • 哦,如果 Node 的实现能处理这种情况,我会非常惊讶。

            我也不太期待它能做到,这取决于应用程序运行的环境。如果部署环境故意包含resolv.conf或类似文件,我预计开发人员会采用更优雅的解决方案,或配置Node以预期这些解析结果。

      • 在应用层设置网络限制也会给许多企业的组织结构带来尴尬问题。

        例如,“一个微服务无法连接到另一个微服务”的问题传统上是运维/环境/SRE的问题。但现在应用程序开发团队也必须参与其中,以防有人使用了这些新限制。或者其他团队需要学习 Node。

        这是非自愿的 DevOps 被强加给我们,每个人都必须学习一切。

        • 我对DevOps的经验是,他们对部署和保护Java、Kotlin或Python非常了解,但对Node.js及其工具知之甚少,而且往往拒绝学习其生态系统。

          这导致Node.js团队不得不学习DevOps,因为DevOps团队在处理Node.js时做得不够好。

          前端构建也是如此。在其他语言中(特别是Java/Kotlin),DevOps团队通常负责维护构建工具和相关配置。但Node生态系统并非如此,无论是后端还是前端。

      • 一个真诚的问题,因为我对此了解不多。操作系统中哪些功能可以实现此类网络限制?通过基本搜索或咨询 AI,我发现这似乎在一般情况下难度很大,除非使用类似 AppArmor 的工具,但此时似乎已不再属于操作系统范畴。

      • 您认为有多少应用程序在生产环境中正确设置了仅限所需的用户和访问权限?即使这个比例很高,那么开发者的机器呢?那些运行可能导入未知内容的节点脚本的人呢?虽然可以安全运行,但我怀疑这样的人比例不高。此类功能可以提高这个比例

        • 难道“简化”或甚至意识到此类操作系统功能的存在,不是比在运行时重新构建更好的解决方案吗?

          如果现有功能使用率过低,我并不确定将其重建到其他地方是否是合适的解决方案。除非现有功能本身处于根本错误的位置。而这并非如此:操作系统可能是访问权限管理的唯一正确位置。

          显而易见的解决方案是教育。教人们如何正确使用Docker挂载。如何使用chroot。如何理解Linux的chmod和chown命令。或者提供现代且易于使用的替代方案。

          • 你关于操作系统关注这些问题的观点很正确,但说解决方案是教育似乎有点天真。你打算如何教导人们?或者谁来做这件事?如果 Node 运行时通过实现这一功能使使用更安全,那将帮助很多人。说人们需要自己学习对谁都没有帮助。

      • 有什么不喜欢的吗?它们并没有取代操作系统层面的限制,而是对其进行了补充。

        • 这与“不应该在语言X中添加该功能,人们应该改用语言Y”的论调类似(软件开发者说“应该”通常是个警示信号,根据我的经验)

        • 不,它们不会增加功能。它们只会造成混乱。从管理员的角度来看,当同一个概念配置可以在多个不同位置使用不同的配置语言进行设置,且受不同升级策略管理、由非预期用户拥有、登录到非预期位置时,这简直令人头疼。

          此外,我敢用我一个月的工资打赌,这个 Node.js 实现的特性没有考虑到系统级别上可能存在的多种边界情况和配置。特别是,我担心 DNS 搜索路径,我认为在用户空间应用程序中正确实现这一点会很困难。另外,/etc/hosts 文件会发生什么?

          从管理员的角度来看,我不希望应用程序添加另一个(有缺陷的)层来操纵发现协议。通常,弄清楚为什么两个本应连接的应用程序无法连接是一项非常耗时且劳动密集型的工作。如果你继续随机添加更多变量到这个问题中,你肯定会遇到麻烦。

          • 如果你对这些事情感到困惑,你就是一个糟糕的管理员。

      • 实际原因在于运行时应拥有比代码更高的权限,例如在 Node.js 中 require(‘fs’) 可能需要读取系统文件夹中的文件。

        • 不一定如此,例如在 SELinux 中,你可以为“主进程”配置一个域,该域可以切换到权限较低的“应用程序”代码域。

      • 在 Deno 中,你可以创建一个甚至无法访问文件系统的运行时。

        这是一个很酷的功能。使用 jlink 创建自定义 JVM 也会实现类似的功能。

        这是一个好功能。不过你说的仍然成立,使用操作系统来实现才是正道。

    • 我不会相信它能正确实现。这就像银行相信所有客户都会做正确的事情。如果你想要MAC(而非DAC),应该在内核中实现,就像它应该做的那样;使用apparmor或selinux。这两种方法都能让你控制的范围远不止于读写哪些文件。

      • 是的,但你明白,这需要与应用程序一起部署,需要运维团队的协助。而修改命令行则由应用程序开发者控制。

      • 仅仅因为你有保险箱,并不意味着前门的锁就毫无用处。

      • > 我不相信他们会正确地完成这件事。

        我不明白这种抱怨。你希望他们永远不要开发这个功能吗?你的观点是什么?是在表达信任问题吗?

        • Node 通过 N-API 允许在包中使用原生插件,因此任何原生模块不受这些权限限制。Deno 通过 –allow-ffi 处理此问题,但这些实验性的 Node 权限并不会禁用 N-API,它们只是限制 Node 标准库。

          • > Node 允许通过 N-API 在包中使用原生插件,因此任何原生模块均不受这些权限限制。 (…) Node 权限 (…) 仅限制 Node 标准库。

            那又怎样?这在 Node 的文档中已明确说明。

            https://nodejs.org/api/permissions.html#file-system-permissi

            你认为你在说什么?

            • 一个可以轻易绕过的权限系统有什么意义?

              • > 一个可以轻易绕过的权限系统有什么意义?

                你似乎有些困惑。系统并未被绕过。你唯一能提出的论点是,该系统覆盖了对 node:fs 的调用,而某些模块可能并未使用 node:fs 访问文件系统。你控制着系统中运行的依赖项,以及软件的设计方式。如果你选择以一种方式设计你的系统,使得你的 Node.js 应用程序必须无限制地访问文件系统,你拥有实现这一点的工具。如果你希望限制文件系统访问,只需使用 node:fs 并切换一个开关。

              • 为了勾选一个框

                > 需要证明符合安全标准。

    • 路径限制看似简单,但正确实现起来非常困难。

      PHP曾经(实际上现在仍然)有一个“open_basedir”设置,用于限制脚本可以读写的位置,但人们发现了一些利用符号链接和其他手段绕过该设置的方法。开发人员花了一段时间才修复已知的漏洞。看起来 Node 在过去几年中也经历了类似的过程。

      同样,如果有人能利用 DNS 技巧以某种方式绕过 –allow-net 限制,我也不会感到惊讶。这可能不值得单独作为一个漏洞,但它可以作为针对性攻击中的一个步骤。所以不要过于信任它,并始终实践深度防御!

      • 上次有主要运行时尝试在虚拟机层面实施此类限制时,是.NET——而它从Java那里借鉴了这个想法,Java只比它早了5年。

        在当今的Java和.NET虚拟机中,这一整套功能已被废弃,因为它们无法确保其安全性。

      • 我认为各种操作系统已通过实现相应的系统调用(如openat)来支持这一功能

        例如:https://go.dev/blog/osroot

        • 即使如此,也无法防范绑定挂载。其理由似乎是只有 root 用户才能创建绑定挂载。但你知道吗,无特权用户也可以使用 FUSE 创建各种离谱的挂载。

          分层目录结构的整个概念不过是种幻觉。其中可能存在各种交叉链接,甚至循环引用。

    • 似乎找不到 allow-net 的官方文档链接,只有博客文章。

  2. 这里的关键升级不是 ESM。而是 Node 将 fetch + AbortController 集成到核心中。移除 axios/node-fetch 使我的 Lambda 包减小,并减少了约 100 毫秒的冷启动延迟。如果你还在习惯性地使用 npm i axios,2025 年的 Node 就是你该放弃这些辅助工具的信号。

    • 在发布16年后,以网络请求为核心的JS运行时现在原生支持网络请求。

      • 显然它支持网络请求,当时fetch API还不存在,而当时的标准XMLHttpRequest简直令人发指。

        • 虽然疯狂但效果不错。至少我们可以获取下载进度。

          • 你可以通过Fetch获取下载进度,但无法获取上传进度。

            编辑:实际上,你甚至可以获取上传进度,但由于文档匮乏,实现起来似乎很棘手。你可能最好使用XMLHttpRequest来处理上传。我打算现在尝试一个简单的实现。这引起了我的好奇心。

            • 花了我几个小时,但最终实现了上传和下载的进度条功能。我的uploadFile方法大约有40行格式化代码,downloadFile方法大约有28行。一旦弄明白原理,其实相当简单!

              需要注意的是,关键细节在于你的服务器(以及任何中间服务器,如反向代理)必须支持HTTP/2或QUIC。我在这方面花费的时间比前端代码多得多。在2025年,这对任何现代客户端来说都不是问题,而且已经有好几年了。然而,这可能取决于你的后端代码库的成熟度。例如,Express在没有其他依赖项的情况下不支持HTTP/2。在折腾了一段时间后,我放弃了它,转而使用Fastify(内置HTTP/2和高级流处理)。因此,我理解你对此可能有的顾虑。

              总体而言,我对fetch在轻松跟踪进度方面有广泛支持感到满意。

              • 能否分享一个代码片段?

                  • 您在哪些浏览器中测试过此功能?我运行了 Chrome 文档中的功能检测脚本,Safari 和 Firefox 似乎都不支持 fetch 上传流式传输:https://developer.chrome.com/docs/capabilities/web-apis/fetc

                      const supportsRequestStreams = (() => {
                        let duplexAccessed = false;
                      
                        const hasContentType = new Request(‘http://localhost’, {
                          body: new ReadableStream(),
                          method: ‘POST’,
                          get duplex() {
                            duplexAccessed = true;
                            return ‘half’;
                          },
                        }).headers.has(‘Content-Type’);
                      
                        return duplexAccessed && !hasContentType;
                      })();
                    

                    Safari似乎不支持duplex选项(duplex获取器从未被触发),而Firefox甚至无法处理将流用作Request对象的正文,最终将正文转换为字符串,并设置内容类型标头为'text/plain'。

                    • 哎呀。仅限Chrome!我完全错了。也许我应该少做深夜开发。

                      看来我之前关于下载但不支持上传的表述不幸是正确的。我原本以为可读/可转换流就足够了,但如你所指出的,我显然忽视了 Safari/Firefox[0][1]中缺乏双工选项支持这一关键问题。这绝对不是广泛支持!我喝了太多咖啡。

                      感谢您指出这个问题!经过进一步调查,我也遇到了与您相同的问题。Firefox 确实像您所说的那样出现了故障。有趣的是,如果使用 transformStream 与 file.stream().pipeThrough([您的 transform stream 这里]),Safari 会静默失败,但如果使用可写 transform stream 与 file.stream().pipeTo([可写 transform stream 这里]),它会显示一条提示“不支持”的错误信息。

                      我查阅了您提到的文章,但当然没有完全阅读。令人失望的是,这篇文章是2020年的,而且在这方面没有取得任何进展。在caniuse上查看,似乎Safari和Firefox对Web Workers中的类似行为支持不完整,要么是部分支持,要么需要通过标志启用。所以我想还是有希望的,但如果我让任何人的希望太高了,我感到抱歉 🙁

                      [0] https://caniuse.com/mdn-api_fetch_init_duplex_parameter [1] https://caniuse.com/mdn-api_request_duplex

            • 我感到困惑的是,流式传输方法是否与XHR方法相同。我完全不清楚XHR方法是如何实现的,也不确定其实现是否符合标准规范——因此我的问题是:

              XHR是否会跟踪数据包是否已到达目的地,还是仅记录其已被操作系统排队待发?

            • 狙击

      • 真是奇怪的评论。你总是可以进行网络请求。Fetch 是一个在浏览器和服务器端具有相似语义的 API,使用 Promises。

      • 告诉我你不是 Node.js 开发者 🙂

      • Node 一直都有一个低级别的 HTTP 模块。

    • 虽然有点跑题,但觉得值得分享,因为验证和 API 调用是相辅相成的:我个人更倾向于使用 `ts-rest` 覆盖整个栈,因为它是目前所有基于 Zod/JSON 模式的编译时 + 运行时验证库中最为精简的。它允许你使用任何你想要的 HTTP 客户端(我个人使用 bun,或在 Node 环境中使用 fastify)。额外的开销完全值得(对我来说),因为它将几乎所有的类型安全正确性转移到了编译时。

      好奇其他人对此有何看法,以及是否有其他选项?我觉得自己已经搜索得相当彻底,而这是我找到的唯一一个既轻量级又具备足够类型安全性的方案。

      • 我对 Hono 的 zod 验证器 [1] 以及从中获取的类型安全“RPC”客户端 [2] 印象深刻。我使用Hono的大部分场景是在Deno项目中,但它在Node和Bun上的支持似乎也相当不错。

        [1] https://hono.dev/docs/guides/validation#zod-validator-middle

        [2] https://hono.dev/docs/guides/rpc#client

        • 同意。Hono对我来说非常出色,且具有很高的可移植性。

      • 就在上周,我正准备将 `ts-rest` 集成到一个项目中,原因与你上面提到的相同……直到我意识到他们目前还不支持 Express v5:https://github.com/ts-rest/ts-rest/issues/715

        我认为 `ts-rest` 是一个很棒的库,但缺乏维护让我不敢投资,即使我没有使用 express。你有没有考虑过构建自己的内部解决方案?如果你已经设置了 `ts-rest` 并且对此感到满意,我并不一定会推荐这样做,但由于大语言模型(LLMs) 的出现,如今重建第三方依赖项的定制版本实际上更可行了。我最终构建了一个精简版的 `ts-rest`,对此我非常满意。能够完全控制/理解内部结构感觉非常好,而且令人惊讶的是,这只花了我几天时间。Claude 提供了巨大的帮助,填补了我许多知识空白,尤其是复杂的 Typescript 类型。如果你决定走这条路,我还会注意树摇动和意外的客户端 zod 导入。

        我仍然对能够完成这件事感到有些震惊,但确实,在2025年,自行开发解决方案绝对是一个可行的选择。

        • ts-rest 如今已不再受到广泛支持。其未能采用现代 tanstack 查询集成模式最终迫使我们寻找替代方案。

          幸运的是,oRPC已经发展到足以成为可行方案的程度。我强烈推荐它而非ts-rest。它本质上是tRPC,但支持ts-rest风格的合同,从而能够实现标准的OpenAPI REST接口。

          https://orpc.unnoq.com/

          https://github.com/unnoq/orpc

          • 第一次听说oRPC,从未听说过或使用过ts-rest,而我一直是tRPC的忠实用户。切换到oRPC是否值得花费时间和精力?

        • 您需要大语言模型(LLM)吗?我已经制作了自己的内部分叉的Java库,无需任何LLM帮助。我需要apache.poi的Excel处理程序进行流式传输,而poi只支持单向传输。有人编写了一个与POI兼容的库,支持反向流式传输,但其依赖项与我的项目不兼容。因此我创建了自己的分支,使用了与我项目兼容的依赖项。这让我摆脱了Maven依赖地狱。

          当然,我更希望不必维护这个本应是POI核心功能的分支,但这比维护一堆无法兼容的依赖项要好。

          • 对于分叉和更改一些细节,我可以看到大语言模型(LLMs)的需求可能会减少,特别是如果你知道你在做什么的话。但在我的情况下,我实际上并没有分叉`ts-rest`,而是从头开始构建了一个更小的自定义抽象,而且我并不认为自己是一个顶级开发人员。在这种情况下,大语言模型(LLMs)似乎提供了更多价值,这并不一定是因为问题过于困难,而是因为节省了时间。如果没有大语言模型(LLMs),我可能永远不会考虑这样做,因为机会成本太高(即 DX 工作与关键的用户面对面工作)。我估计,如果没有大语言模型(LLMs),我可能需要两周或更长时间才能完成这项任务,而有了大语言模型(LLMs),我只用了几天时间就完成了。

            我确实觉得,我们正在朝着一个方向发展,即内部构建将比默认使用第三方依赖项更常见——这完全是因为机会成本大大降低了。我还想知道,代码共享和开源库在未来会发生什么变化。我可以看到这样一个世界:维护者不再上传包供其他人插入他们的项目,而是上传详细的指南,指导如何自己构建和定制库。我认为这种方法非常适合大语言模型(LLM)。我认为一个很好的例子是 `lucia-auth`[0],维护者放弃了他们的库,转而创建了一个指南。他们的决定与大语言模型(LLMs)无关,但我个人更愿意使用这样的指南和人工智能(我已经这样做了),而不是依赖未来不确定的第三方依赖项。

            [0] https://lucia-auth.com/

        • 算了,我真是个傻瓜,哈哈,ts-rest确实支持Express v5:https://github.com/ts-rest/ts-rest/pull/786。别听我上面说的错误信息!!

          不过我认为这个疏忽倒是个意外之喜,我真的很欣赏尽量减少依赖项的做法。如果能回到过去,知道现在所知的一切,我仍然会选择同样的道路。

      • API调用的类型安全至关重要。我没有使用过ts-rest,但编译时验证的方法听起来很可靠。比运行时意外要好得多。实际使用体验如何?您认为模式定义的开销值得吗,还是对简单端点来说感觉太重?

        • 我总是会在需要可靠性的代码库中为API调用添加某种模式验证。

          对于原型,我偶尔会使用tRPC。我不喜欢它为生产应用程序添加的魔法程度,但它确实很快就能完成原型设计,而且我们最终还是会使用RPC调用。

          对于生产环境,我最熟悉的是zod,但也有不少不错的选择。我会使用一个类似 fetchApi 的封装调用,该调用接受模式 + fetch() 参数,并验证响应。

          • 如何在另一端提供模式?

            我发现保持前端与后端同步是一大挑战,因此编写了一个脚本,从后端读取模式并在前端生成 API 文件。

            • 有几种方法,但我认为SSOT(单一数据源)是关键,正如其他人所说。一些方法:

              1. 共享TypeScript类型

              2. tRPC/ts-rest风格:自动生成的客户端,带编译时和运行时类型安全

              3. RTK(Redux工具包)查询风格:代码生成的前端客户端

              我个人更倾向于#3,因为它更明确——你可以实际审查它为新/更改的端点生成的代码。它确实有缺点,即代码量更多,而且随着代码库变大,你开始需要一个缓存来避免每次小改动都重新生成整个API。

              总体而言,我认为采用显式方法是值得的,因为根据我的经验,在大型生产代码库中,这种方法可以节省数天甚至数周的开发时间,避免在后续开发中追踪服务器/客户端验证的特殊情况。

              • 使用服务器端Zod模式时,会出现哪些验证特殊情况,而这些情况在使用代码生成的客户端时却不会发生?

            • 我几乎总是倾向于使用独立的包来处理此类共享逻辑(至少在两端都能使用相同语言的情况下)。

              对于JS/TS,我会创建一个共享的模型包,仅定义后端和前端都关心的请求和响应的模式和类型。如果需要模型迁移来支持持久化或缓存层,我也可以在那里定义迁移。

              虽然这样做需要多花些功夫,但我更喜欢自己掌控设置并清楚了解其工作原理,而不是依赖工具在构建步骤或转译过程中自动完成这些配置。

            • 将两者都用 TypeScript 编写,并为每个 API 端点定义请求和响应的形状作为模式。

              服务器验证请求主体并生成与响应模式类型签名匹配的响应。

              客户端代码有一个 API,其中请求主体作为其输入形状。客户端甚至可以验证服务器响应以确保其符合合同。

              在实际应用中,这种设计非常优雅:只需对 API 进行一次修改(例如重命名字段),所有使用该字段的代码点都会立即被标记为类型错误。

              • 这将导致旧客户端无法正常工作。因此,制定一个考虑此问题的部署策略至关重要。

        • Effect 提供了一个非常好的编译时模式验证引擎,可以与各种数据获取和处理管道组合使用,并在外部数据不符合模式或网络请求失败时提供合理的错误处理。

        • 无论如何,模式定义比从头编写输入验证更高效,因此这完全是双赢的,除非你想要冒险不做任何验证

      • 还想提一下 ts-rest。我们有一个 TypeScript 单仓库,后端和前端从共享包中导入 API 合同,使前端集成既类型安全又简单明了。

      • 我从 ts-rest 迁移到了 Effect/HttpApi。这是一个令人惊叹的生态系统,Effect/Schema 已经取代了我的领域层。不过确实需要一定的学习曲线。

      • 就我而言,我是 ts-rest 的忠实用户。这是我目前找到的最佳解决方案。

    • 我一直不喜欢 fetch 的语法,以及需要等待 response.json 的过程,还得实现额外的错误处理 –

        async function fetchDataWithAxios() {
          try {
            const response = await axios.get(‘https://jsonplaceholder.typicode.com/posts/1’);
            console.log(‘Axios数据:’, response.data);
          } catch (error) {
            console.error(‘Axios错误:’, error);
          }
        }
      
      
      
        async function fetchDataWithFetch() {
          try {
            const response = await fetch(‘https://jsonplaceholder.typicode.com/posts/1’);
      
            if (!response.ok) { // 检查 HTTP 状态码是否在 200-299 范围内
              throw new Error(`HTTP 错误!状态码:${response.status}`);
            }
      
            const data = await response.json(); // 解析 JSON 响应
            console.log(‘Fetch 数据:’, data);
          } catch (error) {
            console.error(‘获取错误:’, error);
          }
        }
      
      • 虽然在理论上是正确的,但在实际中你只会将这段代码作为一个工具函数编写一次;相比之下,在自己的工具函数中添加两行额外代码与加载36 KB的JS文件相比,显然前者更优。

      • 没错,这就是经典的包大小与开发体验的权衡。Fetch 确实需要更多冗余代码。手动检查 response.ok 和双重 await 确实令人烦躁。对于 Lambda 环境(我需要优化冷启动性能),我会接受这一点,但对于普通应用开发(包大小影响较小),axios 的更简洁 API 可能更适合我。

        • 同意,但我认为在每个项目中,我都会在 axios 或 fetch 周围添加一个最小的包装函数——因此,为了让 fetch 更友好而添加一点点额外代码对我来说就像是“番茄”和“西红柿”的区别。

        • 为什么不直接将这些设置为选项呢?

          { throwNotOk, parseJson }

          他们知道这是99%的Fetch调用,我不明白为什么不能直接内置进去。

        • 如果你在自己的客户端 SDK 中到处使用裸露的 fetch 调用,那你就是在自掘坟墓。或者至少是在徒增麻烦却毫无益处。

      • 我似乎没明白你的意思。

        以下代码比你的两个示例都更简洁。但我肯定我错过了重点。

          fetch(url).then(r => r.ok ? r.json() : Promise.reject(r.status))
          .then(
            j => console.log(‘Fetch Data:’, j),
            e => console.log(‘Fetch Error:’, e)
          );
        

        我冒着让自己尴尬的风险分享这一点,希望能从中学习。

        • 这取决于你对“干净”的定义,我认为这段代码是“巧妙的”,但它更难一目了然地阅读。

          你可能会将执行请求的代码放在一个工具函数中,因此调用处就是await myFetchFunction(params),简单明了。由于它被隐藏起来,myFetchFunction的实现无需过于巧妙或紧凑;优先考虑可读性,不要害怕代码长度。

      • 不过你可能需要对不同错误代码采用不同的错误处理方式。例如,我们的验证错误也会返回一个 JSON 对象,但错误码为 422。

        因此,将“获取响应”和“从响应中获取数据”分开处理对我们来说效果很好。

      • 我通常这样写:

            const data = (await fetch(url)).then(r => r.json())
        

        但显然,你可以将语法包装成你喜欢的任何形式。

        • 为什么不呢?

              const data = await (await fetch(url)).json()
          
          • 这非常简洁。不过,双重 await 仍然显得奇怪。为什么需要这样做?

            • 第一个 `await` 是等待响应头部到达,这样你就能知道状态码并决定下一步该做什么。第二个 `await` 是等待完整主体到达(并解析为 JSON)。

              这样设计是为了支持除缓冲整个正文以外的其他操作;你可以选择流式传输、提前关闭连接等。但这会导致在常见场景下(始终加载整个正文,然后再决定下一步操作)出现双重等待的尴尬情况。

            • 所以你可以这样写:

                  let r = await fetch(...);
                  if(!r.ok) ...
                  let len = response.headers.get(“Content-Length”);
                  if(!len || new Number(len) > 1000 * 1000)
                      throw new Error(“Eek!”);
              
            • 这不是问题,以下代码运行正常…

                  var data = await fetch(url).then(r => r.json());
              

              理解Promises/A(thenables)和async/await有时会让人感到困难或困惑,尤其是在像上面那样混合使用这两种方式时。

            • IMU,因为你并不一定需要响应正文。第一个 Promise 在收到头部后解析,而 .json() Promise 仅在收到完整正文(并通过 JSON.parse 解析)后解析(但这本身是同步操作)。

        • 坦白说,这感觉像是无谓的繁琐操作;很少有人会经常编写如此底层的代码。如果你连接到一个 API,很可能所有响应都是 JSON,因此你可以为该 API 的所有请求编写一个实用函数。

          代码不需要简洁,需要清晰。尤其是在后端代码中,代码大小不像在网页上那么重要。如果你在无服务器平台上运行代码,代码大小仍然有些重要,但更重要的是管理你的依赖项,而不是你自己的代码行数。

        • 你不需要所有这些括号:

            await fetch(url).then(r => r.json())
          
    • 如果一个技术栈(Node + Lambda)在某些请求中增加 100ms 延迟,仅仅是为了获得在几乎完全通过 HTTP 请求进行通信的环境中发送 HTTP 请求的能力 [1],那么这个技术栈一定有问题。

      [1] 便捷功能——否则你将使用 XMLHttpRequest

      • 1. 这不是请求的 100ms 延迟。这是加载此代码的进程初始化过程的 100ms 延迟。而这具体是在 Lambda 函数的上下文中,该函数可能仅有 128MB 内存和约 0.25vCPU。一个用Java编写的Hello World应用程序,没有导入任何库,只是将内容打印到标准输出,其初始化延迟会比这更高。

        2. 你不需要使用axios。其主要价值在于它提供了一个可在不同运行时环境中使用的统一API,并包含许多便捷的抽象功能。还有许多其他轻量级的HTTP库比标准库的'http'模块更方便。

        • 在初始化时,Lambda 函数会以全核心运行,但在调用时,128MB 内存仅以 1/20 核心运行。

    • Node.js 的 fetch 接口远优于 Axios(更易于使用/理解,更简单);没想到还有人仍在使用 Axios

      • 我确实怀念 axios 的扩展功能,添加速率限制、限流、重试策略、缓存、日志记录等非常方便。

        当然你可以用 fetch 实现这些功能,但实现方式更分散且需要更多冗余代码

        • 完全理解!我认为这取决于具体场景。对于Lambda这种每KB和毫秒都至关重要的场景,原生fetch更胜一筹,但对于需要健壮HTTP处理的完整应用,Axios的插件生态系统确实相当不错。fetch库的碎片化问题确实存在。你最终会评估5个不同的重试包,而不是直接使用axios-retry。

        • 听起来似乎有空间开发一个基于fetch的axios类库。

          • 我认为这就是最佳平衡点。兼具原生请求性能与axios风格的便捷性。一些库正在朝这个方向发展,但目前还没有哪个库真正做到了这一点。挑战可能在于在保持轻量级的同时,仍能解决评估5个重试包的问题。

            • 这是你想要的吗?https://www.npmjs.com/package/ky

              我还没用过,但每周下载量看起来很稳定。

              • Ky 确实是朝着这个方向发展的库之一。根据下载量来看,它的采用率不错,但生态系统仍然有些碎片化。有 ky、ofetch、wretch 等库都在解决类似的问题。不过,在我看来,ky 目前可能是最强大的竞争者。

          • 就像 axios 可以通过指定 fetch 后端来实现,但它不会异步执行 .json() 操作。

            • 我其实不太喜欢 fetch 的异步 .json 功能,因为当它失败(因为“不是 JSON”)时,你无法查看文本内容。当然,你可以克隆响应,然后从克隆中读取文本……如果你在进行其他处理,这也不是太糟糕。

      • 你仍然会在dev.to等网站的入门教程中看到axios的使用。还有很多遗留代码存在。

        • 人工智能会像 80 年代播放 Wham 歌曲的迪斯科舞厅一样,将它带回来。如果你要做,就做错吧…

          • 我让 Claude 决定用 Axios(在项目中未安装或根本不存在)替换我现有的基于 fetch 的 API 调用,这与无关的更改毫无关系。

            • 我让 Gemini 使用谷歌的 new LLM API 纠正我的代码,以使用旧的代码。

          • 哈哈,我在回复中经常看到这种情况。我立即拒绝了。

      • 对吧?!我认为很多开发者在 Node 18 之前就养成了使用 Axios 的习惯,因为当时 fetch 还未内置。而且 Axios 提供了完整的功能集,如拦截器、自动 JSON 解析等。但对于大多数用例而言,原生 fetch 加上几行封装代码,比引入整个依赖库更具优势。

      • 这都是好消息。我刚收到关于axios依赖项中存在漏洞的警报(这是一个较旧的项目)。摆脱这些依赖项比仅仅升级它们要吸引人得多。

        • 升级Node版本难道不是更大的挑战吗?(如果你使用的是不再接受维护的Node版本)

          • 不清楚兼容性破坏的程度,但这可能迟早要发生,而减少依赖关系对我来说是值得的。

      • 我以为axios几年前就已停更,现在没人应该还在用它!

      • 拦截器(以及扩展功能)仍是axios的核心优势。Fetch对于脚本很棒,但我不建议完全基于它构建应用程序;你将不得不大量重写代码或拼凑其他库。

      • 拦截器呢?

    • 作为库作者,情况恰恰相反。虽然 fetch() 非常出色,但 ESM 虽然升级过程痛苦,但绝对值得。它具备作者描述的所有特性。

      • 从库作者的视角来看很有趣。公平地说,你们不得不应对整个生态系统转变:双包风险、CJS/ESM兼容性噩梦、工具链变更等,因此从你们的角度来看,ESM确实是更重要的变革。

        • 我是一个小型的作者,但有一段时间真的很痛苦,因为我们都在 CJS 和 ESM 中双重发布,这简直是一团糟。后来一些知名作者决定完全转向 ESM,基本上我们很多人也跟着做了。

          fetch() 的更改对需要 HTTP 请求的库来说影响很大,否则变化并不算太大。即使在这些库中,主要也是移除了部分依赖项,在某些情况下这使我将库大小缩减了90%,但这在Node.js中并不算太大问题,不像在前端那样严重。

          现在还有一个未解决的问题,即Node.js流与WebStreams的差异,这目前是一个巨大的混乱。这是一个复杂的话题,但由于存在两种难以匹配的不同流标准,问题变得更加复杂。

          • 双发布真是噩梦。有人必须首先打破僵局。即使 Node 包大小不是那么关键,90% 的大小减少也是实打实的。不过流的问题听起来很混乱。同一运行时中存在两种不兼容的流式传输标准,必然会带来麻烦。

        • 我也维护一个库,转向 ESM 的过程极其痛苦,因为你仍然需要发布 CJS,只是现在必须想办法以一种既能以两种方式打包、又能进行测试等方式编写代码。

          • 这确实是个麻烦,但如果你用ESM格式编写源代码,Rollup可以同时导出两者。我最头疼的是导出TypeScript类型。这部分没有树摇功能!

            • 对于简单项目,现在你需要添加 Rollup 或其他之前不需要的构建系统。对于复杂系统(涉及非trivial 导出),现在会变得一团糟,因为它无法直接工作。

              现在使用 ESM,如果你编写纯 JavaScript,它又能正常工作了。如果你使用 Bun,它也能直接与 TypeScript 兼容。

              • 这就是我真正欣赏 Deno 从 npm 彻底脱离,并后来推动 jsr 的地方。不过,我对 Node 有多少内容被引入 Deno 持保留态度。

        • CJS/ESM兼容性问题逐渐消失的事实表明,这始终是设计选择而非技术限制(大多数CJS格式代码可兼容ESM,反之亦然)。这个问题浪费了太多时间。

          • 这既不是设计选择,也不是技术限制。这是一个复杂的系统工程,必然涉及繁琐的内部工作和相对孤立的团队之间的协调。当有人(Joyee Cheung)实际上做出了相当英雄般的努力,推动所有这些工作完成时,问题才得以解决。

            Joyee 写了一篇详细的文章。阅读这篇文章可以更准确地了解为什么在像 Node 这样的大型项目中,某些事情会发生而另一些事情不会发生:https://joyeecheung.github.io/blog/2024/03/18/require-esm-in

            • Node.js做出了许多对ESM采用产生重大影响的决策。从强制使用扩展名并放弃index.js,到加载器和复杂的package.json“导出”设置。除了Node.js强行推进外,TC39还在不断对规范做出愚蠢的改动,比如deferred importwith语法变更。

              • 要求文件扩展名并禁止自动“index”导入是浏览器环境的强制要求,因为无法直接扫描文件系统,如果浏览器模块需要发送 4-10 次 HEAD 请求来查找目标文件,用户必然会感到不满。

                package.json 中的“exports”控制功能是包/库作者在 CJS 时代就一直呼吁的功能。ESM 因“exports”的复杂性而受到很多指责,因为 ESM 包必须使用它,而 CJS 可以选择性使用并被保留,但格式中的大部分复杂性完全归因于 CJS 的复杂性以及 Node 试图支持 CJS 包中已经存在的各种“exports”选项。由于“桶”模块(仅包含 export thing from ‘./thing.js’ 的模块)在 ESM 中编写起来要简单得多,我尚未见过任何仅使用 ESM 的项目采用复杂的“exports”结构。(“exports”可以像旧版的 main 字段一样简单,只需一个“index.js”,而这可以轻松地编写为一个“桶”模块。)

                > TC39 继续对规范进行愚蠢的修改,如 `deffered import` 和 `with` 语法更改

                我对延迟导入的判断暂缓,直到我弄清楚它解决了哪些用例,但 with 语法对 import 的补充非常出色。我记得 AMD 加载器和 Webpack 中那些嵌入模块名称的疯狂字符串语法(如 json!embed!some-file.jsonpostcss! style-loader!css!sass!some-file.scss)的时代,以及调试这些代码时遇到的困难,还有它们如何将你绑定到特定的文件加载器(永久堵塞你的 AMD 配置,或迫使你锁定特定版本的 Webpack,以防升级破坏你的加载器堆栈)。像 `import someJson from ‘some-file.json’ with { type: ‘json’, webpackEmbed: true }` 这样的语法,仅此一项就是巨大的改进。更重要的是,它是一个单一的语法,看起来与普通 JS 对象非常相似,这对于其他非常有用的元数据属性工具(如为 ESM 导入添加完整性检查而无需 importmap)也非常有用。

            • 你说得对。这并非设计选择或技术限制,而是一个令人担忧的第三种情况:某些贡献者持续传播关于 ESM 本质上是异步的误导信息(实际上它只是条件性异步),并营造出一个敌对环境,导致“贡献者远离 ESM 工作”——正如实现者本人所描述的。

              如今,没有人会为ERR_REQUIRE_ESM辩护,称其为良好设计,但它在2019年已有可行解决方案的情况下仍存在了5年。文档和讨论中的系统性误导信息,加上对话的冷淡化,暗示存在有组织的抵制(“线下对话”)。我怀疑“事情发生与不发生”的真正原因在于Bun/Deno的竞争。

          • 确实有一些合理的技术决策,不过依我之见,Node 本应继续与 Babel 的实现保持兼容,这样沿途的摩擦会少得多。显然这是一个选择,无论好坏。

            有趣的是,随着 Deno 提升与 Node 的兼容性,越来越多的想法被借鉴自 Deno 的实现。我仍然更喜欢 Deno 用于大多数场景。

    • 使用 node:fetch 时,你必须为任何规模的应用程序或服务编写一个用于错误处理、日志记录和重试等的封装层。最终,我们还是不得不使用类似 axios/got 的实现,并修复了大量 bug。

    • 一直让我惊讶的是,平台没有提供原生的一流“HTTP客户端”支持。过去20年几乎每个项目都需要这样的东西。

      此外,“fetch”这个命名很糟糕,因为大多数API调用都是POST。

      • Node.js从创建之初就提供了原生的一流HTTP服务器和客户端支持。封装库可以平滑底层 API 的粗糙边缘,同时使服务器端 JavaScript(Node)看起来和工作方式与客户端 JavaScript(浏览器)相似。

      • “大多数”在这里承担了大量工作。我使用了大量 GET 请求的 API

        • 我指的是服务器端,那里可能有 90% 以上的请求是 POST。确实,客户端情况大不相同(Node 是客户端唯一的参与者,尽管如此)。

      • 这是类别错误。Fetch 仅指发送请求。POST 是发送请求时使用的 HTTP 动词或方法。如果你真的想,你可以自己实现

          const post = (url) => fetch(url, {method:“POST”})
        
        • 我认为 OP 在评论这个类别的双重含义。在英语中,“fetch”是“GET”的同义词,所以“fetch”作为一个类别独立于 HTTP 方法是没有意义的

    • Undici作为内置请求库特别令人兴奋,https://undici.nodejs.org

      • Undici 非常可靠。作为 Node.js 中的 fetch 引擎,其性能提升是实打实的,且内置于核心意味着无需再进行依赖性讨论。此外,它还具备一些高级功能(连接池、流处理),如果你需要从 fetch API 切换到更底层的实现,这些功能非常实用。两全其美。

        • 它已集成到核心中,但未直接暴露给用户。如果你想使用它,仍需安装 npm 模块,这在生产环境中需要通过出站代理时是必需的

    • 这种情况已经持续了一段时间,本文中提到的绝大多数内容并非全新

    • 这些…并非相互排斥的重大升级。不再需要使用毫无意义的 CJS 语法绝对也是一个巨大的进步。

      Web 兼容性“始终”会实现,但拒绝添加 ESM 支持,然后在最终添加后,又拒绝制定将 ESM 设为默认、CJS 作为备用方案的过渡计划,这在过去多年里一直令人非常不满。

      • 尤其是似乎完全可以同时支持两者。Bun 就是这样做的。如果有边界案例,我还没遇到过。

    • 看到人们继续使用 axios 而不是 fetch,这让我很沮丧,感觉他们根本不在乎,只是复制粘贴现有项目作为起点,就这样了。

      • 也许我错了,它可能已经更新了,但axios不是默认支持进度指示器并且整体更干净吗?

        话说回来,有一些npm包是极其过时且被过度使用的。

        • 也许吧,不过我很少看到这些东西被使用,通常只是请求响应类的工作流程。

    • Axios 可以在生产代码中同时支持 Node 和浏览器,不过不确定 Fetch 在浏览器中是否能像 Axios 那样功能齐全。

  3. 你现在也不需要安装 Chalk 或 Picocolors 了,你可以自己样式化文本:

    `const { styleText } = require(‘node:util’);`

    文档:https://nodejs.org/api/util.html#utilstyletextformat-text-op

    • 我从未需要过这些。我通常会定义一个全局应用对象属性,例如:

                  text: {
                      angry    : "u001b[1mu001b[31m",
                      blue     : "u001b[34m",
                      bold     : "u001b[1m",
                      boldLine : "u001b[1mu001b[4m",
                      clear    : "u001b[24mu001b[22m",
                      cyan     : "u001b[36m",
                      green    : "u001b[32m",
                      noColor  : "u001b[39m",
                      none     : "u001b[0m",
                      purple   : "u001b[35m",
                      red      : "u001b[31m",
                      underline: "u001b[4m",
                      yellow   : "u001b[33m"
                  }
      

      然后你可以直接调用它,比如:

          `${vars.text.green}whatever${vars.text.none}`;
      
      • 这就是人们试图耍小聪明的问题。现在无论终端设置如何,你都会输出转义序列。

        使用一个处理这些问题(以及其他成千上万个怪癖)的库要合理得多

        • 这里没有聪明可言。转义序列已有数十年历史,并作为事实标准被普遍支持。在这种情况下,转义序列被赋值给与其他字符串关联的变量。这与使用运算符一样聪明。这些转义序列甚至直接在浏览器的Chrome开发者工具控制台中得到支持。

          真正的问题是“发明综合症”。人们非理性地依赖库来缓解对不确定性的情感恐惧。对于真正的大问题,比如完整的终端模拟,我理解这一点。然而,当这种依赖被推到极致,比如左填充事件,显然人们正在因非理性原因堆砌依赖项。

          • 我讨厌这些微型库,但你的解决方案也会在不需要时打印转义码(例如将输出管道传输到 grep)。如果这是仅在交互模式下有意义的事情,那没问题,但我见过太多明显未设计为在 UNIX 壳层中运行的程序,即使这样做很有意义。

            不过解决起来很简单,只需在输出不是交互式 shell 时,将空字符串赋值给转义码变量。

            • 是的,pnpm 的依赖关系树视图在通过 |less 管道传输时会破坏我的终端环境。许多与 JS 相关的工具似乎都有这种不希望的行为。我假设大多数用户从未以这种深度查看依赖关系,或使用更复杂的工具来实现。我认为这反映了JS生态系统的现状。

            • 这不是字符串输出问题,而是终端模拟器问题。应用程序无需了解调用终端/shell的模式和行为。这一原则同样适用于所有其他向stdout写入输出的应用程序。这里没有巧妙之处。但是,如果你真的真的想避免使用ANSI描述符,可能是因为你不喜欢彩色输出,我的应用程序提供了一个无色选项,它将ANSI控制字符串值替换为空字符串。

              • 这是 100% 应用程序的职责。这就是为什么许多程序提供类似 –color=auto 的选项,以便根据输出文件描述符类型推断最佳输出模式,例如对终端使用颜色但对管道不使用颜色。

              • 终端模拟器不参与管道传输到其他进程。如果在非交互式环境中未抑制转义码,Grep 将搜索转义码,因此这绝对是字符串输出问题。

                如果你想自己实现,那就做对——或者依赖经过实战验证的库来处理这个问题,以及你未考虑到的边界情况(如 NO_COLOR)。

                • 如何定义“经过实战验证”?

                  通常 JS 开发者认为,如果足够多的人使用某项技术,那么它就应该是安全的。这与那些真正重视安全的组织之间存在巨大分歧。NPM 上大多数软件都没有安全评级,而且越来越多的高使用率包被识别为恶意或被入侵。

                  如果你自己动手做错了,你仍然有可能比完全放弃依赖管理或对“经过社区审核”的包进行盲目猜测更安全。

                  • 我的意思是,我们这里讨论的是用于字符串格式的库,而不是渲染引擎。如果你对这样的库的质量有疑虑,就去GitHub上阅读源代码。这就是我在决定是否安装某个库或自己实现之前通常会做的事情。

              • 许多应用程序使用isTTY来确定输出方式,供参考

        • 这取决于应用程序的使用场景和环境。如果是面向公众的应用,使用库更合适。如果是内部或公司定义的环境,则无需额外依赖(但仅限于这类简单解决方案,这类方案可以轻松用库替代)。

      • 我认为广泛使用的终端转义序列现在已经广为人知,但我看不出来为什么我要把这些内容复制到每个项目中。

        另外,我猜如果你把日志管道传输到一个文件中,你仍然会把转义序列写入其中?为什么不让事情变得更简单呢?

        • 可以说,使用库也相当于“把代码复制到每个项目中”。

          • 这未免过于较真。GP指出这些序列已内置于Node.js标准库中,这可能让你无需安装库或复制ANSI转义序列表。

      • 公平地说,我手动编写shell脚本时确实会这样做。但既然它已经内置,为什么还要这样做呢?

      • 我有一个“ascii.txt”文件,用于复制粘贴“书籍表情符号”块字符到日志开头。这样可以减少日志中的噪音。HN无法显示这些字符,所以我需要链接到包含它们的页面:https://www.piliapp.com/emojis/books/

        • 为什么要把那个文件命名为“ascii.txt”?

          • 因为它包含的书本表情符号不止这些。这使得编写几何代码文档字符串更加方便。以下是剩余内容(HN 格式化效果不佳,建议复制粘贴)。

               cjk→⋰⋱| | ← cjk 空格 btw | |
               thinsp | |
               deg° 
               ⋯ …
               ‾⎻⎼⎽ 线条
               _ 细线
               ⏤ 宽线
               ↕
               ∧∨ 
             ┌────┬────┐ 
             │    │ ⋱  ⎸ ← 左边框,右边框:⎹
             └────┴────┘
                ⊃⊂ ⊐≣⊏
                ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯==== ›‥‥‥‥
            

            ◁ ◿ ◺ ◻ ◸ Λ ╱│╲ ╱ │ ╲ ──┼── ╲ │ ╱ ╲│╱ V ┌ ─┏━━┳━━━━━━━┓ │ ┃ ┃ ┃ ├ ─┣━━╋━━━━━━━┫ │ ┃ ┃ ┃ └ ─┗━━┻━━━━━━━┛ ┌ ─ ┬ ─ ┐ ├ ─ ┼ ─ ┤ └ ─ ┴ ─ ┘ ┌───┬───┐ ├───┼───┤ │ │ │ └───┴───┘ . ╱│╲ ↘╱ │ ╲ ↙ ╱ │ ╲ →‹───┼───›← ╲ │ ╱ ↗ ╲ │ ╱ ↖ ╲│╱ ↓↑ ╳ . ╱ ╲ ╱ ╲ ╱ ⋰ ╲ ╱⋰______╲

  4. 这太棒了。阅读这篇文章让我学到了几件事,可以立即应用到我的小型个人项目中。

    1. Node 现在内置了测试支持:看来我可以放弃 Jest 了!

    2. Node 现在内置了监听支持:看来我可以放弃 nodemon 了!

    • 我尝试了 node:test,觉得这对小型项目和需要减少第三方依赖的库作者非常有用,但对于大型应用来说它太过简陋,而 node:assert 又有点像玩具,因此至少需要引入一个更完善的断言库。vitest “直接可用”,并且省去了大量 TypeScript 配置的麻烦。Jest 因自身重量而崩溃。

      • 作为多年来因 mocha 的简洁性而放弃 jest 等工具的人,我仍然欣赏 mocha 将断言库与测试框架分离的设计决策。这意味着 chai [1] 仍然是一个伟大的断言库,而且仅仅是一个断言库。

        (我在 node:test 项目中并未遇到太多 TypeScript 配置问题,但部分原因在于使用了 “type”: ‘module’ 以及各种版本的 “erasableSyntaxOnly” 及其严格模式标志和 linter 前身,其中一些在古代 Mocha 测试中也是不错的想法。)

        [1] https://www.chaijs.com/

    • 嗯,Node 的测试相关功能相当糟糕,而 Node 团队似乎对改进它毫无兴趣。先试用几周再深入研究,你就会明白我的意思(然后如果你去提交这些问题的报告,你会发现 Node 团队并不在意)。

      • 我刚查看了文档,似乎有一些相当强大的模拟功能,甚至还有自定义测试报告器。这听起来确实是一个很好的补充。正如你所建议的那样,在实际尝试之前,我会先抑制我的热情。

      • 但我还是更愿意使用它,而不是导入 mocha、chai、Sinon、istanbul。

        归根结底,这只是测试,语法可能更冗长,但大语言模型(LLMs) 还是会写出来 😉

        • 另外,我发现大语言模型(LLMs)为我编写的测试非常糟糕。这真的令人遗憾,因为这是我第一次使用副驾驶,而且我完全被它迷住了。

          它们非常擅长模拟,以达到 90% 的覆盖率。但除此之外,它们似乎只是测试了足够通过的实现。

          比如我发现,如果实现有问题(即使是明显到可笑的问题,比如if (dontReturnPancake) return pancake;),它们通常会写测试来通过这个坏代码,而不是说“嘿,我觉得你在第55行搞错了……”

        • > 但大语言模型(LLMs)还是会写出来

          问题不在于写作,而在于阅读!

      • 你能详细说明 Node 测试与 jest 相比的缺点吗?

        • 模块支持仍处于试验阶段,需要设置标志,并且没有模拟机制。

    • 我仍然喜欢 Jest,仅仅是因为我可以使用 `jest-extended`。

      • 如果你还没有尝试过 vitest,我强烈推荐你试试。它与 `jest-extended` 以及大多数 Jest 匹配器库兼容。

        • 上次我尝试时,IDE 集成(例如 VSCode 中的 Test Explorer)与 Jest 相比有所欠缺。

        • 我听说有人推荐过;除了速度,它还有什么优势?我对个人项目中5秒的测试运行时间缩短半秒并不太在意 😛

          • 我认为它并不在所有情况下都比Jest更快。在我看来,Vitest的主要优势在于它使用Vite的配置和工具在运行测试前进行转换。这避免了需要将模块解析行为映射到你的打包工具。无需处理ts-jest或babel-jest也是一个优势。

          • Jest 已经过时了,它无法原生支持现代的异步/ESM 等功能。Vitest 则一切正常。

          • 它原生支持 TypeScript 和 JSX,提供出色的间谍、模块和 DOM 模拟功能,支持基准测试,兼容 Vite 配置,并通过并行化测试实现极快速度。

            • 它还能透明地在浏览器中直接运行测试而非模拟 DOM,这是一个非常酷的功能,但我目前还没充分利用。

          • 此外,它拥有非常友好的浏览器界面。按特定测试运行归类日志。非常棒。

            我确信 Jest 也有类似的包(不知道是不是 Jest Extended?)但 Vitest 的体验非常出色且完整

  5. 好帖子!这里有很多我之前不知道已经内置的功能。

    我尝试使用提供的命令生成独立可执行文件,但生成了一个.blob文件,我认为这仍然需要Node运行时环境才能运行。我按照Node文档[1]使用postject生成了真正的可执行文件,但一个简单的Hello World程序生成了一个110MB的二进制文件。这可能是值得一提的缺点。

    看到那些任意的超时限制,我不禁想到南极洲那位因硬编码超时而头疼不已的人[2]。

    [1]: https://nodejs.org/api/single-executable-applications.html

    [2]: https://brr.fyi/posts/engineering-for-slow-internet

    • 我有一篇博客文章[1]和配套的仓库[2],展示了如何使用SEA构建二进制文件(并与bun和deno进行比较),并将文件大小压缩至67MB(对我来说,具体取决于本地节点二进制文件的大小)。

      [1]: https://notes.billmill.org/programming/javascript/Making_a_s

      [2]: https://github.com/llimllib/node-esbuild-executable#making-a

      • > 67 MB 二进制文件

        我希望你能理解这对 JavaScript 圈子以外的人来说有多么疯狂。你成功缩小了文件大小,但天啊……

        • 这根本不算疯狂。任何包含完整运行时环境的二进制文件都会达到MB级别。但这就是重点,终端用户只需下载一个独立的片段,无需关心为了让该二进制文件运行而必须预先安装的垃圾软件。你认为2025年人们会在乎二进制文件是5MB还是50MB?你认为这疯狂比它本身更疯狂。这让我想起所有Membros和Perfbros在抱怨Electron应用,而与此同时,这些应用以100MB+的二进制文件和1GB+的内存占用在数以百万计的普通电脑上运行。

          • 使用大量内存来运行小型应用程序已经成为常态,这不值得庆祝。

            我可以向你保证,这种信念在规模化时会导致基础设施崩溃,而我已经见过这种情况发生过无数次。从未考虑过性能的 Web 开发者们欢快地将巨大的 JSON 数据块或序列化应用程序模型扔进数据库,当性能变得糟糕时,他们继续点击“扩展规模”,而当这最终行不通时,才会雇佣一个真正关心性能的人来修复它。然而,这个人或团队不仅要修复多年积累的垃圾代码,还要改变深植的文化,并与产品团队争夺开发时间。

        • 这个包包含整个Node运行时和所有项目依赖项。这并不疯狂。

        • 你看看.NET打包二进制的平均大小。

          • 不明白为什么会被点赞……我的主要工作项目目前规模不大,二进制目录大小约为96MB。

            虽然我并不认为这特别离谱,但我的Rust(Web服务器)应用即使未优化也轻松低于10MB。

        • 考虑到你打包的是一个不打算独立安装在其他计算机上的完整运行时环境,67MB其实不算太糟糕。

          Go二进制文件例如就有20MB。

        • 哈哈,没错,这确实离谱。我甚至不敢说自己属于JS世界!

    • 是的,这里很多人都在说这是AI生成的。可能是完全由AI生成的。

      它说:“现在你可以将你的Node.js应用程序打包成一个可执行文件”,但实际上并没有提供创建二进制文件的命令。比如:

          npx postject hello NODE_SEA_BLOB sea-prep.blob 
              --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
      
  6. Matteo Collina 指出,底层的 Node.js 获取功能实际上是来自 undici Node 客户端的获取功能 [0];并且由于它需要生成 WHATWG 网络流,因此天生比替代方案——undici 请求 [1] 更慢。

    [0] – https://www.youtube.com/watch?v=cIyiDDts0lo

    [1] – https://blog.platformatic.dev/http-fundamentals-understandin

    • 如果有人好奇他们是如何进行测试的,以下是基准测试结果:https://github.com/nodejs/undici/blob/main/benchmarks/benchm

      几周前,我在一台M3 Max MacBook Pro上进行了一些测试。我将他们提供的本地服务器基准测试与网络上的基准测试进行了比较。Undici在本地使用时表现最佳,但Axios在网络上的性能更好。

      我不确定具体原因,但过去一年半以来,我一直成功使用Undici。它无疑已具备生产环境使用条件,但若想榨取每一滴性能,通常需要根据具体使用场景进行一些考量,这与常见情况一致。

  7. 我真希望 ESM 更容易采用。但我们已经到了 2025 年中期,它仍然存在兼容性问题。而且现在越来越多的包只支持 ESM,情况变得更糟。你不得不选择要舍弃什么。我用 TS 编写代码时使用 ESM 语法,但为了保持理智,我仍然将构建目标设置为 CJS。

    从许多方面来看,这场混乱让人联想到Python 2到3的迁移。我希望我们当初能采用双向导入互操作性和双模块发布,并通过平滑过渡实现,而不是这种“新版本只发布ESM”的突然转变。

    • Bun 证明了两个世界可以协同工作,混乱都归咎于 Node.js 方面,而我们开发者却被指责为不采用

    • 你能详细说明你遇到的 ESM 兼容性问题吗?它们与特定库或用例相关吗?

      • 我认为最令人讨厌的两点

        提升/导入顺序,尤其是在进行模拟测试时。

        是否包含扩展名以及使用哪种扩展名,.js 还是 .ts。

  8. 别忘了原生的 TypeScript 转译器,它能大大降低使用 TS 时的复杂性

    • 它会去除 TS,而不是进行转译。

      像TS枚举这样的东西将无法工作。

      • 在Node 22.7及以上版本中,你可以通过–experimental-transform-types CLI选项启用枚举和参数属性等功能(不要与旧的–experimental-strip-types选项混淆)。

      • 不要使用枚举。它们存在一些问题,但能够在不进行构建步骤的情况下运行不包含枚举的 TS 代码,这已经足够成为使用 const 对象的理由。

    • 它仍然不适合使用。我不在乎枚举。但你无法导入没有扩展名的本地文件。你不能在构造函数中定义类属性。

      • 枚举和参数属性可以通过 –experimental-transform-types CLI 选项启用。

        无法在不包含 ts 扩展名的情况下导入 TypeScript 文件确实令人烦恼。不过,TS 5.7 中添加的 rewriteRelativeImportExtensions tsconfig 选项让情况好多了。启用该选项后,TS 编译器不仅会在导入语句中指定 ‘.ts’ 扩展名时停止报错(就像 allowImportingTsExtensions 选项一直允许的那样),而且在编译文件时还会重写路径,确保构建生成的文件具有正确的 js 扩展名:https://www.typescriptlang.org/docs/handbook/release-notes/t

      • 为什么不直接导入带扩展名的文件?这才是 JS(和 TS)导入机制的正确工作方式。

        • 我不得不承认,当我突然被要求在导入时去除所有扩展名时,我有点惊讶。虽然看起来很干净,但这样做对吗?

      • 为什么你会想做这两件事中的任何一件?

        • 这两种都是非常常见的 TypeScript 模式。

          • 不带扩展名导入并非 TypeScript 的特性。Node.js 最初引入了这一功能,但在实现 ESM 时停止了支持。严格模式本身就是一个特性。

            事实是,它们“支持 TS”,但要求使用 .ts 扩展名,而这一要求在 Node.js 添加“TS 支持”之前从未被允许过。_那_部分简直荒谬。

            TS 始终只接受 .js 文件,并正式拒绝在导入中使用 .ts 扩展名。随后 Node 强行要求他们接受这一做法。

          • 可能在某些遗留代码库中较为常见。我建议在新代码库中使用 `erasableSyntaxOnly` 选项。

    • 完全正确。现在甚至不需要使用 –experimental-strip-types 选项了。

  9. 其他人是否也曾偶然发现这类问题?我从未清楚某个功能何时被添加,但总有一种“这是现代技术”的模糊概念。这与我只做 C# 时的情况不同,那时阅读新语言特性会让人兴奋不已。在多语言环境中,单个语言的演进速度之快,让人难以跟上!我通常通过潜移默化或阅读此类博客文章学习(但这属于随机学习)。

    • 我推荐一份优秀的Node每周[0]简讯,涵盖所有Node相关资讯。它已作为可靠的更新来源超过十年。

      [0] https://nodeweekly.com

    • 阅读发布说明就能解决这个问题 😉

      • 哪些发布说明?我得读上百份!

        • 也许这里有个好主意,一个网站能显示自上次使用某项功能以来所有发布说明,去除已被后续版本取代的条目,并按重要性排序。

    • 我真的很喜欢 Node(和 V8),所以每隔一段时间(2-3 个月?)我会阅读他们的发布说明并了解这些内容。

      有时我也会阅读提案,https://github.com/tc39/proposals

      我真的很希望管道运算符能被纳入。

  10. 最重要的是,Node在LTS版本中也支持TypeScript(从v22.18开始)。

    我强烈推荐在 tsconfig 中使用 `erasableSyntaxOnly` 选项,因为 TypeScript 最有用的功能是作为代码检查工具和更智能的代码提示,而不会影响运行时代码:

    https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly

  11. 我认为 Node 正在逐步发展,足以与 Bun.js、Deno 等形成强有力的竞争,因此几乎没有理由进行切换。这种相互竞争对 JavaScript 运行时的持续发展是有益的

    • 确实,这些变化值得欢迎。不过我仍然怀念 Bun 的 `$` 壳函数。将 JavaScript 用作脚本语言非常方便,我真的不想在服务器上运行两个运行时。

    • 在启动新项目时,经过一番研究后我选择了 Deno。NPM 生态系统看起来一团糟;而 Node 的创建者认为 Deno 是未来,并表示它解决了 Node 中的设计缺陷,我没有理由怀疑他。

      • 我真的想使用 Deno,但它目前还不够成熟。Node 可能不是最酷的,但它有效,而且如果你遇到问题,整个互联网都在这里帮助你(至少有 Stack Overflow)。

        我们曾报告过一个问题,它很快就被修复了。但后来我们遇到了通过 TLS 连接(Google Cloud Platform 上的 MySQL)的麻烦,经过长时间调试后发现问题其实不在 Deno,而在于 Deno 使用的 RustTLS。即使是 RustTLS 中的已知问题,如果不知道具体要查找什么,也很难发现。

        后来切换到 Node.js 并使用 TypeScript 运行器反而更快。

        • 有趣。你认为 Deno 在选择组件时做出了有争议的决定吗?

  12. 我已经很久没有接触 Node 生态系统了。这里有很多很棒的东西。

    很难想象这与市场竞争无关。过去几年,Deno和Bun试图抢占部分Node市场,似乎迫使Node开发者加快了步伐。

  13. “使用AsyncIterators进行现代事件处理”部分似乎缺失了内容。

    示例代码会触发事件,但没有代码接收这些事件。希望只是复制粘贴错误,而不是更多由AI生成的垃圾内容充斥互联网。

    • (异步)迭代器本质上是拉取式,不适合用于事件(推送)处理。

      正如另一位网友提到的,它们已经存在多年了。

    • 这显然是AI生成的低质量内容。参见动态导入示例中那个荒谬的尝试,试图条件性地加载SQLite两次。

      功能列表或许对那些不关注新版本更新的人来说还不错,但依我之见,如果你是专业从事Node和JS开发的工作者,应该对这些功能中的大部分,甚至全部都了如指掌。

      • AsyncIterator在Node中不是已经可用好几年了吗?我记得大约三年前就广泛使用过它。

        它确实很棒,但似乎不值得大书特书。实验性内容似乎更值得报道。

        • > AsyncIterator在Node中不是已经可用好几年了吗?我曾大量使用它——我想说——大约在三年前。

          是的。它在 V8/Node.js 中已经存在并相对稳定多年了。

  14. 看到 Node.js 模式在 HN 上成为 #1,我感到很高兴,尽管它在 2012 年至 2018 年间一直被忽视。

    • 无论是浏览器还是 Electron,即使是我们这些讨厌这个生态系统的人,也迫不得已要与之打交道,而如果不得不使用它,至少可以借助新的工具集稍感舒适。

  15. 嗯,我写了不少 Node 代码,这里有很多新内容对我来说是陌生的。比如内置的测试功能。

    另外,我还没跟上node:命名空间。

  16. 我看到两种新兴功能类别,就像在浏览器中一样:

    1. 新技术

    2. 针对已存在功能的装饰性层

    有趣的是,人们在这些两个领域中如何分配优先级

    • 一个人的“装饰性功能层”可能是另一个人眼中的易用性改进。

      • 而在这里讨论的许多案例中,“装饰性功能层”实际上带来了巨大的互操作性提升。

    • > 现有功能的装饰性功能层

      例如?

  17.   try {
        // 并行执行独立操作
        const [config, userData] = await Promise.all([
          readFile(‘config.json’, ‘utf8’),
          fetch(‘/api/user’).then(r => r.json())
        ]);
        ...
      } catch (error) {
        // 带上下文的结构化错误日志记录
        ...
      }
    

    乍一看这似乎没问题,但我对 Node.js 的异步/Promise 辅助函数的一个主要顾虑是,你无法区分哪个 Promise 返回了值或抛出了异常。

    在這個例子中,如果你想處理 config.json 檔案不存在的情況,你需要 somehow 知道 readFile 函式可能拋出的錯誤類型,並 somehow 在 ‘error’ 變數中檢查它。

    当尝试使用类似 Promise.race 的方法来处理承诺完成时,情况会变得更糟,例如:

      const result = Promise.race([op1, op2, op3]);
    

    你需要以某种方式将每个承诺所代表的信息嵌入到承诺结果中,这通常是通过一个包装器来实现的,该包装器将承诺值注入到自己的响应中……这真的很丑陋。

    • 你可能在寻找 `Promise.allSettled`[1])。公平地说,这在使用解构时会变得相当复杂(注意 try-catch 已经不再必要,因为 allSettled 不会“抛出”异常):

        // 并行执行独立操作
        const [
          { value: config, reason: configError },
          { value: userData, reason: userDataError },
        ] = await Promise.allSettled([
          readFile(‘config.json’, ‘utf8’),
          fetch(‘/api/user’).then(r => r.json())
        ]);
      
        if (configError) {
          // 配置错误
        }
      
        if (userDataError) {
          // 用户数据错误
        }
      

      当处理多个并行任务且需要单独处理每个任务的错误时,我更倾向于先启动所有 Promise,然后在所有任务启动后再等待其结果,这样可以使用 try-catch 块或更明确地管理资源:

        // 独立操作的并行执行
        const configPromise = readFile(‘config.json’, ‘utf8’)
        const userDataPromise = fetch(‘/api/user’).then(r => r.json())
      
        let config;
        try {
          config = await configPromise
        } catch (err) {
          // config 操作出现错误
        }
      
        let userData;
        try {
          userData = await userDataPromise
        } catch (err) {
          // userData 操作出现错误
        }
      

      编辑:添加了使用 allSettled 处理错误的示例

      [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe

    • 依我之见,当你在catch块中进行控制流时,你是在与语言对抗。你会失去TypeScript的类型安全,而整个“if e instanceof … else throw e”的操作会产生过多的冗余代码。

      如果配置文件不存在是一个可处理的情况,那么编写一个“loadConfig”函数,使其返回undefined。

  18. 说实话吧。这篇文章有多少是你写的,有多少是 ChatGPT 写的?

    • 说实话吧。你用大语言模型(LLM)写了这条评论吗?

      为什么内容的正确性之外的事情会重要,无论如何,你和作者都需要评估这一点。

      我对这种观点感到厌倦。质疑某件事是如何被写出来的没有价值,只有最终结果才重要。否则,我们也会对铅笔、打字机、词典和拼写检查提出同样的问题,这是一种毫无意义的对纯粹性的追求。

    • 对于后者:绝对全部,虽然我更倾向于 Claude,但它更有其突出的模式。

    • 什么,你不会是在暗示以下这些爆炸性新闻是 GPT 的人造物吧?“这些变化不仅仅是表面上的,它们代表了我们处理服务器端 JavaScript 开发的方式发生了根本性的转变。”

      • 现在我们不得不抛弃整篇文章,因为我们无法确定这些功能是否只是幻觉。

        • 你无法确定作者是否真的了解自己在谈论什么,无论是否涉及AI。

          • 没错!但撰写一篇严重错误的博客文章曾经需要付出实际的努力。博客文章的存在是一种工作证明,表明一个人至少认为自己对某个话题有足够的了解,可以写下来并与他人分享。

            现在,这篇文章的存在只是证明作者拥有足够的 AI 积分,能够向 Claude 发送一些发行说明并生成一些标记,这并没有什么特别之处。

        • 我认为这里有足够多的节点开发者来判断其真实性。

          • 我不确定——很多热门评论都表示这篇文章很棒,他们学到了很多新东西。这很好,只要他们学到的东西是真实的。

  19. 对于一种更现代的.env文件处理方式,包括内置验证和类型安全,请查看https://varlock.dev

    与.env.example(很快就会过时)不同,它使用.env.schema——其中包含作为装饰器注释的额外元数据。它还引入了一种新的函数调用语法,以安全地从外部来源加载值。

  20. 大语言模型(LLM)让这听起来非常壮丽:“node: 前缀不仅仅是一个惯例——它向开发人员和工具明确表明,您正在导入的是 Node.js 内置函数,而不是 npm 包。这可以防止潜在的冲突,并使您的代码在依赖关系方面更加明确。”

    • 不再需要使用 IIFE 进行顶级 await 据称是一个“游戏规则改变者”。

    • 换句话说,这只是一个约定

    • 同意。在首页看到这种粗略的内容令人感到惊讶,但也许这仍然值得,因为可以刺激评论中的讨论?

      • 我从中学到了不少新知识,我并不在乎 OP 在发布之前是否通过大语言模型 (LLM) 进行了过滤。

        • 同感,但我不太接受这样一个想法:即使我学到了之前不知道的东西,但如果我们一直对这种写作方式网开一面,最终会让人感到烦躁——我认为“过滤”可能不是恰当的词汇,我更倾向于“净减少”。添加无意义的内容(有多少是真正改变游戏规则的?)

          我一直在思考的另一种表述是,显然当你保留那些明显降低所有读者信噪比的片段时,这本身就有问题。

          再加上该账号是新注册的,嗯,我希望这不是一个不祥的预兆。*

          * 它确实是,而且已经太晚了。

          • 你可以批评写作本身,而无需质疑其写作方式。对写作工具的猜测除了对作者做出可能毫无根据的价值判断外,毫无意义。

            https://hbr.org/2025/08/research-the-hidden-penalty-of-using

            • 我认为这既是有价值的,同时也是森林为何会变得黑暗的关键。

              我并非在猜测——我不得不频繁处理这些问题,以至于其中的迹象已然显而易见——而这些迹象早已为人所知,例如有人会用“这不仅仅是x,而是y”的口号来为不同模型进行基准测试。

              然而,从严格意义上讲,我是在猜测:我不可能知道是否使用了大语言模型(LLM)。

              因此,当使用大语言模型(LLM)时,我看到越来越多的对话在争论是否合适、是否重要、大语言模型(LLMs)是否好,而且由于任何指出这一点的人都可能是在猜测,现在,反应取决于你最初如何框架这个观察结果。

              例如,在此处,我特意做出一个相对中立的评论,基于我上周的经历(见我稍后发布的另一条评论)

              假设我从不提及LLM,而是将其表述为“这不就是一种惯例吗?”和“为什么会有这么多颠覆性创新?”,观众显然会明白这是使用LLM的必然结果,但同时又显得你在针对某人 (这两种说法是糟糕的写作吗?我只有一位老师会对这种微妙的修饰语感到不满)

              总之,这在某种程度上都是一些抱怨,你说得对,这是正确的回应方式。这里唯一的真正困难在于,如何在不显得是在针对某人的情况下批评写作。

              编辑:好吧,还有一件事:当我看到有人使用大语言模型(LLM),却无法发现线索,甚至无法至少发现线索会削弱写作时,我最担心的是……好吧,他们还错过了什么?大语言模型(LLM)还写了什么,我必须自己去评估?所以,让我感到困扰的并不是文章写得不好(仍然有 90% 以上),而是我不知道什么才是真实的,如果我必须检查一切,那么即使有人给我文章看,也感觉是在浪费时间。

              • 我认为对输出结果的批评是可以接受的。如果你不喜欢风格、格式、词汇选择等,我认为这是公平的,即使这些是表面上的或主观的。艺术往往就是这样。

                对艺术的创作方式进行价值判断是设置门槛。如果这个人是有残疾的,出于无障碍的原因使用大语言模型(LLM),就像使用许多其他工具一样,那又如何呢?我不知道,在我看来这似乎有问题,但我理解对输出的厌恶。

                例如,这可能就像批评霍金没有改变他单调的声音,而是完全使用语音合成器一样。也许这不是最好的类比。

                作者仍然可以根据对输出的批评,选择使用大语言模型(LLM)来调整风格。

      • 我也觉得这难以阅读,我想这就是每天从事这类工作的不利之处,你会真的讨厌看到它。

        这确实表明,如果连95%的HN用户都分辨不出来,那么99%的公众也分辨不出来。这相当令人惊叹。

        • 没错,这大概就是“我每天要看这个好几个小时”的问题——我很难提起这件事,因为有很多人自认为有类似经历却看不到同样的迹象。但说真的,我已经每天花8小时以上在这上面,持续了两年。

          听起来你和我有着同样超现实的体验……它如此显而易见,以至于唯一奇怪的是人们没有提到它。

          这些特征如此难以察觉,比如,六周前我一直想反复强调“这不是X,而是Y”这件事,我以为这是GPT-4.1的特征。

          然后,我发现了这个鲜为人知的绅士在做着上帝的工作:大量的基准测试,其中之一是“不是 X,而是 Y”的垃圾,结果发现还有 40 多个模型领先于它,包括 Gemini(在我看来,这是个垃圾机器)和 Claude,我永远不会猜到是 Claude。https://x.com/sam_paech/status/1950343925270794323

      • 我对此越来越感到绝望。

        森林正在迅速变暗。

        我敢说,7 月份首页帖子中有 15% 无法通过“避免使用众所周知的语言模型(LLM) 口号”的检查。

        昨天晚上,我的 TikTok 推荐页面上大约 30% 的视频是 Veo 3 生成的种族歧视和/或同性恋歧视视频。

        去年,我以为社会惯例会打击这种现象(即,如果你能证明这是大语言模型(LLM) 的输出,就会让人们看起来很愚蠢,因此人们不会这样做)。

        最新一轮的发布足够智能,且已足够普及,似乎我们已达到这样一个时刻:大多数人不知道最新一轮的“特征”,且它通过了他们的图灵测试,因此没有足够的羞耻感来阻止它成为内容的相当一部分。

        上周我对“粗制滥造”发表了类似评论,但犯了错误,提到了关于Markdown格式的旁枝末节。结果被大量点赞并遭到版主批评,因为人们蜂拥而至指责这很刻薄,他们是新用户所以我们应该更友善,此外HN上的Markdown语法很难,而且似乎英语是他们的第二语言。

        文章的后半部分完全由四个项目列表组成。

        • 这篇文章中充斥着太多破绽,而且这些破绽并非新鲜事物。光是写作风格本身就是一个破绽,渗透在每一个字里行间。

          我也很惊讶HN用户似乎没有注意到或不在意这些破绽,我认为这使得文章难以阅读。

          我本可以写一篇关于这个的文章,但这样做只会让人们避免那些破绽,我不确定这是否是一种改进。

        • 我迫切期待着巴特勒的圣战;_;

  21. 我是不是唯一一个觉得普通JS很好用,不喜欢ESM的人?或者换个说法,我根本看不到在Node中使用ESM的必要性。更不用说浏览器了,想象一下通过网络加载大量模块而不是打包它们

    • ESM导出是静态的,而导入也大多是静态的,这使得在打包过程中可以去除大量无用代码。仅此一点就是一个巨大的优势,我认为。

    • 你仍然可以打包它们,也许你甚至应该这样做。Webpack仍然做得很好。也可以移除未使用的部分。

      • 如果有什么的话,Bun证明了两个世界可以共存,尽管网络标准的倡导者们怎么说

        • > 如果说 Bun 证明了两个世界可以共存,尽管网络标准的支持者们怎么说

          当然,但 Bun 的实现往往是一团混乱。我更喜欢它们分开。

          注:这并非针对 Bun。我是 Bun 的粉丝,也欣赏其团队的创新精神。

    • 是的。

      此外,CommonJS 不支持树摇动。

      • ESM也不支持,如果你使用* as导入。不过我相信它可以被修改以支持树摇动

        编辑:证明我观点的证据在于许多库都有一个开放的问题,因为即使是ESM,它们也不支持树摇动

        • import * as 仍然可以进行树摇动,浏览器会在内存中进行树摇动,因为导入形式为弱引用。

          打包工具可以进行树摇动,只是实现起来更困难,因此它并不总是优先功能。esbuild 在过去几年中做了大量工作,在更多场景下支持 import * as 的树摇动。当然,它还无法在所有场景下进行树摇,但仍在不断改进。

  22. 该是时候了!对 ESM 的采用拖延不前简直是疯狂。npm 仍停留在 commonjs 阶段的情况相当普遍。某种程度上,jsr 的出现令人欣慰。

    • 我认为工具开发者在抽象问题方面做得太好了,当然这并不是在责怪他们。

      估计 70% 到 80% 的 JS 用户对差异几乎一无所知,因为他们的工具让一切正常运行。

  23. 你好,关于流的互操作性,我之前已经记录了如何处理文件流,这是在实验 Next.js 的旧系统(基于 Node.js)和新系统(基于 Web)之后:https://www.ericburel.tech/blog/nextjs-stream-files#2024-upd…。总结来说,使用 Node.js 的 fs 模块创建一个 Web 流,而不是创建一个 Node.js 流,只需使用 “const stream = fileHandle.readableWebStream()” 即可。

  24. 看到 Node.js 在追赶中令人欣慰,尽管 Bun 似乎拥有更多的开发者支持,因此我通常会默认使用 Bun,除非需要在 Node.js 兼容性更好的环境中运行。

  25. 在第10节中应添加鼓励开发者在抛出新Error实例时传入cause选项的提示。例如

    new Error(“发生了一些问题”, {cause:innerException})

  26. 我认为顶级异步是不好的,因为它无法与 JavaScript 浏览器兼容。

    Node 测试我也觉得不太好,因为在同构应用中,你将有两种测试语法。

    我认为权限是核心问题,即使我们在 Docker/开发容器中运行应用程序。

    别名很不错,node:fetch,但我想这会破坏所有同构代码。

  27. 这里有一些不错的内容。我之前对异步迭代器一无所知,但过去曾用生成器做过类似的事情。

    其中一些内容似乎借鉴了 Bun(除非我之前不知道这些内容?)。这似乎是 JavaScript 生态系统不断更迭带来的积极影响。

  28. 作为主要后端开发者,我想发表一下我的看法:

    > 顶级 await:简化初始化

    这让我感到非常糟糕。没有一个合适的入口函数来让开发者完全控制在其他事情发生之前需要执行的所有操作,这是不可接受的。例如创建数据库连接、启动服务、连接API、预热缓存等。所有这些操作都应该运行(可能并发)。

    在这种情况成为可能之前,即使使用了顶级 await,我个人仍不得不认为 Node.js 存在缺陷。

    > 使用 Node.js 内置测试运行器进行现代测试

    抱歉,但请专注于做好一件事。

    > 异步/等待与增强的错误处理

    我希望 Node.js 能拥有类似 JVM 的日志记录和堆栈跟踪(包括原因嵌套)…

    > 6. 工作线程:CPU密集型任务的真正并行处理

    这是最大的问题。应该有一个内置支持并行处理的替代方案,不需要我手动进行序列化/反序列化。

    其他方面都有不错的进展。但上述问题令人沮丧。

  29. 感谢分享。这对刚开始重新学习 Node.js 的我非常有帮助。

  30. Node.js 现已支持有限的 TypeScript 功能并内置 SQLite,因此非常适合小型/个人导向的 Web 项目。

  31. 我感觉 Node 和 Deno 的规范正在某种程度上融合(这是件好事)

  32. Node.js 有一天会吸收 TypeScript 并将其作为默认语言吗?

  33. JavaScript 缺少一些能将其提升到更高水平的功能,但我还不确定具体是什么。

    也许它需要一个编译时宏系统,这样我们就能像Java一样拥有神奇的依赖注入注解、面向方面编程(AOP)和JavaScriptBeans(你知道你想要它!)。

    或者也许它需要朝Ruby/Python/SmallTalk的方向发展,添加适当的元编程,这样我们终于可以拥有JavaScript on Rails,或者也许……Django?

  34. Deno有沙箱功能

  35. 等你读完这篇指南并更新代码库时,最先进的JS最佳实践至少已经更新过两次

  36. 内置的测试执行器是否会收集并报告测试覆盖率?

  37. 那是什么

  38. vb

  39. k

  40. “SlopDetector 检测到 2 个 ‘seamlessly’ 和 7 个 ‘em-dash’,是否继续?”

    • 大喊“你不仅仅是在编写现代代码——你正在构建更易于维护、性能更优且与需求更契合的应用程序”

    • 我最近使用破折号只是为了触发像你这样的“锡箔帽”用户。

    • 顺便说一下,我使用破折号

      • 我以前也用过。 : (当人们开始认为只有大语言模型(LLMs)才会使用它时,我不得不停止使用它。

  41. 除非它改变了 NodeJS 的处理方式,否则你不应该使用 Promise.all()。因为如果多个承诺被拒绝,那么第二个拒绝会触发一个未处理的拒绝事件,默认情况下会导致服务器崩溃。请使用 Promise.allSettled() 代替。

    • Promise.all()本身并不会导致unhandledRejection事件。任何未处理的拒绝承诺都会抛出unhandledRejection,而allSettled只是为你收集所有拒绝和履行结果。Promise.all仍有合法的使用场景,就像Promise.allSettled、Promise.race、Promise.any等一样。它们各自满足不同的需求。

      亲自尝试一下:

      > node

      > Promise.all([Promise.reject()])

      > Promise.reject()

      > Promise.allSettled([Promise.reject()])

      Promise.allSettled 绝不会导致未处理的拒绝事件,因为它在任何情况下都不会被拒绝。

    • 这感觉不对劲,所以我测试了一下。

          process.on(“uncaughException”, (e) => {
              console.log(“uncaughException”, e);
          });
      
          try {
              const r = await Promise.all([
                  Promise.reject(new Error(‘1’)),
                  new Promise((resolve, reject) => {
                      setTimeout(() => reject(new Error(‘2’), 1000));
                  }),
              ]);
      
              console.log(“r”, r);
          } catch (e) {
              console.log(“catch”, e);
          }
      
          setTimeout(() => {
              console.log(“setTimeout”);
          }, 2000);
      

      输出:

          alvaro@DESKTOP ~/Projects/tests
          $ node -v
          v22.12.0
      
          alvaro@DESKTOP ~/Projects/tests
          $ node index.js 
          catch Error: 1
              at file:///C:/Users/kaoD/Projects/tests/index.js:7:22
              at ModuleJob.run (node:internal/modules/esm/module_job:271:25)
              at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:547:26)
              在 async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:116:5) 中
          setTimeout
      

      所以,不行。这些承诺被忽略了。

      • 他们确实改了!很好。

        我之前确实遇到过类似的崩溃,而且可以找到多篇文章描述这种行为。这种情况存在了相当长一段时间,所以我以为他们不会修复它,因此没有跟踪它。

        • 也许你把它和别的什么搞混了?我试过降级到 Node 8(2017 年),行为仍然相同。

          可能是用户空间承诺(如 Bluebird)中的 bug?或者是在承诺仍处于实验阶段的旧版 Node 中?

          我喜欢解谜!

          • 奇怪。我刚刚用v8试了一下,它的行为和我记忆中的不一样,也不像我在线上找到的一些描述。我记得这件事,因为当我发现这种行为时,我感到非常震惊,这让我重新检查了所有的代码,将任何Promise.all()都替换成了Promise.allSettled()。而且不只是我,这篇博客文章也提到了这种行为:

            https://chrysanthos.xyz/article/dont-ever-use-promise-all/

            也许当时我的 bug 是其他问题,我找到一个声称存在这种行为的来源,于是修改了代码,结果 bug 恰好消失了?

      • 拼写错误?“uncaughException”

        • 哦,谢谢!如果真的未捕获异常,本应看到默认处理程序。

    • 使用 Promise.all() 时,如果每个 Promise 都有自己的 .catch() 处理程序,则不会完全失败。

      • 足够微妙,如果你不希望出现这种行为,你会在第一次尝试后就不再这样做。

  42. 我喜欢 Node 的内置测试功能以及它与 VSCode 测试运行器的集成。但我仍然怀念 Jest 匹配器。Vitest 团队为自己的使用移植了 Jest 匹配器。我希望 Jest 匹配器与 Node 测试之间也能有类似的兼容性。

    • 目前对于非常小的项目,我使用 NodeJS 的内置测试工具。

      但对于更大更复杂的项目,我倾向于使用 Vitest。下载大小为 40MB,且大部分依赖项重量落在 Vite(33MB,且我可能已经直接安装了)上,因此这不是一个太重的依赖项。

      • 它基于 Vite,而打包工具在我后端没有用武之地。Vite 基于 Rollup,Rollup 又使用了其他工具如 SWC。我希望使用 TypeScript 项目和 npm 工作区,而 Vite 似乎并不关心这些。

    • 与 Jest 相比,Node 测试中的断言感觉“技术上正确但有点丑陋”,但我还是会使用它

      • 是的,但考虑一下这个 Jest 代码,在 Node 测试中复制这样的代码是痛苦的。测试代码应该像 DSL 一样,应该非常容易阅读。

                    expect(bar).toEqual(
                        expect.objectContaining({
                            symbol: `BTC`,
                            interval: `hour`,
                            timestamp: expect.any(Number),
                            o: expect.any(Number),
                            h: expect.any(Number),
                            l: expect.any(Number),
                            c: expect.any(Number),
                            v: expect.any(Number)
                        })
                    );
        
  43. 既然Bun是新项目更好的选择,为什么还要使用Node呢?

    • 我坦率地说,我更喜欢X世代编写的项目(Node和Deno),而不是Z世代编写的项目(Bun)。

      Bun 获得风险投资支持,让我可以借此为这种情感偏好披上理性的外衣。

      • 我大概明白你的意思,Bun 让我感到有些不适,它就像一个时髦的 ORM 或前端框架,而 Node 和 Deno 则试图成为运行时应有的无聊基础设施。

        这并非说Deno没有努力,但他们的部分营销策略让人感觉像是“大家好,我是新来的”,仿佛在试图跟上JS的热潮却不知如何下手。

        • 没错,就是这样。我不想用一个可爱的运行时,我想要一个无聊但可靠的。

          Deno 有一个可爱的吉祥物,但它的一切都表明“相信我,我并不令人兴奋”。Ryan Dahl 本人也有“我之前做过”的背景。

    • 因为 Bun 仍然远不够成熟(而它所基于的栈更是如此——Zig 甚至还未达到 1.0 版本)。

      因为它的 Node.js 兼容性并不完美,因此如果你出于某种原因在生产环境中运行 Node(例如因为它是 Electron 应用程序),你可能希望在开发环境中使用相同的东西,以避免“为什么它不工作??”的困惑。

      因为 Bun 的 IDE 集成不如 Node 那么好。

    • 我已经有几个月没用过它了,但根据我的经验,它的包/单仓库管理功能与pnpm相比简直糟糕透顶(单仓库包之间的依赖关系会泄露,命令行界面存在 bug 等),bun –bun 是个蠢货,包的构建脚本经常出问题,因为它们使用了 Node,所以我不得不同时安装 Node 和 bun 才能正常安装,包经常崩溃,因为它们不兼容 bun,大多数有用的优化最终都会被纳入 Node,安装 ramda 或其他包只需 2 秒,我信任它,所以 bun 的随机辅助库几乎没有用处。

      • 过去几个月我们在bun安装方面取得了很大进展:

        – 类似pnpm的符号链接安装方式,用于node_modules目录

        – 目录管理

        – yarn.lock支持(今日晚些时候推出)

        – bun审计

        – bun更新——交互式模式

        – bun why <pkg> 帮助查找包安装的原因

        – bun info <pkg>

        – bun pm pkg get

        – bun pm version(用于版本升级)

        下周我们将支持 pnpm 锁定文件迁移。为此,我们正在编写一个 YAML 解析器。这也将解锁在 JavaScript 运行时导入 YAML 文件的功能。

    • 既然 deno 2 是新项目更好的选择,为什么还要使用 bun?

      • 既然 node 22 是新项目更好的选择,为什么还要使用 deno 2?

        (闭环)

    • 因为 Bun 是用一种不稳定的语言(Zig)编写的,并且使用 WebKit。开发者友好的特性无法掩盖这一点。我也不确定他们能否实现商业化,这意味着如果资金枯竭,它可能会消失。

  44. 我现在的 Node.js 模式是先安装 Bun。

  45. 我一直无法入门 Node,但最近开始尝试 Bun,感觉非常不错。我仍然不认为我会给 Node 一个机会,但也许我错过了什么。

  46. 当前的 Node.js 是否比 .NET 6/7/8/9 更优越?为什么或为什么不?

    • Node.js是一个运行时环境,而非编程语言。它功能强大,但如往常一样,这取决于你的需求、现有资源和技术掌握程度,ASP.NET Core也是一个非常好的选择。

      • > ASP.NET Core也是一个非常好的选择。

        我发现这并不完全正确。

        • 最近?

          根据我的经验,ASP.NET 9 比 Node.js 生产力高得多且功能更强大。它拥有更友好的开发体验,编译速度更快,部署速度更快,启动速度更快,响应速度更快,内置更多功能,等等,等等……

          缺点是什么?

          • 编译速度和主观的开发体验(DX)观点非常有争议。

            npm 包的丰富性是使用 Node 的好理由。它几乎包含一切。

            • 它有_糟糕的_未完成版本,所有这些都与其他内容微妙地不兼容。

              我经常看到一些流行的包是由一个人或一个小型志愿者团队开发的,而他们的优先事项并非让东西正常工作。

              我还注意到,NPM 包几乎没有“远见”或规划……因为它们只是有人需要解决的一个问题。没有一个统一的愿景或企业计划作为驱动力,因此你得到的是支持、兼容性、生命周期、支持等方面的随机拼凑。

              这或许很有趣,如果你喜欢整天面对选择的组合爆炸和兼容性补丁的调试,而不是交付像“商业价值”这样无聊的东西。

              • 如果你愿意坚持使用纯粹的MS库……

                我以前也这么认为,但当你看到像Mediatr、Mass Transit和Moq这样的库开始或计划收费时,我对整个生态系统的状况并不乐观。

    • 不。因为C#虽然远非完美,但仍比JS(甚至TS)好得多,而且.NET标准库自带大量现成功能。此外,JS的包生态系统,坦白说,简直疯狂;一切都经常出问题。运行一个几年未维护的随机Node.js项目的成功概率相当低。

    • 根据我的经验,不是。

      它仍然是单线程的,仍然使用数百万个小型文件(导致启动非常缓慢),仍然因为缺乏“内置功能”而基本管理极不一致,等等……

      • 你可以将所有内容打包到一个文件中,这样就不再是单线程了。有一个叫做 worker_threads 的东西。

        但是是的,确实有缺点。但你提到的最大缺点并不成立。

        • > 你可以将所有内容打包成一个文件

          这是我第一次听说这个方法,快速搜索后发现NestJS生态系统中存在多种相互矛盾的“方法”,且没有明确指示哪种方法真正有效。

              nest build --webpack
              nest build --builder=webpack
          

          … 当然,使用这两种方法时我都会遇到错误,而使用普通的“nest build”则不会遇到这些错误。(错误信息还贴心地只指定了源代码中的_目录_,而不是文件名!这是什么情况?)

          这是因为 NestJS 是一个专为爱好者设计的“灵活脚本系统”,允许他们在生产服务器上实时编辑 API 控制器脚本,而这是它第一次被真正构建,还是……是因为 webpack 与某个包存在一些模糊的兼容性问题?

          ……还是因为我在某个 TypeScript 配置文件中使用了“错误”的符号?

          谁知道呢!

          > 有个东西叫 worker_threads。

          它们与 .NET 运行时和 ASP.NET 完全不同,后者采用对称线程模型,请求默认在线程池中处理。Node.js 允许将“特殊”计算任务卸载到 worker,但不包括 HTTP 请求。这些 worker 线程只能通过字节缓冲区与主线程通信!

          在 .NET 环境中,我可以简单地使用并发字典或其他类似的共享数据结构……而且它就是能正常工作。天啊,我甚至可以轻松地使用并行 worker 处理单个 IEnumerable、列表或数组。

          • 如果你读过我的评论,你会发现我提到过缺点:

            “但确实存在缺点。但你提到的最大缺点并不成立。”

            我的意思是……你所说的并不正确。即使在你回复之后,它仍然不正确。你在后续回复中提到了些许缺点……但再次强调,你最初的回复并不正确。

            仅此而已。我承认这些缺点,但我的观点仍然不变。

  47. 持续的更新。

  48. 我们无法逃避AI生成的垃圾内容,对吧?

    2022年前的在线写作是信息时代的低成本钢材。如今这些模型将全部基于自身输出进行训练。此举将带来何种后果?

  49. 如果你不得不花时间强制执行这些模式,那么你使用的技术可能已经布满了数百个陷阱。

    与其将重点放在赚钱上,不如浪费时间在代码的重新排列上,成为一个专注于细节而非交付的架构师。

    2025年仍在服务器端使用Node.js和JavaScript可能是最大的错误之一。

    • 当JS生态系统尚不成熟时,在后端使用JS或许是个更大的错误。临时解决方案的层层叠加令人眼花缭乱。尽管我们或许可以追溯得更远,质疑JS被添加到浏览器时是否也是个错误。

      我常想象一个假设的平行历史场景:如果Java以更审慎的方式被引入浏览器会怎样。糟糕的沙箱机制、Netscape插件模式,以及Sun的许可需求与微软的实践方式,最终毁了这一切。

      • > 当JS生态系统尚不成熟时,在后端使用JS或许是一个更大的错误。

        我看到它已被奥地利国家广播公司使用超过25年。最初基于Rhino,因此也与你们喜爱的Java混合在一起。看不到大问题,因为它已经稳定运行了这么久。

    • 你在说什么呢。它只是列出了现代 Node.js 的功能,没什么好强制的。

      • 再读一遍评论:

        > 也许你使用的技术充满了数百个陷阱

        考虑到整个生态系统及其语言非常容易自毁,Node.js 中的“现代功能”毫无意义。

  50. [已删除]

    • 这难道不是今天用大语言模型(LLM)编码代理完全可以解决的少数问题之一吗?

      • 理想情况下,一个代码修改器可以解决这个问题,但两个模块系统由于动态编程的原因而不兼容。

        • 没错,这些转换对于代码修改器来说有些太棘手了,但我猜对于大语言模型(LLMs)来说,它们仍然足够机械化。

    • “任何与我意见相左的人都是初级工程师。”

      仅仅因为新功能无法轻松融入旧代码库,并不意味着它是个糟糕的功能。

      • 这显然是个糟糕的设计。根本没有必要设计如此糟糕的模块系统,并无端破坏他人的工作成果。

        是的,这100%是初级、业余的心态。我想你喜欢无意义的劳作而不完成事情。

        • ESM 实际上是一个标准。你可以抱怨个够,但最终还是会采用它。

          • 其实不然,从我所见,作者基本上被迫同时发布两种版本,这不过是又一次分裂。那些停止发布CJS的库我们从未采用,因为我们不会为了这种毫无意义的初级态度而放弃成熟技术。

            不明白你为何有此看法,我这边正在实际发布。

  51. 又是一群本应专注于if语句、for循环、数组和函数的人,却在表现出“架构宇航员”的行为。

    • “架构宇航员”这个术语我之前没听过,但能理解其含义。不过我在这篇文章中并未看到这种现象。这篇文章对Node.js的新特性做了一个不错的概述……我已经有几年没碰Node.js了,所以这篇文章还挺有用的。

      • 这个术语源自Joel Spolsky的一篇文章(至少我是从那里知道的)。这是一篇值得一读的文章:https://www.joelonsoftware.com/2001/04/21/dont-let-architect

        不过感觉与本文内容无关。

      • 这是一篇不错的文章,涉及一些历史背景和日益增长的公众认知。我建议深入研究,它可以追溯到至少CPP和Smalltalk时代。

        虽然我能理解“我们需要像Node这样的好工具,以便更轻松地编写解决实际业务问题的应用程序”这一观点,但在我看来,这恰恰相反。

        我唯一需要做的就是从文件中导入一堆函数,

        “import * from ‘./path’”

        任何比这更复杂的操作都是在为一个不存在的问题寻找解决方案

        • 这不正是推荐的语法吗?你能解释一下文章中哪些内容是“为一个不存在的问题寻找解决方案”吗?

        • 你读过这篇文章吗?你的评论似乎与文章内容完全脱节——大多是低级别的细节或可以替代你可能已经使用的库的内容

    • 什么?这是对编程语言运行时提供的现代功能的概述。你是说作者不应该浪费时间写这些内容,而应该写循环语句吗?还是说语言运行时核心开发者不应该专注于架构,而应该去写 for 循环?

    • Node.js 做对的一件事就是流(streams)。(还记得 substack 的演讲“Thinking in streams”吗?)看到他们继续推进这一方向是件好事。

      • 为什么?为什么流比数组更好?为什么实时循环和遍历缓冲区的概念不够用?

        • 我认为有几个原因。首先,数据流的抽象在程序处理多个实时循环时非常有用。例如,为数据流添加超时、在不同数据流处理器之间切换、将数据流拆分为两个流或将两个流合并为一个流,以及在可观察模式、Unix 管道和更广泛的基于事件的系统中常见的所有模式,都比在实时紧凑循环中更好地通过基于推送和拉取的数据流进行建模。其次,与使用 map 或 forEach 方法遍历数组相比,通常更倾向于使用 for 循环;与使用 while 循环相比,通常更倾向于使用 for 循环;与使用 goto 语句相比,通常更倾向于使用 while 循环。这是因为它减少了人类管理的控制流记录工作,而这正是人类容易引入逻辑错误的地方。最后,因为编写和维护流处理代码通常比编写和维护针对缓冲区的实时循环所需的人力更少。

          希望这有帮助! 😀

        • 流具有背压机制,使下游能够告知上游限制其流速。这避免了许多与排队理论相关的问题。

          这也会自动发生,流的用户无需关心这些细节。

        • 流并不一定总是比数组更好,当然这取决于具体情况。它们是不同的概念。但如果你遇到需要处理数据流、又不希望在处理前将所有数据缓冲在内存中的场景,流式的抽象概念会非常有用。

        • 为什么数组比指针运算和手动管理内存更好?因为它是一种更高层次的抽象,让你摆脱低级细节,并提供新的思考和编码方式。

          流可以被管道传输、拆分、合并等。你也可以用数组实现这些功能,但需要自己处理大量繁琐的细节。此外,流还支持背压信号。

          • 背压信号可以通过自定义的“事件循环”和数组语法来处理。

            手动管理内存实际上几乎总是比 Node.js 和 Java 等语言提供的内存管理机制更好。我们作为社会取得成功,并非因为这种机制,而是尽管存在这种机制。

            存在一个收益递减点,例如虚拟内存与物理内存寻址的差异,但即使如此,了解正在发生的事情仍极具价值。这样当你的“魔法宇航员代码”在 SGI 系统上无法运行时,我们就能明白原因所在。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

链接收藏


京ICP备12002735号