KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

模块与构建 — keel 龙骨

模块与构建 的参考信息:模块与构建

课程导读 · 每一条结论都要能在自己的 Node 上跑出同一个报错

这门课解决的是同一类问题:代码在本机能跑、装到 CI 里跑不了;本地 tsc 绿的、上线白屏;删了两千行代码,产物一个字节没瘦;明明只 import 了一个函数,样式却没了;instanceof 在地图上飘出 false。它们的共同点是——你写下的那行 import 被交给了一个解析器,而你不清楚这个解析器按什么规则办事。

你现在的起点

如果你符合上面这几条,这门课就是为你写的。前提是你会写 JavaScript 并且跑得起来 Node——这门课不教语法,它教的是"这行字被谁翻译、翻译成了什么"。

一条能走通的学习路径

先定盘(01 一个文件按哪套规范加载)
  → 判定链:扩展名 → 就近 package.json 的 type → 语法嗅探
  → 两条方向相反的报错,一条抛在解析期、一条抛在运行期

再拆语义(02 导出的是"接线"还是"快照")
  → 活绑定、只读视图、import 提升
  → 循环依赖:一边抛 TDZ 错,一边静默 undefined

然后是整门课最实用的一章(03 一行 import 落到磁盘上哪个文件)
  → exports 是包围盒;条件靠键序;子路径模式是字符串替换

交界处(04 两套规范相遇时的形状)
  → default 到底等于什么、named 是猜出来的、require(esm) 的边界

发布形态(05 双格式发布的账单)
  → 单例变两份、instanceof 失效、计数器重数

产出侧(06 打包器能看见什么)
  → 它只做静态分析;摇不动的地方恰恰是有函数调用的那行

收口(07 三套解析器,三个答案)
  → Node / 打包器 / TypeScript 各按各的规则解析同一行 import

你会拿到什么

章节 一句话 关键动作
01 一个文件,两套加载规范 "type" 不是唯一判定者 判定矩阵真跑 + 两个方向的报错原文与抛出阶段
02 导出的是接线还是快照 同一段"改了就生效"的代码,一边生效一边不生效 活绑定三态对照 + 循环依赖双形态(TDZ 错 vs 残缺 exports)
03 一行 import 落到磁盘上哪个文件 exports 是一道包围盒 23 个探针各自的报错码 + 条件选择的键序规则
04 两套规范相遇时的形状 default 不是你以为的那个东西 九种 CJS 导出写法 × 两种导入方式 + require(esm) 边界
05 双格式发布的账单 一份代码会被加载两次 同进程双侧对照:单例 id、instanceof、计数器
06 打包器能看见什么 摇不动的地方是有函数调用的那行 逐例产物字节数 + 残留标记 grep + 摇树粒度表
07 三套解析器,三个答案 编译通过 ≠ 运行时找得到 三档 moduleResolution × 产物能否真跑

每章结构统一:现场 → 形态 → 原因 → 本章脉络 → 生产边界 → 动手:可观察结果 → 故障注入 → 自测题 → 现在能解释什么。

三个必须先纠偏的认知

  1. "type": "module" 不是一个"把文件变成模块"的开关。 它只是判定链上的一环。Node 22 起,一个完全没有 package.json 的目录里,写了 export 的 .js 会被嗅探为 ESM 并正常跑完(实测 exit=0)。反过来,你显式写 "type": "commonjs" 会把嗅探关掉。所以"我的文件是什么模块"这件事,答案取决于一条三段判定链,而不是某一个字段。

  2. import 不是 require 的语法糖。 导入进来的是接线(导出方改了,你这边跟着变),CJS 里写进字面量的是快照(永远不变)。报错时机也不同:import 出现在 CJS 里是解析期的 SyntaxError,require 出现在 ESM 里是运行期的 ReferenceError。循环依赖上更极端:ESM 抛错(fail fast),CJS 静默 undefined(fail silent,只附一条警告)。

  3. 打包器摇不掉一段"看起来没用"的代码,通常不是它笨,而是那行初始化代码里有一次函数调用。 实测:未使用的 const OBJ = {...}、数组、函数、类、纯字面量拼接全部被摇掉;而 const CALL = "x" + "y".repeat(400)、"MARK".toLowerCase()、Math.random() 一个都没摇掉。"明显是纯函数"是你的判断,不是打包器的判断。

学完这门课你能做什么

前置要求

替身边界

实验以 Node 22.22.2(另有 24.9.0 对照)+ esbuild 0.28.2 + TypeScript 7.0.2 为准。这一行比任何框架都动得快——本课程记下的多处行为都是被某一版改过的:"type" 非法值会被静默忽略、.js 的语法嗅探默认开启(且开关形式是 --no-experimental-detect-module)、require(esm) 从"要 flag"变成"默认可用"、moduleResolution: "node10" 在 TS 7 里已被移除。

结论的形状("判定链的顺序"、"绑定是接线还是快照"、"exports 是包围盒"、"包围盒里条件靠键序")跨版本成立;具体字段名、默认值、报错文案请在你自己的版本上复核。

实验全部是单机、小文件、无网络。凡涉及字节数与毫秒数的地方,请只看比值和形状,不要抄绝对值。

进入 keel 阅读