JavaScript 基礎:模組系統

2026-01-26 07:56 | By justin | JavaScript
(Updated: 2026-01-26 07:56)

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 留言

目前沒有留言

發表留言
回覆