本文 AI 產出,尚未審核

ES6+ 新特性(Modern JS) – 模組動態載入(import()


簡介

在傳統的 JavaScript 開發中,我們習慣於在檔案最上方使用 靜態 import 來一次載入所有依賴。這種寫法在小型專案或是單頁應用(SPA)仍然可行,但隨著應用規模成長、功能拆分變得更細緻,一次性載入全部程式碼會造成不必要的資源浪費,影響首屏渲染速度與使用者體驗。

ES2020(即 ES6+)引入的 動態模組載入 import(),讓開發者可以在程式執行的任意時刻,根據需求去載入模組。這不僅提升了 程式碼分割(code‑splitting) 的彈性,也為 懶載入(lazy‑loading)條件載入插件化架構 等進階需求提供了原生支援。本文將從概念、語法到實務應用,完整說明 import() 的使用方式與注意事項,幫助你在專案中正確、有效地運用這項功能。


核心概念

1. import() 與靜態 import 的差異

項目 靜態 import 動態 import()
載入時機 編譯階段(模組解析時)即載入 執行階段,呼叫時才載入
語法 import { foo } from './module.js' import('./module.js').then(...)
返回值 直接取得綁定的變數/函式 回傳 Promise,resolve 後得到模組物件
使用限制 必須在模組頂層,不能放在條件或迴圈內 可以放在任意程式碼塊、條件、迴圈等

重點import() 本質上是一個 函式,所以它支援 非同步 操作,且必須在支援 ES 模組的環境(如瀏覽器的 <script type="module"> 或 Node.js 的 ES 模組)中使用。


2. 基本語法與 Promise 介面

import() 接收一個字串參數,代表要載入的模組路徑,並回傳一個 Promise。當模組成功載入時,Promise 會以 模組命名空間物件(module namespace object)作為解析值。

// 動態載入 utils.js,取得模組物件
import('./utils.js')
  .then((module) => {
    // 使用解構賦值取得匯出的函式
    const { formatDate, calculateSum } = module;
    console.log(formatDate(new Date()));
    console.log(calculateSum([1, 2, 3]));
  })
  .catch((err) => {
    console.error('模組載入失敗:', err);
  });

小技巧:如果你只需要單一匯出,直接在 then 中使用 module.default(當模組使用 export default)會更簡潔。


3. 搭配 async/await 的寫法

在支援 async/await 的環境下,import() 的 Promise 可寫成同步感的程式碼,提升可讀性。

async function loadChartLibrary() {
  try {
    const chart = await import('chart.js'); // 只在需要時載入
    const ctx = document.getElementById('myChart').getContext('2d');
    new chart.Chart(ctx, { /* 設定 */ });
  } catch (e) {
    console.error('Chart.js 載入失敗', e);
  }
}

// 假設使用者點擊「顯示圖表」按鈕才呼叫
document.getElementById('showChartBtn').addEventListener('click', loadChartLibrary);

4. 條件載入與程式碼分割

import() 允許根據執行環境或使用者行為條件載入不同的模組,這是實作 程式碼分割(code splitting) 的核心。

function loadFeature(flag) {
  if (flag === 'admin') {
    // 只在管理員介面載入 adminPanel 模組
    return import('./adminPanel.js');
  } else {
    // 一般使用者載入 userDashboard 模組
    return import('./userDashboard.js');
  }
}

// 使用範例
loadFeature(currentUser.role).then((mod) => {
  mod.init(); // 兩個模組皆提供相同的 init 介面
});

5. 動態路徑與 Webpack / Vite 的相容性

在使用打包工具(如 Webpack、Vite)時,動態路徑需要留意工具的分析機制。以下示範在 Webpack 中使用「變數路徑」:

// webpack 會根據以下寫法自動產生多個 chunk
function loadLocale(lang) {
  return import(
    /* webpackChunkName: "locale-[request]" */
    `./locales/${lang}.js`
  );
}

// 依使用者選擇的語系載入對應檔案
loadLocale('zh-TW').then((locale) => {
  console.log('載入中文語系檔案', locale.default);
});

注意:若路徑寫成完全動態(如 import(someVariable)),打包工具無法預先知道要產生哪些 chunk,可能會導致 整個應用程式被打包成單一檔案,失去分割的好處。


程式碼範例

下面提供 五個實務上常見且易於理解import() 範例,涵蓋懶載入、條件載入、錯誤處理、與打包工具的整合。

範例 1️⃣ 懶載入圖表函式庫(Chart.js)

// chartHelper.js
export async function renderBarChart(data) {
  // 只有在真正需要畫圖時才載入 Chart.js
  const { Chart } = await import('chart.js');
  const ctx = document.getElementById('barChart').getContext('2d');
  new Chart(ctx, {
    type: 'bar',
    data,
    options: { responsive: true },
  });
}

說明renderBarChart 只會在呼叫時才下載 Chart.js,減少首屏載入重量。


範例 2️⃣ 條件載入不同的 API 客戶端

// apiClientFactory.js
export function getApiClient(env) {
  if (env === 'mock') {
    // 開發環境使用 mock 伺服器
    return import('./mockClient.js');
  }
  // 正式環境使用真實 API
  return import('./realClient.js');
}

// 使用方式
getApiClient(process.env.NODE_ENV).then((client) => {
  client.fetchData().then(console.log);
});

說明:根據 process.env.NODE_ENV 動態決定載入哪個模組,讓測試與正式環境的程式碼完全分離。


範例 3️⃣ 動態載入多語系檔案(搭配 Vite)

// i18n.js
export async function loadLocale(lang) {
  const locale = await import(
    /* @vite-ignore */ `./locales/${lang}.js`
  );
  // Vite 會自動產生對應的 chunk
  return locale.default; // 每個 locale 檔案都使用 export default
}

// 範例:切換語系
document.getElementById('langSelect').addEventListener('change', async (e) => {
  const messages = await loadLocale(e.target.value);
  // 重新渲染 UI
  updateUI(messages);
});

說明/* @vite-ignore */ 告訴 Vite 允許變數路徑,並在執行時動態載入對應語系。


範例 4️⃣ 錯誤處理:備援載入

async function loadAnalytics() {
  try {
    const analytics = await import('./analytics.js');
    analytics.init();
  } catch (e) {
    console.warn('Analytics 模組載入失敗,改用備援方案');
    // 載入輕量版或直接跳過
    const fallback = await import('./analyticsFallback.js');
    fallback.init();
  }
}

說明:在網路不穩或 CDN 失效時,可提供 備援(fallback) 模組,避免整個功能卡住。


範例 5️⃣ 與 Service Worker 結合的預先快取

// sw.js (Service Worker)
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('dynamic-modules').then((cache) => {
      // 預先快取即將在 UI 中使用的模組
      return cache.addAll([
        '/src/components/HeavyComponent.js',
        '/src/utils/heavyUtil.js',
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('HeavyComponent.js')) {
    event.respondWith(
      caches.match(event.request).then((response) => response || fetch(event.request))
    );
  }
});

說明:在安裝階段先把大型模組快取,使用者點擊後即使離線也能即時載入,提升 PWA 體驗。


常見陷阱與最佳實踐

陷阱 說明 解決方案
1️⃣ 動態路徑未被打包工具偵測 import(someVar) 讓 Webpack/Vite 無法預先產生 chunk,導致整個程式碼被打包成單一檔案。 使用 魔術註解(magic comments)或固定的路徑模式,讓工具能解析。
2️⃣ 重複載入同一模組 多次呼叫 import() 仍只會載入一次(快取),但若使用不同的相對路徑會被視為不同模組。 統一使用絕對或別名路徑,確保相同模組只會有一個入口。
3️⃣ 錯誤處理不完整 忽略 Promise reject,導致未捕獲的例外使應用崩潰。 必須 .catch() 或在 async/await 中使用 try/catch
4️⃣ 依賴遞迴載入 A 模組動態載入 B,B 又動態載入 A,可能產生循環依賴、執行順序不確定。 設計清晰的依賴圖,盡量避免跨模組的相互載入,或使用 事件/回呼 解耦。
5️⃣ 失去 Tree‑shaking 效益 若在 import() 中載入整個大型套件,會把整個套件全部拉下來,失去拆分的好處。 只載入需要的子模組(例如 import('lodash/cloneDeep')),或使用 ES 模組化的套件

最佳實踐

  1. 以功能為單位拆分 Chunk:每個懶載入的檔案盡量只包含單一功能,方便快取與更新。
  2. 搭配 webpackChunkName:在大型專案中,使用魔術註解自訂 chunk 名稱,提升除錯與 CDN 緩存效率。
  3. 先行測試網路環境:在低速或離線情境下提供備援或預快取,避免使用者體驗卡住。
  4. 保持路徑一致性:在 tsconfig.json / jsconfig.json 中設定 path alias,讓開發與打包都使用相同別名。
  5. 監控載入時間:使用 Performance API(performance.markperformance.measure)記錄模組載入時長,找出瓶頸。
// 範例:測量載入時間
performance.mark('load-start');
import('./bigModule.js').then((mod) => {
  performance.mark('load-end');
  performance.measure('bigModule-load', 'load-start', 'load-end');
  console.log('載入完成', mod);
});

實際應用場景

場景 為何使用 import() 範例
單頁應用(SPA)路由切換 只在使用者切換到特定路由時才載入相關頁面模組,減少首屏資源 router.on('/settings', () => import('./pages/Settings.js')).then(m => m.render());
多語系網站 根據使用者語系動態載入對應的翻譯檔,避免一次載入全部語系 loadLocale(userLang).then(messages => i18n.update(messages));
大型圖表或視覺化套件 只有在使用者點擊「顯示報表」時才載入 Chart.js、D3.js 等 button.addEventListener('click', async () => { const { Chart } = await import('chart.js'); /* 繪圖 */ });
插件化平台 第三方插件以 ES 模組形式提供,平台根據需求動態載入 import(./plugins/${pluginId}.js).then(p => p.init(app));
離線優先的 PWA 先在 Service Worker 中快取即將使用的模組,確保離線時仍可載入 (見上方 Service Worker 範例)

總結

  • import() 為 ES6+ 的原生動態載入機制,讓 JavaScript 在 非同步、條件與懶載入 上變得更靈活。
  • 它回傳 Promise,可與 then/catchasync/await 無縫結合,程式碼可讀性大幅提升。
  • 正確使用 魔術註解路徑別名 以及 錯誤處理,即可在 Webpack、Vite 等打包工具中保持 程式碼分割 的效益。
  • 在實務開發中,懶載入大型套件、條件載入不同環境的模組、以及支援多語系與離線快取 都是 import() 的典型應用。
  • 注意常見陷阱(如動態路徑無法被分析、重複載入、循環依賴等),並遵循最佳實踐(功能拆分、命名 Chunk、監控載入時間),即可在專案中安全、有效地運用動態模組載入。

透過本文的概念說明與實作範例,你現在應該已經掌握了 何時、如何以及為什麼 使用 import(),並能在自己的 JavaScript 專案中即刻實踐,讓應用程式更快、更省資源,也更具擴充彈性。祝開發順利!