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 - 主程序逻辑模块系统解决了以下问题:
要使用模块,需要在 <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 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; }}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 };function calculateCircleArea(radius) { return Math.PI * radius * radius;}
function calculateRectangleArea(width, height) { return width * height;}
// 导出时重命名export { calculateCircleArea as circleArea, calculateRectangleArea as rectArea,};每个模块只能有一个默认导出:
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;可以同时使用命名导出和默认导出:
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 { PI, add, multiply, Calculator } from "./math.js";
console.log("PI的值:", PI); // 3.14159console.log("2 + 3 =", add(2, 3)); // 5console.log("4 * 5 =", multiply(4, 5)); // 20console.log("计算器: 10 + 5 =", Calculator.add(10, 5)); // 15import 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);import { circleArea as calculateCircle, rectArea as calculateRectangle,} from "./math.js";
console.log("圆形面积:", calculateCircle(5));console.log("矩形面积:", calculateRectangle(4, 6));import * as MathUtils from "./math.js";
console.log("PI:", MathUtils.PI);console.log("加法:", MathUtils.add(2, 3));console.log("计算器:", MathUtils.Calculator.subtract(10, 3));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); });有时只需要执行模块代码,不需要导入任何内容:
console.log("初始化应用...");window.APP_VERSION = "1.0.0";
// 配置全局错误处理window.addEventListener("error", (event) => { console.error("全局错误:", event.error);});import "./init.js"; // 仅执行,不导入任何内容import Logger from "./logger.js";
const logger = new Logger("App");logger.log(`应用版本: ${window.APP_VERSION}`);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);}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; }}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">×</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; }}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(); // 导出单例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();});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; }}模块只能在 HTTP(S) 协议下工作,不能在 file:// 协议下使用:
// ❌ 这在本地文件中不工作// file:///path/to/index.html
// ✅ 需要使用服务器// http://localhost:3000/index.html// https://example.com/index.html// ✅ 相对路径 - 推荐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"; // 在浏览器中会报错var globalVar = "I am global"; // 实际上不是全局的let moduleVar = "I am module scoped";
console.log(this); // undefined(在模块中)
// module2.jsconsole.log(globalVar); // ReferenceError: globalVar is not definedlet count = 0;
export function increment() { count++; return count;}
export function getCount() { return count;}import { increment, getCount } from "./counter.js";import { increment as inc } from "./counter.js"; // 相同模块
console.log(getCount()); // 0increment();console.log(getCount()); // 1inc(); // 操作同一个 count 变量console.log(getCount()); // 2import { b } from "./b.js";export const a = "A";console.log("a.js:", b); // undefined(此时b还未初始化)
// b.jsimport { a } from "./a.js";export const b = "B";console.log("b.js:", a); // 'A'(a已经初始化)避免循环依赖的策略:
// shared.js - 提取共同依赖export const shared = "shared data";
import { shared } from "./shared.js";export const a = "A";
// b.jsimport { shared } from "./shared.js";export const b = "B";// ✅ 好的命名export class UserService {} // 类使用 PascalCaseexport const API_URL = "https://api.com"; // 常量使用 UPPER_CASEexport function formatDate(date) {} // 函数使用 camelCase
// ✅ 文件命名// UserService.js - 类文件// api-client.js - 多词文件名使用连字符// utils.js - 工具函数// constants.js - 常量文件// ❌ 不好的做法:导入整个模块但只使用一小部分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);// 在构建工具中,可以导入 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"));// package.json 中启用 ES 模块{ "type": "module"}
// 或者使用 .mjs 扩展名// utils.mjsexport function helper() { console.log('Helper function');}// 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 开发的基础,掌握好模块系统对于构建可维护的大型应用至关重要。