跳到主要内容

Node 与最终项目 — JS 走出浏览器

这一章讲三件事: Node 是什么、它为什么挑中 JavaScript; 服务端三大件(文件系统、HTTP 服务器、流)怎么用; 以及全书最后一个项目——一个「别人一发言,所有打开页面的人立刻看见」的直播小站。 读完你能读懂一个 Node 服务器的骨架,并解释「长轮询」三个字背后的全部机关。

1. Node 是什么:一次刻意的错配

Node 让你在浏览器之外跑 JS:小到命令行工具,大到驱动动态网站的 HTTP 服务器1

它为什么挑中 JS?书里的答案是个漂亮的洞见:Node 为「让异步编程容易」而生,而 JavaScript 恰好是极少数没有内建输入输出手段的语言——于是可以把它整体套上 Node 那套(相当)古怪的网络与文件系统做法,而不产生两套互相打架的接口。再加一条时代背景:2009 年设计 Node 时,浏览器社区早已习惯了回调风格的异步2

上手三分钟:命令行 node hello.js 跑文件;不带参数进交互提示符。console.log 在这里打到标准输出(终端)。全局多了个 process:process.exit(0) 的 0 表示成功、其他表示出错;process.argv 是命令行参数数组——注意前两项是 node 本身和脚本名,真正的参数从下标 2 开始。浏览器的那套(document、prompt)在这里不存在3

模块解析的规则(第 10 章的落地版):.js 默认按 CommonJS;.mjs 才是 ES 模块(用 import/export,不能用 require)。./..// 开头按路径找文件;node:fs 这类带前缀的是内建模块;裸名字去 node_modules 里找4

2. 三大件:fs、http、流

node:fs 的三种口味,同一个函数都有5:

import {readFile} from "node:fs"; // 回调风格(错误在第一参)
readFile("file.txt", "utf8", (error, text) => {});

import {readFile} from "node:fs/promises"; // promise 版
readFile("file.txt", "utf8").then();

import {readFileSync} from "node:fs"; // 同步版:运行时整个程序停住

不传编码就给你 Buffer——把文件当一串字节(每块 8 位)看的对象——而不是字符串;写字符串默认按 UTF-8。readdir 列目录、stat 查文件信息、rename 改名、unlink 删除。同步版写起来顺手,但若程序正该响应别人,卡住就是事故6

node:http 一个服务器只要五行:

import {createServer} from "node:http";
createServer((request, response) => {
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<h1>Hello!</h1>");
response.end();
}).listen(8000);

每来一个客户端连接,回调跑一次;listen(8000) 之后进程不会跑到脚本末尾就退出——它在监听,得 ctrl-C 才走7。另外 Node 里也有全局的 fetch,书里明说:node:http 自带的 request 函数笨重,推荐直接用 fetch8

流(stream) 解决「数据一块块来」的问题:可写流有 write/end;可读流用事件——on("data", 块 => …) 来一块处理一块,on("end", …) 收尾。书里的回声服务器把请求体转大写流回去;它正是「数据到齐前就能开工」这一模型的示范9

3. 主走查(一):文件服务器的一个回合

项目:把 HTTP 方法映射到文件系统——GET 读、PUT 写、DELETE 删,URL 路径就是文件路径(相对服务器启动目录)。骨架是一个 methods 对象:每个 handler 是 async 函数,返回 {body, status = 200, type = "text/plain"};handler 抛的异常被统一接住,转成 500 响应;body 若是流,直接 pipe 给响应10

安全先行。路径是字符串,/../secret_file 这种写法会爬出工作目录、把整个文件系统暴露给网络。防御两步11:

urlPath(url):
① 用 URL 类解析,decodeURIComponent 解码路径
② resolve() 把它折算成绝对路径
③ 校验:结果必须等于工作目录,或以「工作目录+路径分隔符」开头
不满足 → 抛 {status: 403, body: "Forbidden"}

然后带着 curl 走一个完整回合12:

$ curl localhost:8000/file.txt
File not found ← GET:stat 查不到,错误码 ENOENT → 404
$ curl -X PUT -d CONTENT localhost:8000/file.txt
← PUT:把请求体 pipe 进 createWriteStream
$ curl localhost:8000/file.txt
CONTENT ← 再读,有了(Content-Type 由 mime-types 包按扩展名猜)
$ curl -X DELETE localhost:8000/file.txt
← DELETE:返回 204 no content
$ curl localhost:8000/file.txt
File not found

三个值得背的细节:ENOENT 这类 Unix 风错误码是 Node 里辨认错误类型的方式(比读错误文案可靠);目录用 GET 返回文件清单(readdir 拼行);删除一个不存在的文件也返回成功——HTTP 标准鼓励幂等(idempotent):同一请求发多次与发一次效果相同;「想让某物消失」这个目标,在它已不在时也算达成13。PUT 那边的收尾用 pipeStream 把流包装成 promise:两端任何 error 都 reject,输出流的 finish 才 resolve——错误事件驱动 + promise 的合体样板14

