模块化设计最佳实践与按需加载

本节目标

好模块的标准:一件事、明确进出口、低耦合。

// 运行环境:Node.js 16+(.mjs 示意)
// ✅ 单一职责:format.js 只管格式化
export const formatPrice = (n) => `¥${n.toFixed(2)}`
export const formatDate = (ts) => new Date(ts).toISOString().slice(0, 10)

// ⚠️ 桶文件陷阱:index.js 里全量 re-export
// export * from './format.js'
// export * from './validate.js'
// 若业务只用 format,却经 index 全引,Tree Shaking 可能失效(取决于打包器对 * 的处理)

// ✅ 按需加载:业务里点击才加载重型模块
async function openEditor() {
  const { RichEditor } = await import('./rich-editor.mjs')
  new RichEditor()
}

设计原则:模块对外只暴露「必要的」,内部实现藏起来;依赖尽量单向(避免循环);一个文件别又做 UI 又做网络又做存储。

名词解释

单一职责(Single Responsibility):一个模块只「做好一件事」。比如 format.js 只管格式化、api.js 只管请求。好处是改格式化不影响网络、测试独立、复用方便。它是模块设计的第一原则,违反它往往导致「一个文件 2000 行、谁都不敢动」。

桶文件(Barrel):用一个 index.js 把多个子模块「export * / 具名 re-export」聚起来,让调用方 import { a, b } from './utils' 一行搞定。便利但有坑:不当的全量 re-export 可能让 Tree Shaking 失效(打包器不敢删「可能被用到」的导出)。按需场景建议直接引具体文件。

课后练习

练习 1:为什么「一个模块又管 UI 又管请求又管存储」不好?

答案:耦合高——改存储逻辑可能误伤 UI;测试时要同时 mock 网络;复用「只想用它的格式化函数」却被迫带上整坨依赖。单一职责让每个模块「可独立理解、测试、复用」,长期维护成本骤降。

练习 2:桶文件什么情况下会让 Tree Shaking 失效?

答案:当用 export * from './x' 聚合,且打包器无法确定「x 里某个导出是否被用到」时(尤其 x 有副作用),可能保守保留整个 x。若追求极致摇树,调用方应直接 import { fn } from './x.js' 而非经桶文件。现代打包器对具名 re-export 多能摇,但对 export * 较保守。

总结

模块化设计的好坏,决定项目半年后「好不好改」。我的观点:原则是简单的——单一职责 + 明确边界 + 依赖单向,但执行起来要克制「顺手往当前文件塞点逻辑」的冲动。桶文件我建议「适度用」:在库入口聚合方便用户一行引入没问题,但在业务代码里为了「少写几行 import」而全量 re-export,常常赔上 Tree Shaking。按需加载则记住「重且不一定用 → 动态 import」,这是首屏性能的低垂果实。好模块像好收纳:东西各归其位,用时一拿就到。