什么是模块

JavaScript 模块(Modules)允许你将代码分割成独立的文件,每个文件包含特定的功能。 这让代码更容易维护、测试和重用,是现代 JavaScript 开发的重要特性。

// 传统方式:所有代码在一个文件中
function calculateArea(radius) {
return Math.PI * radius * radius;
}
function calculateVolume(radius) {
return (4 / 3) * Math.PI * radius * radius * radius;
}
// ... 更多函数
// 模块方式:分离到不同文件
// math.js - 专门处理数学计算
// utils.js - 通用工具函数
// main.js - 主程序逻辑

模块系统解决了以下问题:

  • 命名空间污染:避免全局变量冲突
  • 代码组织:按功能分离代码
  • 依赖管理:明确模块间的依赖关系
  • 代码重用:模块可以在多个项目中使用

模块的基本使用

HTML 中启用模块

要使用模块,需要在 <script> 标签中添加 type="module":

<!DOCTYPE html>
<html>
<head>
<title>模块示例</title>
</head>
<body>
<!-- 传统脚本 -->
<script src="old-style.js"></script>
<!-- 模块脚本 -->
<script type="module" src="main.js"></script>
<!-- 内联模块 -->
<script type="module">
import { greeting } from "./utils.js";
console.log(greeting("World"));
</script>
</body>
</html>

导出 (Export)

模块通过 export 关键字向外暴露功能。有两种导出方式:命名导出和默认导出。

命名导出 (Named Exports)

方式1:内联导出

math.js
export const PI = 3.14159;
export const E = 2.71828;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export class Calculator {
static add(a, b) {
return a + b;
}
static subtract(a, b) {
return a - b;
}
}

方式2:统一导出

math.js
const PI = 3.14159;
const E = 2.71828;
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
class Calculator {
static add(a, b) {
return a + b;
}
static subtract(a, b) {
return a - b;
}
}
// 在文件末尾统一导出
export { PI, E, add, multiply, Calculator };

重命名导出

math.js
function calculateCircleArea(radius) {
return Math.PI * radius * radius;
}
function calculateRectangleArea(width, height) {
return width * height;
}
// 导出时重命名
export {
calculateCircleArea as circleArea,
calculateRectangleArea as rectArea,
};

默认导出 (Default Export)

每个模块只能有一个默认导出:

logger.js
class Logger {
constructor(name) {
this.name = name;
}
log(message) {
console.log(`[${this.name}] ${message}`);
}
error(message) {
console.error(`[${this.name}] ERROR: ${message}`);
}
warn(message) {
console.warn(`[${this.name}] WARNING: ${message}`);
}
}
export default Logger;
// message.js - 导出函数
const createMessage = (name, age) => {
return `Hello, I'm ${name} and I'm ${age} years old.`;
};
export default createMessage;
// config.js - 导出对象
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retryAttempts: 3,
debug: true,
};
export default config;

混合导出

可以同时使用命名导出和默认导出:

