模块系统:代码怎么拆、怎么拼
一个项目不可能全写在一个文件里——那就是个几千行的大泥球,改一行心慌半天。模块系统就是"把代码切成小文件、互相引用"的规则。Node 历史上有两套规则:祖传的 CommonJS(require)和现代的 ES Modules(import)。两套你都得认得,因为老项目全是前者,新项目越来越多用后者。
CommonJS:Node 的祖传写法
这是 Node 最早的模块规则,require 进东西、module.exports 出东西。看代码:
math.js —— 一个"被别人引用"的模块
app.js —— 引用上面那个模块
ES Modules:现代写法,和浏览器、前端统一
Node 14 以后原生支持浏览器那套 import / export。怎么告诉 Node "这个项目用 ESM"?两种办法:① 把文件后缀改成 .mjs;② 在 package.json 里加一行 "type": "module"。
math.mjs —— 用 export 对外暴露
app.mjs —— 用 import 引入
| 对比点 | CommonJS(老) | ES Modules(新) |
|---|---|---|
| 引入 | const x = require('./x') | import x from './x.js' |
| 导出 | module.exports = {...} | export const / export default |
| 文件后缀 | 默认 .js | .mjs,或 package.json 写 "type":"module" |
| 加载时机 | 运行时同步加载,能 require 变量 | 编译时静态分析,更利于打包摇树 |
| 互操作 | 能用 import 引入 CJS 包 | 用 import 引老包时,import 包名 会拿到它的整体 |
三类模块,别搞混
- 内置模块:Node 自带、不用装,比如
fs(文件)、path(路径)、http(网络)。直接require('fs')。 - 第三方模块:npm 上别人写好的包,比如 Express、Prisma。装完直接
require('express'),不用写./。 - 本地模块:你自己项目里的文件,引入时必须以
./或../开头,否则 Node 会以为你在找第三方包。
package.json:一个项目的身份证
每个 Node 项目根目录都有一个 package.json,它记录"这是个什么项目、靠哪些包活着、有哪些命令可以跑"。npm init -y 一键生成一个最简版。
package.json 长这样(关键字段都标了注释)
问package-lock.json 是干嘛的,能删吗
不能删。你 npm install express@^4.21.0 装的其实不是"精确的 4.21.0",而是"4 系列里不低于 21.0 的最新版"。今天装是 4.21.3,下周可能自动变成 4.21.5——这就可能导致你电脑能跑、同事电脑跑不起来的灵异 bug。
package-lock.json 就是把你这次实际装的每一个包、每一个版本、甚至它们的子依赖全部锁死记下来。别人 clone 你的项目跑 npm install,装出来的版本和你一模一样。所以:package.json 进 git,package-lock.json 也必须进 git。
① 老项目认 require/module.exports(CommonJS),新项目用 import/export(ESM),见到别懵。
② 引自己的文件必须带 ./,引第三方包直接写名字。
③ package.json 写项目说明书,package-lock.json 锁版本,两个都要提交到 git。
ESM 里没有 __dirname?import.meta.url 补上
CommonJS 里天然有 __dirname(当前目录)、__filename(当前文件);切到 ESM(package.json 写了 "type": "module")后这俩变量直接消失,用 __dirname 会报错。这是迁移时最常踩的坑,固定三行替换:
ESM 里拿到"当前目录"的标准写法
一句话记忆:CJS 有 __dirname 白送;ESM 要用 fileURLToPath(import.meta.url) 自己造。新项目如果全栈统一 ESM,就把这三行写成个小工具到处复用。
循环依赖:A 引 B、B 又引 A 会怎样
模块 A require('./B'),模块 B require('./A')——绕回来了。CommonJS 的处理是"先给一个没加载完的半成品 exports 出去",于是你拿到的可能是 {} 或 undefined,报 X is not a function,玄学到怀疑人生。
循环依赖的典型翻车现场
怎么避免:① 重构解依赖——把 A、B 共用的底层东西抽到第三个模块 C,让 A、B 都依赖 C,而不是互相依赖;② 把 require 挪到函数内部(用到时才加载),让顶层加载顺序避开环;③ ESM 对循环依赖处理更智能(live binding),但同样推荐从设计上消除环。记住:循环依赖不是报错,而是"拿到 undefined 还能跑",这才是最可怕的。