4. 主走查(二):长轮询 —— 「扣着不答」的艺术

最终项目:演讲分享站。需求一句话:任何人在任何窗口发了言或加了评论,所有打开页面的人立刻看见。麻烦在于——服务器没法主动连客户端(浏览器传统上不接受连接,客户端还常躲在路由器后面),而 HTTP 的形状是「一问一答就散」15

长轮询(long polling) 的答案:客户端让一个请求一直挂着,服务器没新消息就扣着不答;有消息了,用这个挂着的请求把数据送出去16。走查全环:

① Fatma 打开页面 → 浏览器发出 GET /talks,
带头 If-None-Match: 4(我手里的版本号)与 Prefer: wait=90(我愿意等 90 秒)
② 服务器核对版本号:当前也是 4 → 没新东西
把这个请求的 resolve 函数存进 waiting 数组,然后……什么都不发
③ Iman 提交了演讲 → 服务器更新数据,调用 updated():
版本号 4 → 5;遍历 waiting,逐个 resolve(带新数据、新 ETag: 5);清空 waiting
④ Fatma 的挂起请求瞬间收到响应 → 页面更新
⑤ 若 90 秒内什么都没发生:定时器兜底,回一个 304(not modified)
客户端立刻发起下一轮挂起——顺带修好了断线等一切意外

机关全在 HTTP 的老朋友身上:ETag/If-None-Match 本是 HTTP 缓存机制(「资源没变就回 304,用你手里的」),这里被挪用成版本号应答;Prefer: wait=90 本是表达偏好的头,这里被当成长轮询开关17。服务器侧的实现出奇地短:waitForChanges(time) 返回一个 promise——把 resolve 塞进 waiting,再设个定时器到点回 304;updated() 版本号加一、唤醒所有人18

书里特意点破的现实约束:忙碌的服务器可能同时挂着几千个这样的等待请求(即几千条 TCP 连接)——为每条连接开一个线程的模型撑不住,而 Node 的事件循环正为此而生。原文原话:长轮询「恰好是 Node 设计动机之一」19

客户端那一半是第 11 章的全套装备:pollTalks 是个 async 无限循环——发请求(第一轮不带条件头,之后带)、304 就立刻下一轮、失败就等 500 毫秒重试(网络闪断后自动恢复);拿到数据就 dispatch 一个 setTalks action20。界面沿用第 13 章的「状态+action」架构:{talks, user} 是全部状态,handleAction 既改本地状态又对服务器发 PUT/DELETE/POST;fetchOK 包装器把「状态码 ≥400」翻译成 reject(补上 fetch 的那个怪脾气);用户名存 localStorage 以便下次自动填21。书里还顺手承认:复杂 DOM 用裸 elt 拼很乱,业界因此用模板语言或 JSX(非标准方言,长得像 HTML),两者都要预处理工具——本章刻意不用22

5. 作者的判断与证据

  • 「Node 选 JS 因为 JS 没有内建 I/O」:设计史的解释,逻辑自洽(避免两套接口)2;
  • 「同步变体会卡住整个程序」:机制事实,代码可复验;
  • 「删除幂等」:HTTP 标准的立场转述,给了理由(目标已达成)13;
  • 「数据纯在内存,重启即失」:项目明说的局限,补磁盘持久化被留成第一个练习23

判断(我们的,不是书里的): 长轮询在 2026 年更多是教学遗产——生产上早换成了 WebSocket 与各类推送协议。但它是理解所有「实时推送」的最小模型: 客户端挂一个未完成的请求,服务端把「响应」当作推送用。学 WebSocket 前, 先把这一章的 waiting 数组想明白,能省一半的困惑。 如果错,会错在: 若只需要「服务器→客户端」单向且低频的通知(推送类需求),SSE(服务器推送事件) 这类更轻的方案可能比 WebSocket 更贴切——演进方向不是唯一的两端。

6. 边界与局限

  • 全书项目的数据都在内存(练习补持久化);无鉴权——「互联网上流氓很多,这种系统裸奔上线多半没好下场」是书里自己的警告;

  • Node 部分刻意从简:集群(一台机器变一队)、Buffer 细节、npm 脚本都没讲;

  • 环境变量(进程启动时读到的全局配置)也没讲;

  • 长轮询的 90 秒与重试 500 毫秒是拍脑袋的经验值,书里未论证;

  • 前端打包、框架、TS 等真实工程配套全书有意回避。

