====== 第十章 模块化开发 ======
===== 本章目标 =====
* 理解模块化编程的概念和重要性
* 掌握ES6模块(ESM)的语法和使用
* 了解CommonJS模块规范
* 学会使用模块打包工具
* 掌握模块的最佳实践
===== 10.1 模块化的概念 =====
==== 10.1.1 为什么需要模块化 ====
随着JavaScript应用程序规模的增长,代码组织变得越来越重要。模块化编程提供了一种将代码分割成独立、可复用单元的方法。
**模块化带来的好处:**
* **命名空间管理**:避免全局变量污染
* **代码复用**:模块可以在不同项目间共享
* **依赖管理**:清晰地声明模块间的依赖关系
* **可维护性**:代码结构清晰,易于理解和修改
* **作用域隔离**:每个模块有自己的作用域
==== 10.1.2 JavaScript模块化的演进 ====
* **阶段一:全局函数** - 所有代码暴露在全局作用域
* **阶段二:立即执行函数(IIFE)** - 创建局部作用域
* **阶段三:CommonJS** - Node.js采用的模块规范
* **阶段四:AMD/CMD** - 浏览器端的异步模块定义
* **阶段五:ES6模块** - 语言级别的模块支持
===== 10.2 ES6模块(ESM) =====
==== 10.2.1 导出(export) ====
ES6使用export关键字导出模块中的变量、函数或类。
**命名导出:**
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export class Calculator {
multiply(a, b) {
return a * b;
}
}
**默认导出:**
// utils.js
export default function greet(name) {
return `你好,${name}!`;
}
// 一个模块只能有一个默认导出
// 但可以同时有命名导出
export const version = '1.0.0';
**批量导出:**
// api.js
function getUsers() { /* ... */ }
function getPosts() { /* ... */ }
function createPost(data) { /* ... */ }
export { getUsers, getPosts, createPost };
==== 10.2.2 导入(import) ====
**导入命名导出:**
// main.js
import { add, subtract, PI } from './math.js';
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
console.log(PI); // 3.14159
**导入默认导出:**
import greet from './utils.js';
import greet, { version } from './utils.js'; // 同时导入默认和命名
console.log(greet('张三')); // 你好,张三!
**重命名导入:**
import { add as sum, subtract as minus } from './math.js';
console.log(sum(5, 3)); // 8
console.log(minus(5, 3)); // 2
**导入所有内容:**
import * as math from './math.js';
console.log(math.add(5, 3));
console.log(math.PI);
const calc = new math.Calculator();
==== 10.2.3 模块路径 ====
// 相对路径
import { foo } from './module.js'; // 同级目录
import { bar } from '../utils/helper.js'; // 上级目录
import { baz } from './lib/sub.js'; // 子目录
// 绝对路径(从项目根目录)
import { config } from '/src/config.js';
// URL路径
import { helper } from 'https://cdn.example.com/helper.js';
==== 10.2.4 动态导入 ====
动态导入允许在运行时按需加载模块,返回一个Promise。
// 条件加载
if (userPreferDarkMode) {
const darkTheme = await import('./themes/dark.js');
darkTheme.apply();
}
// 懒加载
button.addEventListener('click', async () => {
const { createChart } = await import('./chart.js');
createChart(data);
});
// 根据条件选择模块
const lang = navigator.language;
const messages = await import(`./locales/${lang}.js`);
===== 10.3 CommonJS模块 =====
==== 10.3.1 module.exports导出 ====
CommonJS是Node.js采用的模块规范,使用require()导入,module.exports导出。
// math.cjs
const PI = 3.14159;
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 导出单个对象
module.exports = {
PI,
add,
subtract
};
// 或者分别导出
exports.PI = PI;
exports.add = add;
==== 10.3.2 require导入 ====
// main.cjs
const math = require('./math.cjs');
console.log(math.add(5, 3)); // 8
console.log(math.subtract(5, 3)); // 2
console.log(math.PI); // 3.14159
// 解构导入
const { add, subtract } = require('./math.cjs');
console.log(add(5, 3)); // 8
==== 10.3.3 ES模块与CommonJS的互操作 ====
// 在ES模块中导入CommonJS模块
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const cjsModule = require('./legacy.cjs');
// 在CommonJS中动态导入ES模块
async function loadESM() {
const esmModule = await import('./modern.mjs');
return esmModule;
}
===== 10.4 模块设计模式 =====
==== 10.4.1 单例模式模块 ====
// config.js
class Config {
#data = new Map();
set(key, value) {
this.#data.set(key, value);
}
get(key) {
return this.#data.get(key);
}
getAll() {
return Object.fromEntries(this.#data);
}
}
// 导出单例
export default new Config();
==== 10.4.2 工厂模式模块 ====
// validator.js
const validators = {
email(value) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value);
},
phone(value) {
const regex = /^1[3-9]\d{9}$/;
return regex.test(value);
},
required(value) {
return value !== undefined && value !== null && value !== '';
},
minLength(value, length) {
return String(value).length >= length;
},
maxLength(value, length) {
return String(value).length <= length;
},
range(value, min, max) {
return value >= min && value <= max;
}
};
export function createValidator(rules) {
return function validate(data) {
const errors = {};
for (const [field, fieldRules] of Object.entries(rules)) {
for (const rule of fieldRules) {
const [ruleName, ...params] = rule.split(':');
const validator = validators[ruleName];
if (!validator) {
throw new Error(`未知的验证规则: ${ruleName}`);
}
const isValid = validator(data[field], ...params);
if (!isValid) {
errors[field] = errors[field] || [];
errors[field].push(`${field}验证失败: ${ruleName}`);
}
}
}
return {
isValid: Object.keys(errors).length === 0,
errors
};
};
}
==== 10.4.3 观察者模式模块 ====
// eventBus.js
class EventBus {
#events = new Map();
on(event, callback) {
if (!this.#events.has(event)) {
this.#events.set(event, []);
}
this.#events.get(event).push(callback);
// 返回取消订阅函数
return () => this.off(event, callback);
}
off(event, callback) {
if (!this.#events.has(event)) return;
const callbacks = this.#events.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
emit(event, data) {
if (!this.#events.has(event)) return;
this.#events.get(event).forEach(callback => {
try {
callback(data);
} catch (error) {
console.error('事件处理错误:', error);
}
});
}
once(event, callback) {
const onceCallback = (data) => {
this.off(event, onceCallback);
callback(data);
};
this.on(event, onceCallback);
}
}
export const eventBus = new EventBus();
export default EventBus;
===== 10.5 模块组织与架构 =====
==== 10.5.1 项目结构示例 ====
project/
├── src/
│ ├── main.js # 入口文件
│ ├── config/
│ │ ├── index.js # 配置导出
│ │ └── database.js # 数据库配置
│ ├── utils/
│ │ ├── index.js # 工具函数导出
│ │ ├── date.js # 日期工具
│ │ ├── format.js # 格式化工具
│ │ └── validator.js # 验证工具
│ ├── components/
│ │ ├── Button.js
│ │ ├── Input.js
│ │ └── index.js # 组件统一导出
│ ├── services/
│ │ ├── api.js # API服务
│ │ ├── auth.js # 认证服务
│ │ └── user.js # 用户服务
│ └── styles/
│ ├── variables.css
│ └── main.css
├── package.json
└── index.html
==== 10.5.2 索引文件模式 ====
// utils/index.js
export { formatDate, parseDate } from './date.js';
export { formatCurrency, formatNumber } from './format.js';
export { createValidator } from './validator.js';
// main.js
import { formatDate, formatCurrency, createValidator } from './utils/index.js';
// 或者简写
import { formatDate } from './utils/index.js';
==== 10.5.3 配置驱动的模块加载 ====
// plugins/index.js
const pluginModules = import.meta.glob('./*.js');
export async function loadPlugins() {
const plugins = [];
for (const path in pluginModules) {
const module = await pluginModules[path]();
plugins.push(module.default);
}
return plugins;
}
// main.js
import { loadPlugins } from './plugins/index.js';
const plugins = await loadPlugins();
plugins.forEach(plugin => plugin.install());
===== 10.6 模块打包与构建 =====
==== 10.6.1 常用构建工具 ====
* **Webpack** - 功能全面的模块打包器
* **Vite** - 基于ES模块的快速构建工具
* **Rollup** - 专注于ES模块的打包器
* **Parcel** - 零配置的打包工具
==== 10.6.2 Tree Shaking ====
Tree Shaking是一种消除死代码的优化技术,只打包实际使用的代码。
// utils.js
export function used() {
return '这个函数会被打包';
}
export function unused() {
return '这个函数会被移除';
}
// main.js
import { used } from './utils.js';
console.log(used());
// unused函数不会被打包
==== 10.6.3 代码分割 ====
// 路由级别的代码分割
const routes = [
{
path: '/dashboard',
component: () => import('./pages/Dashboard.js')
},
{
path: '/profile',
component: () => import('./pages/Profile.js')
},
{
path: '/settings',
component: () => import('./pages/Settings.js')
}
];
===== 10.7 模块最佳实践 =====
==== 10.7.1 导出规范 ====
// ✅ 推荐:明确的命名导出
export function helper() { }
export const CONFIG = { };
// ✅ 推荐:默认导出用于主要功能
export default class MainComponent { }
// ❌ 避免:混合多种导出方式造成困惑
export function helper() { }
export default function main() { }
==== 10.7.2 避免循环依赖 ====
// ❌ 避免:a.js 导入 b.js,b.js 又导入 a.js
// a.js
import { b } from './b.js';
export const a = 'A';
// b.js
import { a } from './a.js'; // 循环依赖!
export const b = 'B';
// ✅ 解决:提取公共代码到单独的模块
// types.js
export const a = 'A';
export const b = 'B';
==== 10.7.3 副作用管理 ====
// polyfill.js
// 有副作用的模块(修改全局对象)
if (!Array.prototype.flat) {
Array.prototype.flat = function(depth = 1) {
// 实现...
};
}
// 导入副作用模块
import './polyfill.js';
===== 本章习题 =====
==== 基础练习 ====
**练习1:基本模块**
创建一个math.js模块,导出基本的数学运算函数(加、减、乘、除),在main.js中导入并使用。
**练习2:默认与命名导出**
创建一个dateUtils.js模块,默认导出formatDate函数,命名导出parseDate和isValidDate函数。
**练习3:模块重构**
将以下代码重构为模块形式:
const users = [];
function addUser(user) {
users.push(user);
}
function getUser(id) {
return users.find(u => u.id === id);
}
function deleteUser(id) {
const index = users.findIndex(u => u.id === id);
if (index > -1) {
users.splice(index, 1);
}
}
==== 进阶练习 ====
**练习4:动态模块加载**
实现一个插件系统,根据配置文件动态加载不同的插件模块。
**练习5:模块测试**
为练习3的用户管理模块编写单元测试。
**练习6:Monorepo结构**
设计一个包含多个包的Monorepo项目结构,包括core、ui、utils三个子包。
==== 思考题 ====
1. ES模块和CommonJS模块的主要区别是什么?各自适用于什么场景?
2. 动态导入有什么优势?在什么情况下应该使用?
3. 如何避免模块间的循环依赖?
4. Tree Shaking是如何工作的?如何编写对Tree Shaking友好的代码?
===== 参考答案 =====
**练习3答案:**
// userService.js
const users = [];
export function addUser(user) {
users.push(user);
}
export function getUser(id) {
return users.find(u => u.id === id);
}
export function deleteUser(id) {
const index = users.findIndex(u => u.id === id);
if (index > -1) {
users.splice(index, 1);
}
}
export function getAllUsers() {
return [...users];
}
// main.js
import { addUser, getUser, deleteUser } from './userService.js';
addUser({ id: 1, name: '张三' });
console.log(getUser(1));
deleteUser(1);
**练习4答案:**
// pluginManager.js
export class PluginManager {
#plugins = new Map();
async load(pluginPath) {
try {
const module = await import(pluginPath);
const plugin = module.default || module;
if (typeof plugin.install !== 'function') {
throw new Error('插件必须提供install方法');
}
this.#plugins.set(plugin.name, plugin);
return plugin;
} catch (error) {
console.error(`加载插件失败 ${pluginPath}:`, error);
throw error;
}
}
async installAll(config) {
for (const pluginConfig of config.plugins) {
const plugin = await this.load(pluginConfig.path);
plugin.install(pluginConfig.options);
}
}
get(name) {
return this.#plugins.get(name);
}
}
// config.json
{
"plugins": [
{ "path": "./plugins/logger.js", "options": { "level": "debug" } },
{ "path": "./plugins/analytics.js", "options": { "trackingId": "UA-xxx" } }
]
}
===== 小结 =====
本章我们学习了JavaScript模块化开发的各个方面:
* **模块化的意义**:代码组织、复用、维护
* **ES6模块**:import/export语法、默认导出、动态导入
* **CommonJS**:require/module.exports、Node.js环境
* **设计模式**:单例、工厂、观察者模式在模块中的应用
* **项目架构**:目录结构、索引文件、配置驱动
* **构建优化**:Tree Shaking、代码分割
* **最佳实践**:导出规范、避免循环依赖、副作用管理
模块化是现代JavaScript开发的基石,掌握模块化的思想和技巧对于构建可维护的大型应用至关重要。