用户工具

站点工具


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