7. 可带走的

  1. Node 选 JS,因为 JS 没有内建 I/O——一套异步哲学贯彻到底;
  2. process.argv 从下标 2 起;.mjs 才是 ES 模块;裸包名去 node_modules 找;
  3. fs 三口味:回调 / promises / Sync(会卡住整个程序);无编码=Buffer;
  4. 每个连接跑一次回调;listen 之后进程不退出;
  5. 流:可读侧用 on("data") 边到边处理,别攒齐再算;
  6. 路径安全两步:resolve 成绝对路径 + 校验仍在工作目录内,越界 403;
  7. ENOENT 等错误码是辨认错误的方式;删不存在的东西也回成功——幂等;
  8. 长轮询=客户端挂请求+服务器扣响应;ETag 挪用成版本号,304 兜底;
  9. 几千条挂起连接正是 Node 的主场;
  10. fetchOK:把状态码翻译成 reject;失败后等 500ms 重试是韧性的最小实现。

8. 原文地图

主题原书章原文位置
Node 的用途与安装口径Node.jstext/23-fm-node-js.txt:9(搜「outside of the browser」) · :13(搜「version 18」)
选 JS 的理由(无内建 I/O)同上text/23-fm-node-js.txt:21(搜「input and output」)
node 命令与 process.argv同上text/23-fm-node-js.txt:35(搜「standard output」) · :49(搜「argv」)
模块解析与 .mjs同上text/23-fm-node-js.txt:62(搜「.mjs」) · :66(搜「node_modules」)
npm install 与本地目录同上text/23-fm-node-js.txt:104(搜「node_modules」) · :106(搜「current directory」)
package.json 与语义版本同上text/23-fm-node-js.txt:110(搜「package.json」) · :133(搜「semantic versioning」) · :135(搜「caret」)
readFile 三种口味同上text/23-fm-node-js.txt:153(搜「Buffer」) · :176(搜「node:fs/promises」) · :182(搜「readFileSync」) · :188(搜「stopped entirely」)
createServer 与不退出同上text/23-fm-node-js.txt:209(搜「every time a client」) · :219(搜「ctrl-C」)
fetch 也在 Node同上text/23-fm-node-js.txt:223(搜「fetch」)
流的读写同上text/23-fm-node-js.txt:227(搜「writable stream」) · :235(搜「data and end」)
大写回声服务器同上text/23-fm-node-js.txt:243(搜「toUpperCase」)
文件服务器骨架同上text/23-fm-node-js.txt:269(搜「methods」) · :276(搜「500」)
路径安全 403同上text/23-fm-node-js.txt:314(搜「security」) · :318(搜「Forbidden」)
GET/ENOENT/mime同上text/23-fm-node-js.txt:340(搜「ENOENT」) · :353(搜「Unix」)
204 与幂等删除同上text/23-fm-node-js.txt:377(搜「204」) · :379(搜「idempotent」)
pipeStream 包装 promise同上text/23-fm-node-js.txt:385(搜「pipeStream」)
curl 回合同上text/23-fm-node-js.txt:408(搜「curl」)
项目设定与直播难题Project: Skill-Sharing Websitetext/24-fm-project-skill-sharing-website.txt:19(搜「talks」) · :21(搜「live view」)
长轮询是 Node 动机之一同上text/24-fm-project-skill-sharing-website.txt:23(搜「long polling」) · :37(搜「thousands」)
扣着不答与超时兜底同上text/24-fm-project-skill-sharing-website.txt:31(搜「stalls」) · :35(搜「timing out」)
HTTP 接口设计同上text/24-fm-project-skill-sharing-website.txt:43(搜「/talks」) · :56(搜「encodeURIComponent」)
ETag 挪用与 Prefer同上text/24-fm-project-skill-sharing-website.txt:79(搜「ETag」) · :83(搜「wait=90」)
无鉴权的警告同上text/24-fm-project-skill-sharing-website.txt:100(搜「hooligans」)
Router 与静态文件同上text/24-fm-project-skill-sharing-website.txt:110(搜「router」) · :142(搜「serve-static」)
输入校验 400同上text/24-fm-project-skill-sharing-website.txt:226(搜「nonsense」)
waiting 数组与 updated同上text/24-fm-project-skill-sharing-website.txt:307(搜「waitForChanges」) · :320(搜「updated」)
fetchOK 与 action 架构同上text/24-fm-project-skill-sharing-website.txt:358(搜「handleAction」) · :393(搜「fetchOK」)
JSX 与预处理同上text/24-fm-project-skill-sharing-website.txt:458(搜「JSX」)
pollTalks 与重试同上text/24-fm-project-skill-sharing-website.txt:491(搜「pollTalks」) · :513(搜「recover」)
数据在内存的局限同上text/24-fm-project-skill-sharing-website.txt:577(搜「purely in memory」)

