跳到主要内容

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";

模块设计原则

  1. 单一职责:每个模块只做一件事
  2. 高内聚低耦合:模块内部紧密相关,模块之间依赖最小
  3. 命名规范:使用有意义的命名
  4. 避免循环依赖:设计时避免模块之间的循环引用

ES Module 与 CommonJS 对比

特性ES ModuleCommonJS
语法import/exportrequire/module.exports
加载静态(编译时)动态(运行时)
值类型动态绑定拷贝
循环依赖较难处理较易处理
浏览器支持需要构建需要构建
Node.js 支持需要 .mjs 或配置原生支持

小结

  1. 模块化解决全局污染和命名冲突问题
  2. ES6 模块使用 import/export 语法
  3. Node.js 使用 CommonJS 规范
  4. 动态导入支持代码分割和按需加载
  5. npm/yarn/pnpm 是常用的包管理器

练习

  1. 将一个文件拆分成多个模块
  2. 实现模块间的循环依赖处理
  3. 使用动态导入实现代码分割
  4. 搭建一个简单的项目结构