跳转至

Node.js 与 npm — 单线程服务器的悖论,和一棵依赖树的管理学

这一章讲两件事: Node.js 凭什么用一条线程当服务器(答案不是「它快」,而是「它不让 CPU 干等」); 以及 npm 这套「package.json + node_modules + lockfile」三件套,怎么把「装依赖」这件小事 变成了一棵需要精心管理的树。读完你会明白 import 一行代码背后,文件系统里发生了什么。

1. 先看现象:一个为「不等待」而生的运行时

2009 年,Ryan Dahl 发布 Node.js。他的出发点不是「喜欢 JavaScript」,而是一个工程上的不满: 老牌的 Apache 服务器处理每个请求都要等磁盘、等网络,CPU 空转;Apache 的对策是多线程—— 一个线程等 I/O(输入输出,和磁盘网络的往来数据),就把 CPU 让给别的线程。线程本身有开销, 而且线程一多,切换和同步的复杂度陡增。

Dahl 的思路反着来:不要用线程掩盖等待,干脆不要让代码等待。 他需要的语言要「写异步服务器逻辑足够顺手」——JavaScript 恰好是:它本来就是单线程、 事件驱动的语言,回调是它的母语。1

对比一下两种服务器代码(原书给的例子,我们走一遍主线):

// 阻塞式:读文件这行代码不返回,整个进程就停在这
function indexHandler(req, res) {
  const html = readFile("index.html", "utf8");   // CPU 干等几千个时钟周期
  res.send(html);
}

// Node 式:读文件交给操作系统,代码立刻返回,读完了再「回调」
function indexHandler(req, res) {
  fs.readFile("index.html", "utf8", (html) => {
    res.send(html);
  });
}

第二段里 fs.readFile 的第三个参数就是那个关键角色——回调函数: 「文件读好了,回头调用这个函数,把内容交给它」。2 从这个函数返回的瞬间,CPU 就可以去接下一个请求。I/O 是并行世界的事, JavaScript 线程只管「登记未来」。

