Node 与浏览器运行时
JavaScript 语言相同,宿主(Host)不同:浏览器给 DOM、网络、渲染;Node 给文件系统、进程、TCP。全栈写错宿主 API、把 window 带进 SSR、或在 Node 里当有 fetch 的旧版本上跑,都属于这一层。事件循环通识见 事件循环与任务队列;模块格式见 模块系统。
解决什么问题
| 痛点 | 分清宿主之后 |
|---|---|
| 「这段 JS 为啥在 Node 挂」 | 用了 document / window / localStorage |
| 前端包在服务端 import 报错 | 代码路径没做环境拆分或 polyfill |
| 内存只升不降 | 闭包、全局缓存、未移除的监听,和宿主对象生命周期绑在一起 |
| 同一份工具函数两边用 | 只依赖 globalThis 与语言特性,副作用放到适配层 |
一句话:语言是 ECMAScript;能做什么由运行时注入的全局对象决定。
全局对象
globalThis.crypto; // 两边都有(足够新的 Node / 浏览器)
| 浏览器 | Node | |
|---|---|---|
| 全局别名 | window ≈ self ≈ globalThis | global ≈ globalThis(没有 window) |
| 入口 | 用户事件、页面脚本 | node file.js / 长期进程 |
| 模块默认 | ESM(type="module")或打包结果 | CJS 或 ESM,见模块篇 |
| 退出 | 关标签;页面走了运行时就没了 | 事件循环空了才退出;服务器通常永不空 |
判断环境(不要用 UA 字符串当逻辑开关):
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
SSR / 同构时,这个判断要配合 打包条件导出(browser field / Vite import.meta.env.SSR),否则服务端仍可能打进 DOM 代码。
能力对照(够用即可)
| 能力 | 浏览器 | Node |
|---|---|---|
| DOM / CSSOM | 有 | 无(除非 JSDOM 等测式替身) |
fetch | 有 | Node 18+ 全局 fetch;更老用 undici / node-fetch |
| 文件 | 无任意路径 FS(有 File API / OPFS) | node:fs |
| 环境变量 | 构建时注入(见 多环境发布) | process.env 运行时可读 |
| 二进制 | ArrayBuffer / Uint8Array / Blob | 另有 Buffer(Uint8Array 子类) |
| 多线程 | Web Worker | worker_threads;另有 cluster 多进程 |
| 定时器 | setTimeout 等 | 同名 + setImmediate / nextTick |
Buffer 与前端 Uint8Array 能互相转,但 JSON / 日志里直接把 Buffer 当对象打,会看到 { type: "Buffer", data: [...] }。边界上显式 Buffer.from / Uint8Array。
内存与泄漏直觉
JS 堆由引擎 GC。你留不住的对象会被收;你还指着的对象就不会。
| 模式 | 典型现场 |
|---|---|
| 闭包抓住大对象 | 回调没卸,一直闭包着整个请求上下文 |
| 全局 Map 当缓存 | 没有上限 / TTL,见手写 LRU |
| 监听未移除 | DOM addEventListener、Node EventEmitter、setInterval |
| 脱离的 DOM | 节点从树上拿掉了,JS 还握着引用 |
浏览器多一个「页面关掉进程就没了」;Node 服务是长进程,泄漏会表现为 RSS 只涨、最终 OOM。排障先抓 heap snapshot,不要先加机器。
// 长进程里:有 on 就要想到 off
const onTick = () => {};
emitter.on("tick", onTick);
// 结束时
emitter.off("tick", onTick);
clearInterval(id);
全栈同构时怎么拆
core/ 纯函数、类型、与宿主无关
browser/ DOM、路由、存储
node/ fs、进程、密钥(密钥绝不进前端产物)
- 密钥、数据库 URL 只放 Node 侧
- 前端需要的配置走「构建注入」或「后端下发」,不要把
.env整文件打进 bundle - 不要在通用包里
import fs再指望 Vite 摇掉——静态分析失败就会把 Node 内建带进浏览器
要点
- 先问这段代码跑在哪个宿主,再写 API
globalThis是唯一可靠的跨环境全局- Node 长进程把「忘记取消的订阅」放大成 OOM
- 同构包按 core / browser / node 切开,比满地
if (window)稳 Buffer、process、window都是宿主礼物,不是语言关键字
面试速记
- JS 和运行时什么关系? ECMAScript 定义语言;DOM /
fs/process由宿主提供。 - 为什么 SSR 会报
window is not defined? 首屏在 Node 里执行,没有浏览器全局。 - Node 为啥比页面更容易内存泄漏? 进程不退出,全局缓存和监听会一直活。
- Worker 和
setTimeout差在哪? Worker 是真的另一个线程/消息边界;setTimeout仍在同一 JS 线程排队。 - 前端能读
process.env.SECRET吗? 若打包器内联了就能进产物——等于泄露。密钥只留服务端。