JavaScript 基礎:模組系統
各位好!
這篇是 JavaScript 系列的第十單元,我們要學習模組系統(Modules)。模組讓我們可以將程式碼拆分成多個檔案,提高可維護性和重用性。
為什麼需要模組?
// 沒有模組的問題:
// 1. 所有程式碼在一個檔案裡,難以維護
// 2. 變數污染全域作用域
// 3. 難以重用程式碼
// 4. 依賴關係不明確
// 使用模組的好處:
// 1. 程式碼組織清晰
// 2. 避免命名衝突
// 3. 方便重用和測試
// 4. 明確的依賴關係
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;
}
// 或一次匯出多個
const PI = 3.14159;
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
export { PI, add, subtract };
匯入(Import)
// main.js - 匯入特定項目
import { PI, add, subtract } from "./math.js";
console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
// 匯入時重新命名
import { add as sum, subtract as diff } from "./math.js";
console.log(sum(5, 3)); // 8
console.log(diff(5, 3)); // 2
// 匯入所有內容
import * as math from "./math.js";
console.log(math.PI);
console.log(math.add(5, 3));
console.log(math.subtract(5, 3));
預設匯出(Default Export)
每個模組只能有一個預設匯出。
// calculator.js - 預設匯出
export default class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
}
// 或
class Calculator {
// ...
}
export default Calculator;
// 匯入預設匯出(可自訂名稱)
import Calculator from "./calculator.js";
import Calc from "./calculator.js"; // 名稱可以不同
const calc = new Calculator();
console.log(calc.add(5, 3)); // 8
// 函式的預設匯出
// utils.js
export default function(str) {
return str.toUpperCase();
}
// 匯入
import toUpperCase from "./utils.js";
console.log(toUpperCase("hello")); // "HELLO"
混合匯出
同時使用具名匯出和預設匯出。
// user.js
export default class User {
constructor(name) {
this.name = name;
}
}
export const DEFAULT_ROLE = "guest";
export const ADMIN_ROLE = "admin";
export function createUser(name, role = DEFAULT_ROLE) {
const user = new User(name);
user.role = role;
return user;
}
// 匯入
import User, { DEFAULT_ROLE, ADMIN_ROLE, createUser } from "./user.js";
const user1 = new User("Alice");
const user2 = createUser("Bob", ADMIN_ROLE);
重新匯出(Re-export)
將其他模組的匯出再次匯出。
// components/Button.js
export class Button {
// ...
}
// components/Input.js
export class Input {
// ...
}
// components/index.js - 統一匯出點
export { Button } from "./Button.js";
export { Input } from "./Input.js";
// 或
export * from "./Button.js";
export * from "./Input.js";
// 使用
import { Button, Input } from "./components/index.js";
動態匯入
在需要時才載入模組。
// 傳統 import 是靜態的,在檔案開頭執行
// import { heavy } from "./heavy.js"; // 立即載入
// 動態 import 回傳 Promise
async function loadModule() {
const module = await import("./math.js");
console.log(module.add(5, 3)); // 8
}
// 條件式載入
const lang = "zh";
let messages;
if (lang === "zh") {
messages = await import("./i18n/zh.js");
} else {
messages = await import("./i18n/en.js");
}
// 延遲載入(提升效能)
document.querySelector("#showChart").addEventListener("click", async () => {
const { Chart } = await import("./Chart.js");
const chart = new Chart();
chart.render();
});
// 錯誤處理
try {
const module = await import("./module.js");
} catch (error) {
console.error("模組載入失敗:", error);
}
實際範例
範例 1:工具函式庫
// utils/string.js
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function truncate(str, length) {
if (str.length <= length) return str;
return str.slice(0, length) + "...";
}
export function slugify(str) {
return str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_-]+/g, "-")
.replace(/^-+|-+$/g, "");
}
// utils/array.js
export function unique(arr) {
return [...new Set(arr)];
}
export function chunk(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
export function shuffle(arr) {
const result = [...arr];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
// utils/index.js - 統一匯出
export * from "./string.js";
export * from "./array.js";
// 使用
import { capitalize, truncate, unique, chunk } from "./utils/index.js";
console.log(capitalize("hello")); // "Hello"
console.log(truncate("Long text", 5)); // "Long ..."
console.log(unique([1, 2, 2, 3])); // [1, 2, 3]
範例 2:設定檔管理
// config/constants.js
export const API_BASE_URL = "https://api.example.com";
export const TIMEOUT = 5000;
export const MAX_RETRIES = 3;
// config/development.js
export default {
apiUrl: "http://localhost:3000",
debug: true,
logLevel: "verbose"
};
// config/production.js
export default {
apiUrl: "https://api.example.com",
debug: false,
logLevel: "error"
};
// config/index.js
const env = process.env.NODE_ENV || "development";
let config;
if (env === "production") {
config = await import("./production.js");
} else {
config = await import("./development.js");
}
export default config.default;
範例 3:API 客戶端
// api/client.js
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
get(endpoint) {
return this.request(endpoint);
}
post(endpoint, data) {
return this.request(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
}
put(endpoint, data) {
return this.request(endpoint, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
}
delete(endpoint) {
return this.request(endpoint, {
method: "DELETE"
});
}
}
export default ApiClient;
// api/users.js
import ApiClient from "./client.js";
const client = new ApiClient("https://api.example.com");
export async function getUsers() {
return client.get("/users");
}
export async function getUser(id) {
return client.get(`/users/${id}`);
}
export async function createUser(userData) {
return client.post("/users", userData);
}
export async function updateUser(id, userData) {
return client.put(`/users/${id}`, userData);
}
export async function deleteUser(id) {
return client.delete(`/users/${id}`);
}
// 使用
import { getUsers, createUser } from "./api/users.js";
const users = await getUsers();
const newUser = await createUser({ name: "Alice", email: "[email protected]" });
範例 4:狀態管理
// store/store.js
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
setState(updates) {
this.state = { ...this.state, ...updates };
this.notify();
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notify() {
this.listeners.forEach(listener => listener(this.state));
}
}
export default Store;
// store/userStore.js
import Store from "./store.js";
const userStore = new Store({
user: null,
isLoggedIn: false
});
export function login(user) {
userStore.setState({
user,
isLoggedIn: true
});
}
export function logout() {
userStore.setState({
user: null,
isLoggedIn: false
});
}
export function getUser() {
return userStore.getState().user;
}
export function isLoggedIn() {
return userStore.getState().isLoggedIn;
}
export function subscribe(listener) {
return userStore.subscribe(listener);
}
export default userStore;
// 使用
import { login, logout, subscribe } from "./store/userStore.js";
subscribe(state => {
console.log("狀態更新:", state);
});
login({ id: 1, name: "Alice" });
// 狀態更新:{ user: { id: 1, name: "Alice" }, isLoggedIn: true }
logout();
// 狀態更新:{ user: null, isLoggedIn: false }
模組模式
單例模式
// logger.js
class Logger {
constructor() {
this.logs = [];
}
log(message) {
const entry = {
message,
timestamp: new Date(),
level: "info"
};
this.logs.push(entry);
console.log(`[INFO] ${message}`);
}
error(message) {
const entry = {
message,
timestamp: new Date(),
level: "error"
};
this.logs.push(entry);
console.error(`[ERROR] ${message}`);
}
getLogs() {
return [...this.logs];
}
clear() {
this.logs = [];
}
}
// 匯出單例
export default new Logger();
// 使用(所有地方共用同一個實例)
import logger from "./logger.js";
logger.log("應用程式啟動");
logger.error("發生錯誤");
工廠模式
// validators/factory.js
import { EmailValidator } from "./EmailValidator.js";
import { PhoneValidator } from "./PhoneValidator.js";
import { UrlValidator } from "./UrlValidator.js";
export function createValidator(type) {
switch (type) {
case "email":
return new EmailValidator();
case "phone":
return new PhoneValidator();
case "url":
return new UrlValidator();
default:
throw new Error(`未知的驗證器類型:${type}`);
}
}
// 使用
import { createValidator } from "./validators/factory.js";
const emailValidator = createValidator("email");
const isValid = emailValidator.validate("[email protected]");
循環依賴
避免模組之間的循環依賴。
// 不好:循環依賴
// a.js
import { b } from "./b.js";
export const a = "A";
export function useB() {
console.log(b);
}
// b.js
import { a } from "./a.js";
export const b = "B";
export function useA() {
console.log(a);
}
// 解決方法 1:提取共用部分
// shared.js
export const shared = {
a: "A",
b: "B"
};
// a.js
import { shared } from "./shared.js";
export function useB() {
console.log(shared.b);
}
// b.js
import { shared } from "./shared.js";
export function useA() {
console.log(shared.a);
}
// 解決方法 2:依賴注入
// a.js
export function createA(dependencies) {
return {
useB() {
console.log(dependencies.b);
}
};
}
// b.js
export function createB(dependencies) {
return {
useA() {
console.log(dependencies.a);
}
};
}
HTML 中使用模組
<!DOCTYPE html>
<html>
<head>
<title>ES6 Modules</title>
</head>
<body>
<!-- 必須加上 type="module" -->
<script type="module">
import { add } from "./math.js";
console.log(add(5, 3));
</script>
<!-- 或從外部檔案載入 -->
<script type="module" src="./main.js"></script>
<!-- 注意:模組預設是 defer 的 -->
<!-- 模組有自己的作用域,不會污染全域 -->
</body>
</html>
Node.js 模組
CommonJS(舊版)
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
// 或
exports.add = add;
exports.subtract = subtract;
// main.js
const math = require("./math.js");
console.log(math.add(5, 3));
// 或解構
const { add, subtract } = require("./math.js");
ES Modules(現代)
// package.json 需要加上:
{
"type": "module"
}
// 或檔案使用 .mjs 副檔名
// 然後就可以使用 ES6 語法
import { add } from "./math.js";
最佳實踐
1. 模組組織結構
src/
components/
Button/
Button.js
Button.css
index.js
Input/
Input.js
Input.css
index.js
index.js
utils/
string.js
array.js
date.js
index.js
api/
client.js
users.js
posts.js
index.js
store/
store.js
userStore.js
index.js
config/
constants.js
development.js
production.js
index.js
main.js
2. 命名規範
// 檔案名稱使用小駝峰或烤肉串
// button.js 或 button-component.js
// 類別使用大駝峰
export class UserService { }
// 函式和變數使用小駝峰
export function getUserData() { }
export const apiUrl = "...";
// 常數使用全大寫
export const API_BASE_URL = "...";
export const MAX_RETRIES = 3;
3. 匯入順序
// 1. 外部套件
import React from "react";
import axios from "axios";
// 2. 內部模組(絕對路徑)
import { Button } from "@/components/Button";
import { api } from "@/api";
// 3. 相對路徑
import { helper } from "./helper.js";
import styles from "./styles.css";
// 4. 類型匯入(TypeScript)
import type { User } from "./types";
4. 避免預設匯出的問題
// 不好:預設匯出難以重構
export default function() { }
// 使用時名稱可以隨意
import whatever from "./module.js";
import anything from "./module.js";
// 好:具名匯出更明確
export function processData() { }
// 使用時名稱一致
import { processData } from "./module.js";
除錯技巧
// 檢查模組是否載入
console.log("模組已載入:math.js");
// 追蹤匯入
export function add(a, b) {
console.trace("add 被呼叫");
return a + b;
}
// 檢查循環依賴
// 使用工具:madge, circular-dependency-plugin
// 效能監控
console.time("模組載入");
const module = await import("./heavy.js");
console.timeEnd("模組載入");
小結
這篇我們學習了:
- ES6 模組的 import/export 語法
- 預設匯出與具名匯出
- 動態匯入
- 模組的實際應用
- 模組組織與最佳實踐
下一步:
完成這篇後,你已經能夠: - 將程式碼組織成模組 - 使用現代的匯入匯出語法 - 建立可重用的程式碼庫
前往下一篇:單元十一:DOM 操作基礎
在那裡,我們會學習如何操作網頁元素。
0 留言
發表留言