JavaScript 模块化
模块化是将代码拆分成独立、可复用单元的最佳实践。
模块化基础
什么是模块
模块是一个独立的功能单元,具有以下特点:
- 独立性:模块内部的功能不污染全局作用域
- 封装性:模块内部实现细节对外不可见
- 可复用性:模块可以在不同项目中重复使用
- 依赖管理:模块可以声明依赖其他模块
模块化解决的问题
// 全局变量污染
var name = "张三";
var name = "李四"; // 覆盖了前面的变量
// 命名冲突
function calculate() {}
function calculate() {} // 报错
// 依赖关系混乱
// 难以维护和测试
ES6 模块
基本语法
// math.js
export const PI = 3.14159;
export const E = 2.71828;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// 默认导出
export default function multiply(a, b) {
return a * b;
}
// 主文件
import multiply, { PI, add } from "./math.js";
console.log(PI); // 3.14159
console.log(add(1, 2)); // 3
console.log(multiply(2, 3)); // 6
命名导出
// 方法1:直接导出
export const name = "张三";
export function greet() {}
// 方法2:先定义,再导出
const name = "张三";
function greet() {}
export { name, greet };
// 方法3:导出时重命名
export { name as userName, greet as sayHello };
命名导入
// 导入指定的导出
import { name, greet } from "./module.js";
// 导入时重命名
import { name as userName, greet as sayHello } from "./module.js";
// 导入所有命名导出
import * as module from "./module.js";
console.log(module.name);
console.log(module.greet());
默认导入
// 默认导出
export default class User {
constructor(name) {
this.name = name;
}
}
// 导入时不需要大括号
import User from "./user.js";
const user = new User("张三");
混合导入
// module.js
export const named1 = "命名导出1";
export default function defaultFunc() {}
export const named2 = "命名导出2";
// 主文件
import defaultFunc, { named1, named2 } from "./module.js";
// 或
import defaultFunc, * as module from "./module.js";
Node.js 模块
CommonJS 语法
// 导出
module.exports = {
name: "张三",
greet: function() {
console.log("你好");
}
};
// 或
exports.add = function(a, b) {
return a + b;
};
exports.PI = 3.14159;
导入模块
const utils = require("./utils.js");
console.log(utils.name);
utils.greet();
module.exports vs exports
// 错误:这样会断开与 module.exports 的连接
exports = {
name: "张三"
};
// 正确:直接添加属性
exports.name = "张三";
exports.greet = function() {};
// 正确:完全替换导出
module.exports = {
name: "张三"
};
模块缓存
// moduleA.js
console.log("模块加载");
module.exports = { value: Date.now() };
// 主文件
const obj1 = require("./moduleA");
const obj2 = require("./moduleA");
console.log(obj1 === obj2); // true - 同一个对象
模块的加载顺序
同步加载
// 加载顺序
const a = require("./a"); // 先加载 a
const b = require("./b"); // 再加载 b
循环依赖
// a.js
console.log("a 开始");
const b = require("./b");
console.log("a 加载完成,b:", b);
module.exports = { a: "a 模块" };
// b.js
console.log("b 开始");
const a = require("./a");
console.log("b 加载完成,a:", a);
module.exports = { b: "b 模块" };
// 结果:
// a 开始
// b 开始
// b 加载完成,a: {}
// a 加载完成,b: { b: 'b 模块' }
ES6 模块循环依赖
// a.mjs
console.log("a 开始");
import { b } from "./b.mjs";
console.log("a 获取到 b:", b);
export const a = "a 模块";
console.log("a 导出完成");
// b.mjs
console.log("b 开始");
import { a } from "./a.mjs";
console.log("b 获取到 a:", a);
export const b = "b 模块";
console.log("b 导出完成");
// 结果:
// a 开始
// b 开始
// b 获取到 a: undefined
// b 导出完成
// a 获取到 b: { b: 'b 模块' }
// a 导出完成
动态导入
import() 函数
// 动态导入模块
button.addEventListener("click", async () => {
const module = await import("./module.js");
module.greet();
});
按需加载
// 根据条件加载不同模块
async function loadModule(type) {
if (type === "math") {
const { calculate } = await import("./math.js");
return calculate;
} else if (type === "string") {
const { process } = await import("./string.js");
return process;
}
}
包管理器
npm
# 初始化项目
npm init -y
# 安装依赖
npm install lodash
# 安装开发依赖
npm install --save-dev eslint
# 卸载依赖
npm uninstall lodash
# 查看已安装的包
npm list
package.json
{
"name": "my-project",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "jest"
},
"dependencies": {
"lodash": "^4.17.21"
},
"devDependencies": {
"jest": "^29.0.0"
}
}
yarn 和 pnpm
# yarn
yarn add lodash
yarn remove lodash
# pnpm
pnpm add lodash
pnpm remove lodash
模块化最佳实践
目录结构
src/
├── modules/
│ ├── user/
│ │ ├── index.js # 入口文件
│ │ ├── User.js # 类定义
│ │ ├── UserService.js # 服务
│ │ └── UserModel.js # 模型
│ └── utils/
│ ├── index.js
│ ├── format.js
│ └── validation.js
├── components/
├── pages/
└── main.js
barrel 文件(索引文件)
// modules/user/index.js
export { User } from "./User.js";
export { UserService } from "./UserService.js";
export { UserModel } from "./UserModel.js";
// 使用
import { User, UserService } from "./modules/user/index.js";
模块设计原则
- 单一职责:每个模块只做一件事
- 高内聚低耦合:模块内部紧密相关,模块之间依赖最小
- 命名规范:使用有意义的命名
- 避免循环依赖:设计时避免模块之间的循环引用
ES Module 与 CommonJS 对比
| 特性 | ES Module | CommonJS |
|---|---|---|
| 语法 | import/export | require/module.exports |
| 加载 | 静态(编译时) | 动态(运行时) |
| 值类型 | 动态绑定 | 拷贝 |
| 循环依赖 | 较难处理 | 较易处理 |
| 浏览器支持 | 需要构建 | 需要构建 |
| Node.js 支持 | 需要 .mjs 或配置 | 原生支持 |
小结
- 模块化解决全局污染和命名冲突问题
- ES6 模块使用 import/export 语法
- Node.js 使用 CommonJS 规范
- 动态导入支持代码分割和按需加载
- npm/yarn/pnpm 是常用的包管理器
练习
- 将一个文件拆分成多个模块
- 实现模块间的循环依赖处理
- 使用动态导入实现代码分割
- 搭建一个简单的项目结构