javascript:第十章模块化
目录
第十章 模块化开发
本章目标
- 理解模块化编程的概念和重要性
- 掌握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开发的基石,掌握模块化的思想和技巧对于构建可维护的大型应用至关重要。
javascript/第十章模块化.txt · 最后更改: 由 127.0.0.1