这就是本章第一根支柱,也是全书反复出现的一句口号:不要阻塞线程(Don't block the thread)。 现代写法会用 promise 和 async/await(第 8 章整章),使的是同一个原理:3

async function indexHandler(req, res) {
  const html = await fsPromises.readFile("index.html", "utf8");
  res.send(html);
}

主线例子:一次 import 的旅程

本章其余部分都在追踪这一个问题的答案:

  你的代码里写着:import express from "express";
      │
      ├─ Node 看到这个字符串("express" 不是路径,不是核心模块)
      ├─ 它去哪找这个文件?(§3 模块解析)
      ├─ 找到的包里,哪个文件是入口?(§3 exports/main)
      ├─ express 自己还依赖几十个包,它们在哪?(§2/§3 依赖树)
      ├─ 版本号 "^5.1.0" 是什么意思?(§4 semver)
      ├─ 怎么保证队友装的和你一样?(§4 lockfile)
      └─ 包有 bug 想自己改,怎么办?(§5 fork 与打补丁)

2. REPL 与 Node API:先摸到运行时的形状

不带参数运行 node 命令,会进入一个交互环境,叫 REPL(Read-Eval-Print Loop, 读-求值-打印 循环):敲一行,它立刻执行并把结果打出来。书里让你做的第一件事是输入 console.log("Hello world!"),然后注意它多打了一行 undefined——那是 console.log 的返回值。4

REPL 里输入 global 可以看全局对象。如果你来自浏览器,会有点惊讶:浏览器里 window 上挂着 成百上千个全局 API,Node 的全局对象却稀疏得多。原因是两者提供能力的思路不同: 浏览器把 API 直接铺成全局变量,Node 把 API 收进模块,要用 import 显式取。5

书中用 REPL 环境做了一个命令行计算器小项目,用的是 node:readline/promises 模块—— node: 前缀表示「Node 核心 API」。有两个细节值得记住:其一,import * as readlinePromises from "node:readline/promises" 里的 * 是「把这个模块导出的所有东西装进一个对象」;其二,示例里用了 eval(input) 把输入当代码执行—— 原书当场警告:绝不把 eval 用在不可信的用户输入上,「限制用户只能输入算式」这件事, 本该由正则表达式做(正则留给后续章,MEAP 里没有)。6

顺带一个文件后缀的坑:.mjs 是什么

Node 出生时用 CommonJS 模块标准;后来 ECMAScript 规范自己收了模块(ESM,ECMAScript Modules)。 Node 从 v12.17.0 起两者都支持,但为了向后兼容,默认把 .js 文件当 CommonJS。 .mjs 后缀是显式声明:「这是 ESM」。全书示例都用 ESM,对应地,import 属于 ESM,require 属于 CommonJS——模块标准的深入对比原书安排在后续章,这里只需记住后缀的区分。7

3. npm 与依赖树:三个文件管一棵树

Node 流行起来的另一半功劳是 npm(Node Package Manager):它是包管理器加公共仓库(registry)的组合, 由 Isaac Schlueter 在 Node 发布后不久创建,它让「分享 JavaScript 代码」从手工拷贝文件 变成了一条命令。8

3.1 package.json:只记「你直接要的」

npm init -y 生成一份骨架 package.json。装一个包看看发生什么:9

npm install express@5

三个动作同时发生:

  1. package.json 的 dependencies 里多一行 "express": "^5.1.0"——只记你直接要的;
  2. 磁盘上长出 node_modules/,里面不只有 express,还有 express 的依赖、依赖的依赖…… npm 递归下载整棵依赖树;
  3. 多出 package-lock.json,记录 node_modules 的完整内容。

一张表记住三者的分工:

文件 记什么 谁读它
package.json 你直接声明的依赖 + 版本范围 人读;npm 更新依赖时改
package-lock.json 整棵树的精确版本 npm install 时读,保证重装的树一模一样
node_modules/ 实际的代码文件 运行时的 Node 读

3.2 import 怎么找文件:裸 specifier 与向上查找

回到主线旅程。Node 里的 import 有三种长相:10

import * as fs from "node:fs";        // ① 核心 API(带 node: 前缀)
import { helper } from "./util.mjs";   // ② 相对路径
import express from "express";         // ③ 裸 specifier(bare specifier):只写了包名

第三种是魔法所在。「express」不是路径,Node 的找法(书里叫模块解析算法)是: 从当前文件所在目录开始,逐级向上找 node_modules 目录,看里面有没有叫这个名字的包; 找到就停。11

找到包目录后,哪个文件算入口?按顺序看三样东西:12

  1. 包自己的 package.json 里有没有 exports 字段——有就按它的映射来(最现代、最灵活);
  2. import 里有没有带文件路径(如 "express/lib/view.js"),有就直接用;
  3. 都没有,用 package.json 的 main 字段。

3.3 一棵真实的树:嵌套与提升

依赖树最麻烦的问题:同一个包的不同版本要在同一个项目里共存。 书里构造的情形:你的项目依赖 dep1 和 dep2,dep1 依赖 lodash@3,dep2 依赖 lodash@4。13

解法写在目录结构里——每个包都从离自己最近的 node_modules 找依赖:

node_modules/
  dep1/
    node_modules/
      lodash/          # 这是 lodash@3
  dep2/
    node_modules/
      lodash/          # 这是 lodash@4

dep1 里的 import "lodash" 解析到嵌套的那一份(@3),dep2 解析到它的那一份(@4),互不干扰。 注意一个推论:你自己的代码 import "lodash" 会失败——顶层没有 lodash,向上查找直到根也没有。 lodash 是 dep1 的「私货」,不对外。14

npm 会在版本不冲突时把间接依赖提升(hoisting)到顶层,减少重复下载:

node_modules/
  dep1/
  dep2/
  lodash/              # 两家用同一版本时,一份拷贝服务两家

这套「就近查找 + 能提就提」的算法,是 node_modules 目录经常又大又深的原因, 也是「删了 node_modules 重装」成为行业仪式的根因。15

两个相关的实用补充(原书边栏):想在本地让别的包可用(比如正在开发自己的库), npm link <目录> 建符号链接;一个仓库里多个互相关联的包,用 package.json 的 workspaces 字段声明,npm install 时自动互相链接。符号链接(symbolic link)是文件系统里的快捷方式: 指向另一个位置的引用,改这边等于改那边。16

4. 版本号与 lockfile:协作的基石

4.1 ^ 符号:semver 与「caret 范围」

package.json 里 "express": "^5.1.0" 的 ^ 是什么意思?要懂它,先懂 semver(semantic versioning, 语义化版本):版本号三段 主版本.次版本.修订号(如 5.1.0),约定是——不兼容的变更必须升主版本。 次版本加功能、修订号修 bug,都保持兼容。17

在这个约定下,^5.1.0 读作:「5.1.0 及以上、但 6.0.0 以下的任何版本」—— 即「同主版本内,越新越好」。^ 叫 caret range,是 npm 装包时的默认写入形式。 配套的命令要分清:18

  • npm update --save <包>:升到 package.json 范围内最新;
  • npm install <包>@latest:无视范围,装最新版(并改写 package.json)。

一个行为差异坑:install 默认保存修改,update 默认不保存——所以原书建议 update 时带 --save, 留下一条可读版本变更记录。

4.2 lockfile:把「差不多」变成「分毫不差」

范围意味着灵活:今天装到 5.1.1,明天装到 5.2.0,「应该」都兼容——应该。 lockfile(package-lock.json)做的事是把整棵树的每个精确版本钉死; 队友 clone 下来 npm install,按 lockfile 复刻出和你一模一样的树。19

行业里有一条已成共识的规范:lockfile 提交进版本控制,node_modules 不进。 冲突了?npm 内置了lockfile 的合并冲突解决:遇到冲突跑一遍 npm install 即可。20

一个诚实的分歧(原书边栏照实收录):作为「给别人用的库」要不要 lockfile,社区分两派。 Sindre Sorhus 的观点:库用 lockfile,你的本地依赖树就和「把库当依赖用的用户」的树不一致了, 测出来的问题未必是用户的问题。这条分歧没有标准答案,知道有这回事即可。21

5. 改别人的包:fork 与打补丁

开源的好处:依赖的源码你都能改。两条路:22

Fork(分叉):把包的源码仓库整个复制一份,改完把 package.json 里的版本字符串换成 你的 git 仓库地址("express": "git+ssh://git@github.com:你/express#5.1.0",井号后是 git 标签)。 npm install 会下载你的仓库、跑它的构建脚本、把产物放进 node_modules。 适合「你要长期养一个自己的分支」。

patch-package(打补丁):只想临时修个 bug 等官方合并,或者要改的是一个间接依赖, fork 太重。流程:直接改 node_modules 里的源码 → npx patch-package <包名> → 生成一个 .patch 文件,提交进版本控制 → package.json 里加一行 "postinstall": "patch-package"——postinstall 是生命周期脚本,在每次 npm install 装完包后自动执行, 补丁于是每次都被重新打上。23

为什么不能直接改 node_modules 就走:它不进入版本控制,队友拿不到; npm 任何时候重写 node_modules,你的修改就被冲掉。补丁文件解决的就是「改动持久化」。24

顺带区分一组易混概念:--save-dev(第 2 章用过)把包记进 devDependencies 而非 dependencies—— 前者是「开发时要用」(构建工具、测试框架),后者是「上线也要用」(运行时依赖)。 装包时它的传递依赖照常安装,但它的「开发依赖」不会装。25

6. 把 Node 当脚本语言:scripting 的三种姿势

JavaScript 开发者的一个常见发现:有了 Node 之后,「写个脚本」不再需要切换成 Python 或 shell。 原书给了三种由轻到重的姿势。

6.1 package.json 的 scripts 字段

{ "scripts": { "hello": "echo 'Hello, scripts!'", "lint": "eslint ." } }

npm run hello 执行。四个小知识一次带走:26

  • npm run hello -- "附加参数":双横线后的内容追加给脚本本身(不加会被 npm 当成自己的参数);
  • 四个名字有简写:test、start、restart、stop 可以省掉 run 直接 npm test;
  • pre<名> / post<名> 自动成对执行:定义了 prebuild、build、postbuild, npm run build 会按序跑三个;
  • npm 会把项目所有依赖装的可执行文件放进 scripts 的 PATH——所以 "lint": "eslint ." 不用写路径。

6.2 可执行脚本:shebang 与 zx

想让一个 .mjs 文件本身可执行(像普通命令一样 ./hello.mjs),第一行写:

#!/usr/bin/env node
console.log("Hello, execution!");

#! 组合俗称 shebang,作用是告诉操作系统「这个文件交给 node 执行」; 再 chmod +x hello.mjs 赋予可执行权限,就能直接跑了。27

写系统脚本(child_process 那套 API 啰嗦),社区有封装——Google 的 zx 提供一个 $ 函数, await $`ls` 即执行 shell 命令并拿到输出。28 如果目标环境装不了依赖(比如 CI 防火墙挡了 registry),用 esbuild 把脚本和依赖打成单个文件带走。

6.3 npx:不装也能跑,以及 bin 字段

包不只能导出代码,还能导出可执行文件。包作者在 package.json 里声明 bin 字段:

{ "bin": { "my-command": "bin/my-command.js" } }

npm 安装这个包时,会造出一个叫 my-command 的可执行命令,链接到包里的那个脚本。 ESLint、tsc 都是这么变成命令的。29

npx 是这个机制的延伸:本地装了就直接跑本地的;本地没有,就去 registry 找同名包, 临时下载、跑完即弃——不必为一个只偶尔用一次的工具污染你的 node_modules 和 lockfile。 npx tsc --init(第 2 章用过)就是这个能力。30

7. 作者的判断与证据

机制性内容(可亲手验证):

  • 非阻塞 IO 的对比代码、REPL 行为、模块解析的目录结构,都是跑一遍即可复现的;
  • 「import "lodash" 在项目顶层会解析失败」是嵌套结构下的直接推论,书中用 assert 形式给出了演示。31

作者立场(经验判断):

  • 「不要阻塞线程」被定为 JavaScript 性能的第一戒律——真实,但代价(长计算怎么办)要等后续章的 workers 才补;
  • 附录里对 pnpm 的推荐(大项目、更谨慎的安全策略)是作者偏好,非行业标准答案;
  • 「npm install 经常慢到分钟级」是作者体验陈述,无数开发者共鸣但无硬数据。

8. 边界与局限

  • 模块标准一笔带过。 ESM vs CommonJS 的完整对比、双包陷阱,原书安排在后续章; 本章只够让你看懂 .mjs 和 node: 前缀。
  • npm 的竞争者们只在附录露面(pnpm、Yarn、JSR),正文一律 npm——选型视野见第 13 章。
  • 供应链安全点到为止。 第 1 章提过攻击面,本章有生命周期脚本(postinstall)这个具体载体, 系统防御(锁版本、审计、allowlist)不在 MEAP 范围内。
  • eval 计算器是演示,不是范本——原书自己标了「Never use eval with untrusted user input」。

9. 可带走的(合上这一章,你应该有这九条)

  1. Node 的性能主张是「不等待」:I/O 交给操作系统并登记回调,单线程永不空转; 「不要阻塞线程」是这门语言的第一戒律。
  2. REPL 是即敲即得的探索环境;Node 的 API 收在模块里,不像浏览器铺满全局。
  3. .mjs = ESM 显式声明;import 属 ESM,require 属 CommonJS。
  4. 三件套分工:package.json 记直接依赖和版本范围,lockfile 钉死全树精确版本,node_modules 是实际代码。
  5. 裸 specifier 的解析 = 从当前目录向上逐级找 node_modules;就近原则让同包多版本共存, 也解释了为什么别人的私货你 import 不到。
  6. ^5.1.0 = 同主版本内取新(semver 约定:不兼容变更才升主版本); install 会存盘,update 要手动 --save。
  7. lockfile 进版本控制,node_modules 不进;lockfile 冲突跑 npm install 自动解。
  8. 改依赖:fork 养长期分支, patch-package 打临时补丁(配 postinstall 自动重打); 直接改 node_modules 等于没改。
  9. bin 字段把包变成命令,npx 让命令免安装直接跑;scripts 字段 + pre/post 生命周期是项目的自动化入口。

11. 深读一:非阻塞的完整机制,从「等待」两个字拆开

第 1 节的对比代码值得再往下钻一层:阻塞和非阻塞的差别,精确地说在 「等的时候,谁在等、谁有空」。

  阻塞式 readFile:
    JS 线程 ──发起读──→ 操作系统
       │                  │ 读磁盘(慢,毫秒级)
       │◄──────结果──────┘
       │  ← 这一整段,JS 线程闲着,后面排队的请求干瞪眼
       ▼
    res.send(html)

  非阻塞 fs.readFile + 回调:
    JS 线程 ──发起读(带回调)──→ 操作系统
       │                            │ 读磁盘(慢)
       │   立刻返回,去接下一个请求 ◄┘
       │ ... 处理别的请求 ...
       │◄──────── 操作系统:读好了,回调给你 ────────
       ▼
    回调执行:res.send(html)

两个关键事实(书里明写):

  • 等待的主体换了:阻塞时是整个进程在等;非阻塞时是操作系统在等,JavaScript 线程早就走了。CPU 空转的时间被「处理别的请求」填满了——这就是 Node 用一条线程 扛高并发的全部秘密;
  • 回调会被排到最后:书里有个 NOTE 容易被忽略——JavaScript 单线程意味着 回调「直到所有 JavaScript 代码都执行完才被处理」。 回调不是插入,是排队;这预告了第 4 章的事件队列和第 8 章的微任务纪律。8

11.1 Apache 的线程方案,代价在哪

书里对老方案的评价是「threads add overhead of their own」。展开一句: 每个线程要占自己的栈内存(通常是兆字节级),线程多了,操作系统在线程间切换的 成本也跟着涨——Apache 的「一请求一线程」在几千并发时就开始吃力。 Node 的赌注是:I/O 等待是服务器的时间黑洞,不让线程陪等,就不必养那么多线程。 这个赌注成立的前提是回调不阻塞线程——所以「Don't block the thread」是信条而不是口号。9

12. 深读二:REPL 里能摸到什么,计算器逐行走一遍

12.1 global 清单的读法

第 2 节看了 REPL 的 global。那份清单(书里原样贴了)值得按类读一遍:

  • 定时器三件套:setTimeout/setInterval/setImmediate(以及对应的 clear);
  • 微任务与克隆:queueMicrotask、structuredClone;
  • 浏览器也有的 Web API 遗产:fetch、atob/btoa、crypto、performance。

和浏览器对照:浏览器把几百个 API 直接挂在 window 上(因为当年没有模块), Node 选择「一切皆模块」(node: 前缀显式导入)——所以 Node 的 global 稀疏是 设计选择的结果,不是能力少。写跨端代码时,见到裸全局名先想「这是哪一端给的」。10

12.2 计算器程序,逐行

书里的 Listing 3.1 是 Node API 的第一次实战,逐行走:11

import * as readlinePromises from "node:readline/promises";  // ① 整模块导入,装进一个对象
import { stdin, stdout } from "node:process";                 // ② 具名导入:取两个导出

const rl = readlinePromises.createInterface({                // ③ 造一个「逐行读输入」的界面
  input: stdin,                                              //    从哪读
  output: stdout,                                            //    往哪写
});

while (true) {                                               // ④ 无限循环:反复问
  const input = await rl.question('Enter expression or ".exit":\n> ');  // ⑤ 问一行,等回答
  if (input === ".exit") {                                   // ⑥ 约定退出暗号
    rl.close();
    break;
  } else {
    console.log(eval(input));                                // ⑦ 把输入当 JS 执行(危险!)
  }
}

行号的教学点:①② 是两种 import 形态(星号 vs 具名),第 3 章模块解析的两种雇主; ③ readline 是「读一行」的专用 API,promises 版配合 ⑤ 的 await(第 8 章的语法); ⑦ 的 eval 是书里最重的安全警告——「Never use eval with untrusted user input」, 这个玩具离「用户在你的机器上跑任意代码」只隔一个 eval。12

13. 深读三:node_modules 解析的决策树,与「同包多版本」全案

13.1 裸 specifier 的完整决策树

  import "express"     ← 裸 specifier
      │
      ├─ 当前文件目录 ./node_modules/express 存在? ──是──► 用它
      ├─ 上级 ../node_modules/express 存在?       ──是──► 用它
      ├─ 再上上级 ……一路到文件系统根
      └─ 全都找不到 ──► 解析失败,报错

找到包目录后,入口的三级规则(书里是列表,画成树更直观):

  package.json 里有 exports 字段? ──是──► 按 exports 的映射解析(最现代、最灵活)
      │否
      import 里带文件路径? ──是──► 包目录 + 该路径
      │否
      └──► package.json 的 main 字段;再没有则默认 index.js

exports 字段值得一句注解:它是「按子路径精确开门」的机制——包作者可以声明 「import 'pkg/a' 给 a.js,import 'pkg/b' 给 b.js,其余路径一律拒绝」, 比 main 一把梭安全得多(细节在后续章)。13

13.2 lodash 双版本全案,带目录演化

第 3 节的 lodash 例子,把目录随安装过程演化出来:

  装完 dep1(dep1 要 lodash@3)和 dep2(dep2 要 lodash@4)之初:

  node_modules/
    dep1/
      node_modules/
        lodash/          ← lodash@3(dep1 的私货)
    dep2/
      node_modules/
        lodash/          ← lodash@4(dep2 的私货)
    lodash/              ← 不存在!你的代码 import "lodash" 会失败

如果 dep1 和 dep2 都要 lodash@4,npm 会把唯一一份提到顶层:

  node_modules/
    dep1/                ← 内部 import "lodash" 向上找到顶层的 @4
    dep2/                ← 同上
    lodash/              ← lodash@4,两家共享

「就近优先、能提就提」八个字,解释了你在 node_modules 里看到的一切形状: 深嵌套(版本冲突时)、扁平(版本一致时)、以及「明明装了却 import 不到」 (它是别人的私货,在人家的 node_modules 里)。14

14. 深读四:脚本系统的完整工具箱

14.1 scripts 字段的特殊规则,四条一起背

书里的四条规则,合用一张表:15

规则 例子 说明
简写四君子 npm test = npm run test test/start/restart/stop 省 run
pre/post 配对 prebuild → build → postbuild npm run build 三连跑
生命周期钩子 preinstall / postinstall npm 命令前后自动执行
prepare 被别的包安装时自动跑 常见于「装完编译二进制」

PATH 那条(书里单独强调):scripts 里跑 eslint . 不用写路径, 因为 npm 把项目依赖装的可执行文件全放进了脚本环境的 PATH—— 这正是第 2 章 npx tsc 与 npm run 殊途同归的底层原因。16

14.2 可执行脚本的生态位:shebang、chmod、zx、esbuild

四件套各管一段(书里的顺序):shebang 行(#!/usr/bin/env node) 告诉操作系统「这文件交给 node」;chmod +x 给文件加上可执行权限, 于是 ./hello.mjs 直接跑;zx 的 $`ls` 让「调 shell 命令并拿输出」 从 child_process 的多行样板缩成一行模板串;esbuild 的单文件打包解决 「目标环境装不了依赖」——把脚本和它的依赖打成一个 .js 文件带走, 防火墙挡 registry 的 CI 环境也能跑。17

14.3 npx 的两级查找,与 bin 字段的配合

npx 的行为分两级(书里写得精确):18

  npx eslint .
      │
      ├─ 本地 node_modules/.bin/ 里有 eslint? ──有──► 直接跑本地的
      └─ 没有 ──► 去 registry 找叫 eslint 的包
                    ├─ 包里有与包同名的可执行文件(或只有一个可执行文件)?
                    └─ 临时下载、跑完即弃

bin 字段是包的「可执行文件声明表」:"bin": { "my-command": "bin/my-command.js" } ——安装时 npm 照表造出 my-command 命令(本质是个指向包内脚本的链接)。 npx 让「没装过也能跑一次」成立:一次性工具(create-* 脚手架、一次性转换器) 不必污染项目的依赖树和 lockfile——这是第 3 章依赖卫生的一部分。19

15. 原文地图

主题 原书章 原文位置
Ryan Dahl 与非阻塞 IO 动机 Chapter 3: Working with Node.js and npm text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:10(搜「non-blocking IO」)
阻塞 vs 回调代码对比 同上 :39(搜「blocking IO receives a request」)
不要阻塞线程 同上 :65(搜「Don't block the thread」)
REPL 与全局对象 同上 :82(搜「Read-Eval-Print Loop」) · :97(搜「sparse selection」)
计算器与 eval 警告 同上 :131(搜「Listing 3.1」) · :156(搜「untrusted user input」)
.mjs 与 ESM 边栏 同上 :164(搜「WHAT IS THE .MJS EXTENSION」)
npm 历史与 registry 同上 :190(搜「Isaac Schlueter」)
npm install 三动作 同上 :202(搜「npm init -y」) · :235(搜「recursively downloads」)
三种 import 与裸 specifier 同上 :280(搜「bare specifier」)
入口点规则(exports/main) 同上 :298(搜「entry point」)
嵌套与提升的目录结构 同上 :316(搜「multiple versions of the same package」) · :331(搜「hoisting」)
npm link 与 workspaces 同上 :339(搜「IMPORTING CODE FROM LOCAL PACKAGES」)
lockfile 的由来与规范 同上 :357(搜「lockfile」) · :365(搜「commit package-lock.json」)
semver 与 caret 范围 同上 :389(搜「semantic versioning」) · :401(搜「caret range」)
update 与 install@latest 同上 :407(搜「npm update --save」)
fork 与 git 依赖 同上 :417(搜「Forking and patching」) · :428(搜「git+ssh」)
patch-package 流程 同上 :444(搜「patch-package」) · :461(搜「postinstall」)
devDependencies 区别 同上 :449(搜「save-dev flag」)
scripts 字段四规则 同上 :480(搜「Defining scripts」)
shebang 与 zx 同上 :527(搜「shebang syntax」) · :543(搜「zx」)
bin 字段与 npx 同上 :563(搜「bin entry」) · :576(搜「temporarily install」)
全章总结 同上 :591(搜「Summary」)

  1. 出处:「Chapter 3: Working with Node.js and npm」第 9–16 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:9,搜「In 2009, Ryan Dahl announced Node.js. His goal」)。Dahl 对标 Apache、以非阻塞 IO 为目标的原始动机。 ↩

  2. 出处:「Chapter 3: Working with Node.js and npm」第 39–63 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:51,搜「By contrast, under Node.js, the request handler might」)。两段服务器代码对比;回调定义见第 60 段(搜「a callback」)。 ↩

  3. 出处:「Chapter 3: Working with Node.js and npm」第 65–80 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:71,搜「async/await syntax」)。promise/async 写法与「Don't block the thread」的第一戒律(第 65–68 段边栏)。 ↩

  4. 出处:「Chapter 3: Working with Node.js and npm」第 82–94 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:88,搜「Hello world」)。REPL 行为:执行后显示返回值,console.log 恒返回 undefined。 ↩

  5. 出处:「Chapter 3: Working with Node.js and npm」第 95–123 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:120,搜「sparse selection」)。浏览器全局密集 vs Node 模块化的对比原文。 ↩

  6. 出处:「Chapter 3: Working with Node.js and npm」第 125–186 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:156,搜「untrusted user input」)。eval 警告是原书加粗的安全红线。 ↩

  7. 出处:「Chapter 3: Working with Node.js and npm」第 164–177 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:166,搜「CommonJS」)。CommonJS/ESM 两标准与 .mjs 后缀的由来。 ↩

  8. 出处:「Chapter 3: Working with Node.js and npm」第 59–69 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:65,搜「blocked」)。回调排队的 NOTE 与「Don't block the thread」第一戒律。 ↩↩

  9. 出处:「Chapter 3: Working with Node.js and npm」第 39–50 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:50,搜「threads add overhead of their own」)。Apache 线程方案的代价一句带过,Node 的赌注由此反衬。 ↩↩

  10. 出处:「Chapter 3: Working with Node.js and npm」第 95–123 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:120,搜「exposed as globals」)。Node「API 收进模块」对浏览器「铺满全局」的设计对照。 ↩↩

  11. 出处:「Chapter 3: Working with Node.js and npm」第 125–162 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:131,搜「Listing 3.1」)。计算器程序完整代码与逐行注释(#A–#D)。 ↩↩

  12. 出处:「Chapter 3: Working with Node.js and npm」第 156–162 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:156,搜「eval function」)。eval 的安全警告原文;「限制输入字符」的正则思路见第 184–186 段。 ↩↩

  13. 出处:「Chapter 3: Working with Node.js and npm」第 279–309 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:300,搜「exports field」)。入口三级规则;exports 的灵活性一句见第 301–302 段。 ↩↩

  14. 出处:「Chapter 3: Working with Node.js and npm」第 310–337 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:310,搜「multiple versions of the same package」)。双版本共存的目录结构与提升行为。 ↩↩

  15. 出处:「Chapter 3: Working with Node.js and npm」第 480–526 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:498,搜「special rules」)。scripts 四条特殊规则原文清单。 ↩↩

  16. 出处:「Chapter 3: Working with Node.js and npm」第 510–516 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:511,搜「available to scripts on the PATH」)。依赖可执行文件进 PATH 的机制。 ↩↩

  17. 出处:「Chapter 3: Working with Node.js and npm」第 527–561 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:528,搜「shebang syntax」);与第 543–559 段(搜「esbuild」)。四件套各自的段落位置。 ↩↩

  18. 出处:「Chapter 3: Working with Node.js and npm」第 576–589 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:583,搜「check the registry」)。npx 两级查找的机制描述。 ↩↩

  19. 出处:「Chapter 3: Working with Node.js and npm」第 562–575 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:568,搜「bin entry」)。bin 字段声明与安装时的链接行为。 ↩↩

  20. 出处:「Chapter 3: Working with Node.js and npm」第 365–368 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:365,搜「commit package-lock.json」)。「commit lockfile、不 commit node_modules」的行业规范与冲突自愈。 ↩

  21. 出处:「Chapter 3: Working with Node.js and npm」第 369–375 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:369,搜「divided on the subject」)。库是否该用 lockfile 的社区分歧与 Sorhus 的引文。 ↩

  22. 出处:「Chapter 3: Working with Node.js and npm」第 417–436 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:422,搜「full copy of the repository」)。fork 的定义与 git 依赖写法。 ↩

  23. 出处:「Chapter 3: Working with Node.js and npm」第 437–469 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:444,搜「patch-package」)。打补丁四步与 postinstall 自动重打。 ↩

  24. 出处:「Chapter 3: Working with Node.js and npm」第 441–443 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:441,搜「wouldn't be a stable solution」)。直接改 node_modules 不被版本控制、随时被冲掉的原因。 ↩

  25. 出处:「Chapter 3: Working with Node.js and npm」第 449–452 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:449,搜「devDependencies section」)。devDependencies 与传递依赖规则的区分。 ↩

  26. 出处:「Chapter 3: Working with Node.js and npm」第 480–525 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:480,搜「Defining scripts in package.json」)。scripts 字段与四条特殊规则。 ↩

  27. 出处:「Chapter 3: Working with Node.js and npm」第 527–539 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:527,搜「3.3.2 Writing executable scripts with Node.js」)。shebang 名字由来(#! = sharp + bang)与 chmod 步骤。 ↩

  28. 出处:「Chapter 3: Working with Node.js and npm」第 540–560 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:543,搜「zx」)。zx 的 $ 函数与 esbuild 打包单文件的场景。 ↩

  29. 出处:「Chapter 3: Working with Node.js and npm」第 562–575 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:568,搜「bin entry」)。bin 字段声明可执行文件及安装时的链接行为。 ↩

  30. 出处:「Chapter 3: Working with Node.js and npm」第 576–590 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:582,搜「The power of npx goes far beyond that」)。npx 查 registry、临时安装、跑完即弃的机制。 ↩

  31. 出处:「Chapter 3: Working with Node.js and npm」第 325–328 段(text/06-ch03-chapter-3-working-with-node-js-and-npm.txt:327,搜「fail to resolve」)。「私货 import 不到」的原文断言。 ↩