Footnotes

  1. 出处:「Node.js」第 9 段(text/23-fm-node-js.txt:9,搜「outside of the browser」)。

  2. 出处:「Node.js」第 21 段(text/23-fm-node-js.txt:21,搜「input and output」)。 2

  3. 出处:「Node.js」第 47 段(text/23-fm-node-js.txt:47,搜「exit」)与第 49 段(text/23-fm-node-js.txt:49,搜「argv」)、第 54 段(text/23-fm-node-js.txt:54,搜「document or prompt」)。

  4. 出处:「Node.js」第 62 段(text/23-fm-node-js.txt:62,搜「.mjs」)与第 66 段(text/23-fm-node-js.txt:66,搜「node:fs」)。

  5. 出处:「Node.js」第 147 段(text/23-fm-node-js.txt:147,搜「readFile」)、第 178 段(text/23-fm-node-js.txt:178,搜「node:fs/promises」)与第 184 段(text/23-fm-node-js.txt:184,搜「readFileSync」)。

  6. 出处:「Node.js」第 153 段(text/23-fm-node-js.txt:153,搜「Buffer」)与第 188 段(text/23-fm-node-js.txt:188,搜「stopped entirely」)、第 172 段(text/23-fm-node-js.txt:172,搜「readdir」)。

  7. 出处:「Node.js」第 209 段(text/23-fm-node-js.txt:209,搜「every time a client」)与第 219 段(text/23-fm-node-js.txt:219,搜「ctrl-C」)。

  8. 出处:「Node.js」第 223 段(text/23-fm-node-js.txt:223,搜「recommend sticking to fetch」)。

  9. 出处:「Node.js」第 227 段(text/23-fm-node-js.txt:227,搜「writable stream」)、第 235 段(text/23-fm-node-js.txt:235,搜「data and end」)与第 243 段(text/23-fm-node-js.txt:243,搜「toUpperCase」)。

  10. 出处:「Node.js」第 265 段(text/23-fm-node-js.txt:265,搜「methods」)与第 292 段(text/23-fm-node-js.txt:292,搜「catch」)、第 296 段(text/23-fm-node-js.txt:296,搜「pipe」)。

  11. 出处:「Node.js」第 314 段(text/23-fm-node-js.txt:314,搜「expose our whole filesystem」)与第 318 段(text/23-fm-node-js.txt:318,搜「Forbidden」)。

  12. 出处:「Node.js」第 408 段(text/23-fm-node-js.txt:408,搜「curl」)与第 417 段(text/23-fm-node-js.txt:417,搜「behold」)。

  13. 出处:「Node.js」第 353 段(text/23-fm-node-js.txt:353,搜「Unix」)与第 379 段(text/23-fm-node-js.txt:379,搜「idempotent」)。 2

  14. 出处:「Node.js」第 400 段(text/23-fm-node-js.txt:400,搜「pipe isn’t written to return a promise」)。

  15. 出处:「Project: Skill-Sharing Website」第 21 段(text/24-fm-project-skill-sharing-website.txt:21,搜「live view」)与第 27 段(text/24-fm-project-skill-sharing-website.txt:27,搜「not practical」)。

  16. 出处:「Project: Skill-Sharing Website」第 31 段(text/24-fm-project-skill-sharing-website.txt:31,搜「stalls」)。

  17. 出处:「Project: Skill-Sharing Website」第 81 段(text/24-fm-project-skill-sharing-website.txt:81,搜「If-None-Match」)与第 83 段(text/24-fm-project-skill-sharing-website.txt:83,搜「wait=90」)。

  18. 出处:「Project: Skill-Sharing Website」第 307 段(text/24-fm-project-skill-sharing-website.txt:307,搜「waitForChanges」)与第 320 段(text/24-fm-project-skill-sharing-website.txt:320,搜「updated」)。

  19. 出处:「Project: Skill-Sharing Website」第 37 段(text/24-fm-project-skill-sharing-website.txt:37,搜「thousands of waiting」)与第 23 段(text/24-fm-project-skill-sharing-website.txt:23,搜「motivations for Node」)。

  20. 出处:「Project: Skill-Sharing Website」第 491 段(text/24-fm-project-skill-sharing-website.txt:491,搜「pollTalks」)与第 513 段(text/24-fm-project-skill-sharing-website.txt:513,搜「recover」)。

  21. 出处:「Project: Skill-Sharing Website」第 358 段(text/24-fm-project-skill-sharing-website.txt:358,搜「handleAction」)与第 393 段(text/24-fm-project-skill-sharing-website.txt:393,搜「fetchOK」)、第 389 段(text/24-fm-project-skill-sharing-website.txt:389,搜「localStorage」)。

  22. 出处:「Project: Skill-Sharing Website」第 458 段(text/24-fm-project-skill-sharing-website.txt:458,搜「JSX」)。

  23. 出处:「Project: Skill-Sharing Website」第 577 段(text/24-fm-project-skill-sharing-website.txt:577,搜「purely in memory」)。