模块化:让依赖关系变得明确
项目变大后,脚本顺序和全局变量会制造隐形依赖。import / export 让每个文件明确说出自己需要和提供什么。
- 看到 import 就能追踪功能从哪里来,看到 export 就知道模块提供什么。
- 全局污染、暗依赖与加载顺序
- 把三个互相依赖的脚本改造成显式模块,并从一个入口调用。
- 完成 3.5「第一次把网页发布到公网」
FOUNDATION
必须理解
项目变大后,脚本顺序和全局变量会制造隐形依赖。import / export 让每个文件明确说出自己需要和提供什么。
模块化让文件明确声明自己提供什么、依赖什么。项目变大后,显式依赖比靠脚本顺序和全局变量更容易理解、测试和让 AI 安全修改。
模块像插座标准:每个设备说明输入和输出,就能组合;如果所有电线都藏在墙里互相缠绕,换一个灯泡都可能影响整栋楼。
任务应用把任务数据操作放在 task-store.ts,把任务列表显示放在 task-list.ts,入口 main.ts 负责把两者连接。
全局污染、暗依赖与加载顺序
全局变量像放在公共桌上的东西,谁都能拿、谁都能改,时间一长就不知道是谁动过。模块会把内部细节收好,只把真正需要的能力 export 出去。
基础概念
全局污染是不同代码把名称和数据放进共同作用域,任何地方都可能读取或修改;暗依赖则是代码依赖某个加载顺序或外部变量,却没有明确写出来。
进一步理解
项目小时,全局变量看起来方便;文件变多后,同名覆盖、初始化顺序和意外修改会让行为取决于“谁先运行”。这种关系无法从单个文件的开头看见。
模块拥有自己的作用域,并用 import/export 声明边界。工具因此能分析依赖、发现缺失并只打包真正使用的代码。
任务列表不再偷偷读取 window.tasks,而是明确导入 task-store 提供的读取函数;测试时也能替换数据实现。
把变量改成不导出只能减少全局暴露,若模块仍直接依赖浏览器中某个神秘对象,暗依赖依然存在。
这一小节记住:让每份代码明确说出需要什么、提供什么,不再依赖碰巧的加载顺序。
ES Modules 的 import、export 和入口模块
export 是“我能提供这些”,import 是“我需要使用这些”。入口文件负责把模块接起来并启动应用,不应该顺便装下全部业务代码。
基础概念
ES Modules 是 JavaScript 的标准模块系统。export 声明模块公开的值,import 按路径取得这些值,入口模块负责连接依赖并启动应用。
进一步理解
具名导出适合一个模块提供多个清晰能力,默认导出适合表示该文件唯一主要值。导入路径必须指向正确文件,浏览器直接运行时还需使用 module 脚本。
模块依赖形成有方向的图。入口位于图的起点,但业务逻辑应留在各自模块;入口只组合配置、数据和界面。循环依赖会让初始化顺序变得难懂。
main.ts 导入 `addTask` 和 `renderTaskList`,绑定表单事件;数据模块不知道页面细节,列表模块也不负责生成 id。
import 不是复制代码,而是建立对模块导出值的依赖;模块文件也不等于组件,组件只是模块可能导出的一类值。
这一小节记住:入口负责组装,模块通过公开接口合作。
第三方库与本地模块的边界
边界最好跟着职责走。界面只关心怎么显示任务,数据模块只关心任务怎么保存;以后替换其中一层时,另一层就不用跟着重写。
基础概念
第三方库是项目外部维护、通过包管理器安装的模块;本地模块由项目自身拥有。边界决定升级风险、替换成本和哪些细节可以依赖。
进一步理解
业务代码最好通过少量适配层接触复杂第三方接口,避免几十个组件直接绑定同一个库的内部写法。升级或替换时,变化便能集中处理。
本地模块也要有边界。界面模块不应跨层导入数据库实现,数据模块不应直接操作 DOM;依赖方向应从高层需求指向稳定接口。
日期格式库由 `formatTaskDate` 适配函数包装,任务卡只接收已经格式化的文本,不关心库版本与时区参数。
封装不是给每个第三方函数再起一个同义名称。只有当适配业务语义、隔离变化或统一错误时才有价值。
这一小节记住:依赖可以使用,但要把变化限制在清楚边界内。
- 项目使用支持 ES Modules 的浏览器或构建工具。
- 两个示例片段分别位于 task-store.ts 与 main.ts。
// task-store.ts
export function addTask(title: string) {
return { id: crypto.randomUUID(), title, done: false };
}
// main.ts
import { addTask } from "./task-store";使用方只依赖公开函数,不需要知道模块内部如何生成任务。
- task-store 明确导出 addTask,内部使用 randomUUID 生成标识。
- main 通过相对路径导入函数,只依赖它的参数与返回结果。
- 以后 task-store 改用 API 时,只要公开契约不变,main 不必知道内部细节。
调用 addTask('阅读文档') 会得到含唯一 id、标题和未完成状态的对象;导入路径错误时,构建工具会指出找不到模块。
AI COLLABORATION
AI 如何参与
让 AI 画出模块依赖图,并说明新增依赖为什么应该放在这一层。
推荐协作顺序
- 1
先让 AI 根据现有函数画出谁调用谁,不急着拆文件。
- 2
确认数据、界面和入口三块职责后,再逐块迁移。
- 3
迁移完成后检查 import 路径和浏览器控制台,确保没有循环依赖。
请把一个包含 tasks 数组、addTask 函数、renderTasks 函数和点击事件的单文件脚本拆成 ES Modules。先画依赖图,再给 task-store.js、task-list.js、main.js 的最小代码。解释每个 import/export,避免循环依赖。
一个单向、可追踪的依赖图:入口调用数据与界面模块,数据层不反向依赖页面。
人工检查清单
- 确认文件路径带正确扩展名、script 使用 type=module,并且没有重新制造全局变量。
- 让 AI 画出 import 方向并标出循环依赖、跨层引用和隐含全局变量。
- 运行模块入口并故意改错一次导入路径,确认能从错误信息追到真实文件。
COMMON TRAPS
常见误区
错误不是需要隐藏的失败,而是帮助你看清系统边界的证据。下面三类问题在 AI 辅助学习中最常出现。
为了模块化而切得太碎
- 你会看到
- 一个简单按钮要跳过五个文件才能看懂。
- 为什么发生
- 一个万能 utils 会吸收互不相关的职责,调用者只知道“都在这里”,最终形成新的全局依赖中心。
- 怎样纠正
- 按职责和变化原因拆分,不按代码行数拆分。
模块之间互相调用
- 你会看到
- A 导入 B,B 又导入 A,启动时得到未初始化的值。
- 为什么发生
- 跨越模块公开边界引用内部文件,短期少写一层,长期会让任何目录调整都扩散到全项目。
- 怎样纠正
- 把共同依赖上移到第三个模块,保持数据流方向清楚。
换成 import 后忘记入口设置
- 你会看到
- 浏览器提示 import 不能在普通脚本中使用。
- 为什么发生
- 循环依赖使模块初始化互相等待,某些值在使用时尚未建立,错误常随打包方式变化。
- 怎样纠正
- 确认 script 使用 type=module,并检查相对路径和扩展名。
HANDS-ON
动手任务
把三个互相依赖的脚本改造成显式模块,并从一个入口调用。
- 完成 3.5「第一次把网页发布到公网」
跟着做
- 01
列出当前 script.js 中的数据、渲染与事件三类职责。
- 02
创建 task-store.js,导出读取和新增任务的方法。
- 03
创建 task-list.js,只接收任务并返回或更新列表。
- 04
创建 main.js 导入两者并绑定按钮事件。
- 05
把 HTML 脚本改为 type=module,验证功能与错误控制台。
看到 import 就能追踪功能从哪里来,看到 export 就知道模块提供什么。
加入 removeTask,但保持数据模块不知道 DOM 元素的存在。
离开本课前,自问四件事
- 全局污染与暗依赖分别是什么,为什么项目变大后更危险?
- import、export 和入口模块各自承担什么职责?
- 我能否画出任务应用三个模块的依赖方向?
- 什么时候值得为第三方库建立适配边界,什么时候只是多余包装?
确认完成后,会同步更新学习中心的课程学习进度。