api.js
const BASE_URL = "https://api.example.com";
class ApiClient {
constructor(baseUrl = BASE_URL) {
this.baseUrl = baseUrl;
}
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`);
return response.json();
}
async post(endpoint, data) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return response.json();
}
}
// 工具函数
export function formatResponse(data) {
return {
success: true,
data: data,
timestamp: Date.now(),
};
}
export function handleError(error) {
console.error("API Error:", error);
return {
success: false,
error: error.message,
timestamp: Date.now(),
};
}
// 默认导出主要的类
export default ApiClient;
// 同时导出常量
export { BASE_URL };

导入 (Import)

使用 import 关键字从其他模块导入功能。

导入命名导出

main.js
import { PI, add, multiply, Calculator } from "./math.js";
console.log("PI的值:", PI); // 3.14159
console.log("2 + 3 =", add(2, 3)); // 5
console.log("4 * 5 =", multiply(4, 5)); // 20
console.log("计算器: 10 + 5 =", Calculator.add(10, 5)); // 15

导入默认导出

app.js
import Logger from "./logger.js";
import createMessage from "./message.js";
import config from "./config.js";
const logger = new Logger("MyApp");
logger.log("应用启动");
const message = createMessage("Kayn", 17);
logger.log(message);
console.log("配置:", config);

重命名导入

main.js
import {
circleArea as calculateCircle,
rectArea as calculateRectangle,
} from "./math.js";
console.log("圆形面积:", calculateCircle(5));
console.log("矩形面积:", calculateRectangle(4, 6));

导入所有命名导出

main.js
import * as MathUtils from "./math.js";
console.log("PI:", MathUtils.PI);
console.log("加法:", MathUtils.add(2, 3));
console.log("计算器:", MathUtils.Calculator.subtract(10, 3));

混合导入

main.js
import ApiClient, { formatResponse, handleError, BASE_URL } from "./api.js";
const client = new ApiClient();
console.log("基础URL:", BASE_URL);
client
.get("/users/1")
.then((data) => {
const response = formatResponse(data);
console.log("格式化响应:", response);
})
.catch((error) => {
const errorResponse = handleError(error);
console.log("错误响应:", errorResponse);
});

仅导入执行

有时只需要执行模块代码,不需要导入任何内容:

init.js
console.log("初始化应用...");
window.APP_VERSION = "1.0.0";
// 配置全局错误处理
window.addEventListener("error", (event) => {
console.error("全局错误:", event.error);
});
main.js
import "./init.js"; // 仅执行,不导入任何内容
import Logger from "./logger.js";
const logger = new Logger("App");
logger.log(`应用版本: ${window.APP_VERSION}`);

实际应用场景

1. 工具函数模块

utils.js
export function formatDate(date) {
return date.toLocaleDateString("zh-CN");
}
export function formatCurrency(amount) {
return new Intl.NumberFormat("zh-CN", {
style: "currency",
currency: "CNY",
}).format(amount);
}
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
export function generateId() {
return Math.random().toString(36).substr(2, 9);
}

2. 组件模块

components/Button.js
export default class Button {
constructor(text, onClick, className = "btn") {
this.element = document.createElement("button");
this.element.textContent = text;
this.element.className = className;
this.element.addEventListener("click", onClick);
}
render(container) {
container.appendChild(this.element);
return this;
}
setText(text) {
this.element.textContent = text;
return this;
}
setDisabled(disabled) {
this.element.disabled = disabled;
return this;
}
}
components/Modal.js
export default class Modal {
constructor(title, content) {
this.title = title;
this.content = content;
this.isOpen = false;
this.createElement();
}
createElement() {
this.overlay = document.createElement("div");
this.overlay.className = "modal-overlay";
this.overlay.style.display = "none";
this.modal = document.createElement("div");
this.modal.className = "modal";
this.modal.innerHTML = `
<div class="modal-header">
<h3>${this.title}</h3>
<button class="close-btn">&times;</button>
</div>
<div class="modal-content">
${this.content}
</div>
`;
this.overlay.appendChild(this.modal);
document.body.appendChild(this.overlay);
// 绑定事件
this.modal.querySelector(".close-btn").addEventListener("click", () => {
this.close();
});
this.overlay.addEventListener("click", (e) => {
if (e.target === this.overlay) {
this.close();
}
});
}
open() {
this.isOpen = true;
this.overlay.style.display = "flex";
return this;
}
close() {
this.isOpen = false;
this.overlay.style.display = "none";
return this;
}
}

3. 服务模块

services/UserService.js
import ApiClient from "../api.js";
import { formatResponse, handleError } from "../api.js";
class UserService {
constructor() {
this.api = new ApiClient();
}
async getUser(id) {
try {
const data = await this.api.get(`/users/${id}`);
return formatResponse(data);
} catch (error) {
return handleError(error);
}
}
async createUser(userData) {
try {
const data = await this.api.post("/users", userData);
return formatResponse(data);
} catch (error) {
return handleError(error);
}
}
async updateUser(id, userData) {
try {
const data = await this.api.put(`/users/${id}`, userData);
return formatResponse(data);
} catch (error) {
return handleError(error);
}
}
}
export default new UserService(); // 导出单例

4. 主应用模块

app.js
import Button from "./components/Button.js";
import Modal from "./components/Modal.js";
import UserService from "./services/UserService.js";
import { formatDate, formatCurrency, debounce } from "./utils.js";
class App {
constructor() {
this.init();
}
init() {
this.setupUI();
this.bindEvents();
}
setupUI() {
const container = document.getElementById("app");
// 创建按钮
this.userButton = new Button(
"获取用户信息",
() => {
this.loadUser(1);
},
"btn btn-primary",
);
this.userButton.render(container);
// 创建模态框
this.modal = new Modal("用户信息", '<div id="user-info">加载中...</div>');
}
bindEvents() {
// 使用防抖的搜索
const searchInput = document.getElementById("search");
if (searchInput) {
const debouncedSearch = debounce((query) => {
this.search(query);
}, 300);
searchInput.addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});
}
}
async loadUser(userId) {
this.userButton.setDisabled(true);
const response = await UserService.getUser(userId);
if (response.success) {
const user = response.data;
const userInfo = `
<h4>${user.name}</h4>
<p>邮箱: ${user.email}</p>
<p>注册日期: ${formatDate(new Date(user.createdAt))}</p>
<p>余额: ${formatCurrency(user.balance)}</p>
`;
document.getElementById("user-info").innerHTML = userInfo;
this.modal.open();
} else {
alert("加载用户信息失败: " + response.error);
}
this.userButton.setDisabled(false);
}
search(query) {
console.log("搜索:", query);
// 实现搜索逻辑
}
}
// 启动应用
document.addEventListener("DOMContentLoaded", () => {
new App();
});

动态导入 (Dynamic Import)

ES2020 引入了动态导入,允许在运行时按需加载模块:

// 条件加载模块
async function loadFeature(featureName) {
if (featureName === "chart") {
const ChartModule = await import("./features/charts.js");
return ChartModule.default;
} else if (featureName === "editor") {
const EditorModule = await import("./features/editor.js");
return EditorModule.default;
}
}
// 懒加载模块
document.getElementById("load-chart").addEventListener("click", async () => {
const ChartClass = await loadFeature("chart");
const chart = new ChartClass();
chart.render();
});
// 错误处理
async function safeImport(modulePath) {
try {
const module = await import(modulePath);
return module;
} catch (error) {
console.error(`加载模块失败: ${modulePath}`, error);
return null;
}
}

模块的注意事项和最佳实践

1. HTTP(S) 协议要求

模块只能在 HTTP(S) 协议下工作,不能在 file:// 协议下使用:

// ❌ 这在本地文件中不工作
// file:///path/to/index.html
// ✅ 需要使用服务器
// http://localhost:3000/index.html
// https://example.com/index.html

2. 相对路径和绝对路径

// ✅ 相对路径 - 推荐
import { utils } from "./utils.js";
import { config } from "../config.js";
import { api } from "../../api/client.js";
// ✅ 绝对路径
import { helper } from "/js/helpers.js";
// ❌ 不要省略文件扩展名
import { utils } from "./utils"; // 在浏览器中会报错

3. 模块作用域

module1.js
var globalVar = "I am global"; // 实际上不是全局的
let moduleVar = "I am module scoped";
console.log(this); // undefined(在模块中)
// module2.js
console.log(globalVar); // ReferenceError: globalVar is not defined

4. 模块缓存

counter.js
let count = 0;
export function increment() {
count++;
return count;
}
export function getCount() {
return count;
}
main.js
import { increment, getCount } from "./counter.js";
import { increment as inc } from "./counter.js"; // 相同模块
console.log(getCount()); // 0
increment();
console.log(getCount()); // 1
inc(); // 操作同一个 count 变量
console.log(getCount()); // 2

5. 循环依赖处理

a.js
import { b } from "./b.js";
export const a = "A";
console.log("a.js:", b); // undefined(此时b还未初始化)
// b.js
import { a } from "./a.js";
export const b = "B";
console.log("b.js:", a); // 'A'(a已经初始化)

避免循环依赖的策略:

a.js
// shared.js - 提取共同依赖
export const shared = "shared data";
import { shared } from "./shared.js";
export const a = "A";
// b.js
import { shared } from "./shared.js";
export const b = "B";

6. 命名约定

// ✅ 好的命名
export class UserService {} // 类使用 PascalCase
export const API_URL = "https://api.com"; // 常量使用 UPPER_CASE
export function formatDate(date) {} // 函数使用 camelCase
// ✅ 文件命名
// UserService.js - 类文件
// api-client.js - 多词文件名使用连字符
// utils.js - 工具函数
// constants.js - 常量文件

7. 性能考虑

// ❌ 不好的做法:导入整个模块但只使用一小部分
import * as _ from "lodash";
const result = _.map(data, (item) => item.name);
// ✅ 更好的做法:只导入需要的函数
import { map } from "lodash";
const result = map(data, (item) => item.name);
// ✅ 或者使用具体的子模块
import map from "lodash/map";
const result = map(data, (item) => item.name);

现代构建工具中的模块

Webpack/Vite 中的模块

// 在构建工具中,可以导入 CSS、图片等资源
import "./styles.css"; // CSS 文件
import logoUrl from "./assets/logo.png"; // 图片文件
import data from "./data.json"; // JSON 文件
// 环境变量
import { API_URL } from "./config/production.js";
// 代码分割
const LazyComponent = lazy(() => import("./LazyComponent.js"));

Node.js 中的 ES 模块

// package.json 中启用 ES 模块
{
"type": "module"
}
// 或者使用 .mjs 扩展名
// utils.mjs
export function helper() {
console.log('Helper function');
}

模块模式对比

ES6 模块 vs CommonJS

// ES6 模块 (浏览器/现代 Node.js)
import fs from "fs";
import { readFile } from "fs";
export default function () {}
export const helper = () => {};
// CommonJS (传统 Node.js)
const fs = require("fs");
const { readFile } = require("fs");
module.exports = function () {};
exports.helper = () => {};

总结

JavaScript 模块系统提供了强大的代码组织能力:

特性描述
命名导出可以导出多个具名功能
默认导出每个模块一个主要导出
动态导入运行时按需加载模块
作用域隔离模块有独立的作用域
静态分析构建工具可以优化和打包

最佳实践总结

  • 优先使用命名导出,默认导出用于主要功能
  • 保持模块功能单一,职责清晰
  • 使用相对路径导入本地模块
  • 避免循环依赖
  • 合理使用动态导入进行代码分割
  • 在服务器环境下开发和测试模块功能

模块化是现代 JavaScript 开发的基础,掌握好模块系统对于构建可维护的大型应用至关重要。