《Node.js 设计模式》一书中关于 Node.js 基础知识的 5 个 TIL

2025-06-07

《Node.js 设计模式》一书中关于 Node.js 基础知识的 5 个 TIL

这周我开始读《Node.js 设计模式》。我拿到的是第三版,还没花时间研究它和之前的版本有什么变化。前六章涵盖了基础知识,之后才开始深入探讨“设计模式”这个内容,所以这些笔记都来自这本书的前半部分。

1.libuv反应堆模式

libuv我经常听说它是一个低级 Node.js 库,但现在我对它的功能有了些了解。正如书中所述:

Libuv 代表 Node.js 的底层 I/O 引擎,可能是 Node.js 构建所基于的最重要的组件。除了抽象底层系统调用之外,Libuv 还实现了反应器模式,从而提供了用于创建事件循环、管理事件队列、运行异步 I/O 操作以及将其他类型的任务加入队列的 API。

反应堆模式与多路分解、事件队列和事件循环一起,是其工作原理的核心——将异步事件送入单个队列,在资源释放时执行它们,然后将它们从事件队列中弹出以调用用户代码给出的回调,这是一种紧密协调的舞蹈。

替代文本

2.模块设计模式

我对 CommonJS 模块和 ES 模块的区别比较了解。但我很喜欢 CommonJS 中对 5 种模块定义模式的清晰阐述:

  • 命名导出:exports.foo = () => {}
  • 导出函数:module.exports = () => {}
  • 导出一个类:module.exports = class Foo() {}
  • 导出实例:module.exports = new Foo()类似于单,除非由于同一模块的多个实例而导致。
  • Monkey 修补其他模块(对nock有用)

在 ES 模块中,我很喜欢“只读实时绑定”的解释,对于从未见过它并且一直将模块视为无状态代码块的人来说,这看起来很奇怪:

// counter.js
export let count = 0
export function increment () {
   count++ 
}

// main.js
import { count, increment } from './counter.js'
console.log(count) // prints 0
increment()
console.log(count) // prints 1
count++ // TypeError: Assignment to constant variable!
Enter fullscreen mode Exit fullscreen mode

这种可变模块内部状态模式在Svelte 和 Rich Harris 的作品中很常见,我很欣赏它让代码看起来简洁明了。我不知道这种模式是否存在可扩展性问题,但目前为止,对于 ES 模块开发者来说,它似乎运行良好。

我喜欢的最后一个重要主题是 ESM 和 CJS 互操作问题。ESM不提供require__filename__dirname因此您必须在需要时重建它们:

import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url) 
const __dirname = dirname(__filename)

import { createRequire } from 'module'
const require = createRequire(import.meta.url)
Enter fullscreen mode Exit fullscreen mode

截至撰写本文时,ESM 还无法原生导入 JSON,而 CJS 可以。您可以使用require上面的函数解决这个问题:

import { createRequire } from 'module'
const require = createRequire(import.meta.url) 
const data = require('./data.json') 
console.log(data)
Enter fullscreen mode Exit fullscreen mode

你知道吗?我不知道!

相关新闻:Node v14.13 将允许从 CJS 模块进行命名导入- 这可能是 ESM 在 Node.js 中“正常工作”的最后一步

3. 释放 Zalgo

在 Node.js 中,API 通常是同步或异步的,但 TIL,你可以设计同时同步和异步的 API :

function createFileReader (filename) { 
  const listeners = [] 
  inconsistentRead(filename, value => {
    listeners.forEach(listener => listener(value)) 
  })
  return {
    onDataReady: listener => listeners.push(listener) 
  }
}
Enter fullscreen mode Exit fullscreen mode

这看起来很无辜,除非你将其用作异步然后同步:

const reader1 = createFileReader('data.txt')  // async
reader1.onDataReady(data => {
   console.log(`First call: ${data}`)
   const reader2 = createFileReader('data.txt')  // sync
   reader2.onDataReady(data => {
     console.log(`Second call: ${data}`) 
   })
})
// only outputs First call - never outputs Second call
Enter fullscreen mode Exit fullscreen mode

这是因为 Node 中的模块缓存使得第一次调用异步,而第二次调用同步。izs在一篇博文中将此称为“发布 Zalgo” 。

您可以通过以下方式将 Zalgo 关在笼子里:

  • 对同步 API 使用直接样式函数(而不是延续传递样式
  • 通过仅使用异步 API、使用 CPS 以及使用以下方式延迟同步内存读取,使 I/O 完全异步process.nextTick()

对于 EventEmitter Observers 也可以采用与回调相同的思路。

当必须以异步方式返回结果时,应该使用回调,而当需要传达某事发生时,应该使用事件。

您可以将观察者模式和回调模式结合起来,例如,使用既能实现更简单、更关键的功能又能实现高级事件的回调的glob包。.on

关于 ticks 和微任务的说明:

  • process.nextTick设置一个微任务,在当前操作之后、任何其他 I/O 之前执行
  • setImmediate在所有 I/O 事件都处理完毕后运行。
  • process.nextTick执行得更早,但如果花费的时间太长,则有 I/O匮乏的风险。
  • setTimeout(callback, 0)又落后了一个阶段setImmediate

4. 管理异步并限制并发async

使用 Node.js 时,很容易引发竞争条件,并意外启动无限并行执行,导致服务器瘫痪。Async提供了一些久经考验的实用程序来定义和执行这些问题,特别是那些提供有限并发性的队列。

本书将逐步讲解一个简单的网络蜘蛛程序的四个版本,以阐明需要管理异步进程的动机,并描述在规模化过程中出现的细微问题。说实话,我无法做到面面俱到,我不想直接抄写网络蜘蛛项目的所有版本和讨论内容,因为那占了本书的很大一部分,你只能自己去读这些章节了。

5. 流

我经常说,Streams 是 Node.js 中最好、最难破解的秘密。是时候学习它了。Streams 比全缓冲区更节省内存和 CPU,而且更易于组合

每个流都是 的一个实例EventEmitter,可以流式传输二进制数据块或离散对象。Node 提供了4 个基本抽象流类

  • Readable(您可以在流动(推)或暂停(拉)模式下阅读
  • Writable- 你可能熟悉res.write()Node 的http模块
  • Duplex:可读可写
  • Transform:一种特殊的双工流,具有另外两种方法:_transform_flush,用于数据转换
  • PassThroughTransform不进行任何转换的流 - 对于可观察性或实现延迟管道和惰性流模式很有用。
import { PassThrough } from 'stream'
let bytesWritten = 0
const monitor = new PassThrough() 
monitor.on('data', (chunk) => {
  bytesWritten += chunk.length 
})
monitor.on('finish', () => { 
  console.log(`${bytesWritten} bytes written`)
})
monitor.write('Hello!') monitor.end()

// usage
createReadStream(filename)
 .pipe(createGzip())
 .pipe(monitor) // passthrough stream!
 .pipe(createWriteStream(`${filename}.gz`))
Enter fullscreen mode Exit fullscreen mode

izs 推荐使用minipass,它实现了 PassThrough 流,并具有一些更强大的功能。其他一些有用的流实用程序:

尽管作者确实建议使用本机stream.pipeline函数来最好地组织管道和错误处理。

文章来源:https://dev.to/swyx/5-tils-about-node-js-fundamentals-from-the-node-js-design-patterns-book-4dh2
PREV
开发者技术策略指南
NEXT
35年坚持的35条原则