模块化设计最佳实践与按需加载
本节目标
- 掌握「单一职责」「明确边界」的模块设计原则
- 会用「桶文件(barrel)」聚合导出但注意陷阱
- 理解「按需加载」在业务里的落地
好模块的标准:一件事、明确进出口、低耦合。
// 运行环境: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」,这是首屏性能的低垂果实。好模块像好收纳:东西各归其位,用时一拿就到。