JavaScript 模块化
当项目代码越来越多,把所有内容写在一个文件中是不可维护的。模块化让你将代码拆分成独立的文件,每个文件负责一个功能区域,通过 import 和 export 互相引用。
ES Module 基础
export — 导出
javascript
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator {
multiply(a, b) {
return a * b;
}
} import — 导入
javascript
// app.js
import { PI, add, Calculator } from './math.js';
console.log(PI); // 3.14159
console.log(add(2, 3)); // 5
console.log(new Calculator().multiply(4, 5)); // 20 每个模块有自己的作用域——模块内部的变量不会泄漏到全局,也不会和其他模块冲突。
导出方式
命名导出(Named Export)
一个模块可以有多个命名导出:
javascript
// user.js
export const name = '张三';
export function greet() {
return `你好,${name}`;
} javascript
// 导入
import { name, greet } from './user.js';
// 导入时重命名
import { name as userName, greet as sayHello } from './user.js';
// 导入全部并放命名空间中
import * as User from './user.js';
console.log(User.name); // "张三" 默认导出(Default Export)
每个模块可以有一个默认导出:
javascript
// api.js
export default function fetchData() {
return fetch('/api/data').then(res => res.json());
} javascript
// 导入默认导出——可以任意命名
import getData from './api.js';
// 同时导入默认和命名导出
import getData, { baseUrl } from './api.js'; 选择建议:主要导出一个功能时用默认导出(如一个类、一个组件),导出多个工具函数时用命名导出。一个项目中保持一致即可,混用是常见的但可行。
动态导入 import()
import() 返回一个 Promise,在运行时按需加载模块:
javascript
// 条件加载
if (user.isAdmin) {
const adminModule = await import('./admin.js');
adminModule.initAdminPanel();
}
// 点击时懒加载
button.addEventListener('click', async () => {
const { heavyLibrary } = await import('./heavy-lib.js');
heavyLibrary.process();
}); 动态导入的优势:按需加载代码,减小初始包体积,加速首屏加载。这是现代前端工程中代码分割的基础。
HTML 中使用模块
html
<!-- type="module" 启用 ES Module -->
<script type="module" src="app.js"></script>
<!-- 内联模块 -->
<script type="module">
import { greet } from './utils.js';
console.log(greet('张三'));
</script> type="module" 的 <script> 会自动延迟执行(等同于 defer),且严格模式下运行。
模块 vs 传统脚本
| 特性 | <script> | <script type="module"> |
|---|---|---|
| 执行时机 | 立即执行 | 自动 defer |
| 作用域 | 全局 | 模块作用域(隔离) |
| 严格模式 | 需要声明 'use strict' | 默认严格模式 |
import/export | 不可用 | 可用 |
this | window | undefined |
| 多次引用 | 多次执行 | 单次执行(缓存) |
模块组织建议
plaintext
js/
├── main.js # 入口文件
├── utils/
│ ├── format.js # 格式化工具
│ └── validate.js # 验证工具
├── components/
│ ├── modal.js # 模态框组件
│ └── carousel.js # 轮播组件
└── api/
└── index.js # API 请求封装 一个模块只做一件事,文件名反映其功能。这是保持代码可维护的基本准则。
跨模块共享状态
模块是单例的——导出的同一个变量在所有导入的模块中共享:
javascript
// store.js
export const state = { count: 0 };
export function increment() {
state.count++;
} javascript
// moduleA.js
import { state, increment } from './store.js';
increment();
console.log(state.count); // 1 javascript
// moduleB.js
import { state } from './store.js';
console.log(state.count); // 1(与 moduleA 共享同一个 state 对象) 虽然模块单例可以共享状态,但直接导出一个可变对象会让数据流向难以追踪。在实际项目中,建议使用更明确的状态管理模式(如发布/订阅、Redux、或框架自带的状态管理)。