JavaScript 模組與封裝:動態載入(import())
簡介
在現代前端開發中,模組化 已成為組織程式碼、提升維護性與重用性的核心手段。ES6 (ES2015) 正式引入了靜態 import / export 語法,讓開發者可以在編譯階段就確定依賴關係。然而,隨著單頁應用 (SPA) 越來越龐大、功能拆分越來越細,只在需要時才載入程式碼 成了降低首屏載入時間、提升使用者體驗的關鍵。
import()(又稱「動態匯入」)正是為此而設計的 API:它在執行階段返回一個 Promise,允許我們在程式執行時才載入模組。透過動態載入,我們可以實現程式碼分割 (code‑splitting)、懶載入 (lazy‑loading) 以及條件載入,讓應用程式更輕量、效能更佳。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握 import() 的使用方式,並提供實務應用情境,幫助你在專案中有效運用動態載入。
核心概念
1. import() 與靜態 import 的差異
| 特性 | 靜態 import |
動態 import() |
|---|---|---|
| 載入時機 | 編譯階段即決定,瀏覽器在解析腳本時就會開始載入 | 執行階段才觸發,必須等到程式碼跑到 import() 時才開始載入 |
| 語法 | import { foo } from './module.js'; |
import('./module.js').then(mod => { … }) |
| 返回值 | 直接取得模組的綁定 (binding) | 回傳 Promise<ModuleNamespace> |
| 條件載入 | 不支援(只能在頂層使用) | 完全支援,可放在任何程式碼塊、函式內或條件式中 |
| Tree‑shaking | 可被 bundler 靜態分析,移除未使用的程式碼 | 需要額外設定才能進行 tree‑shaking,因為模組在執行時才決定 |
重點:
import()讓模組載入變成「非同步」操作,這也是它能與await搭配使用的原因。
2. 基本語法
// 使用 Promise
import('./utils/math.js')
.then(math => {
console.log(math.add(2, 3)); // 5
})
.catch(err => {
console.error('載入失敗:', err);
});
// 使用 async/await(更直觀)
async function calculate() {
try {
const { add, multiply } = await import('./utils/math.js');
console.log(add(4, 5)); // 9
console.log(multiply(4, 5)); // 20
} catch (e) {
console.error('載入錯誤', e);
}
}
calculate();
- 模組路徑 必須是相對或絕對 URL(在 Node 環境下則可使用檔案路徑或套件名稱)。
- 返回值 為 module namespace object,其屬性對應於該模組的已匯出成員。
3. 動態載入的常見用途
- 路由懶載入:只有使用者切換到某個路由時才載入對應的頁面或元件。
- 功能分支:根據使用者權限、裝置類型或設定檔決定是否載入特定模組。
- 第三方庫的條件載入:例如在需要圖表功能時才載入
chart.js,避免不必要的下載。 - 國際化 (i18n):根據使用者語系動態載入對應的語言檔。
程式碼範例
以下提供 5 個實用範例,涵蓋從最基礎到較進階的應用情境,並附上說明。
範例 1:最簡單的懶載入
// main.js
document.getElementById('loadBtn').addEventListener('click', async () => {
// 使用者點擊按鈕才載入 math.js
const { add, subtract } = await import('./utils/math.js');
console.log('2 + 3 =', add(2, 3));
console.log('5 - 2 =', subtract(5, 2));
});
說明:
math.js只會在使用者點擊按鈕時才被下載,減少首次載入的資源大小。
範例 2:路由懶載入(配合簡易路由)
// router.js
const routes = {
'/home': () => import('./pages/home.js'),
'/about': () => import('./pages/about.js'),
};
function navigate(path) {
const loader = routes[path];
if (!loader) {
console.error('找不到路由');
return;
}
loader()
.then(mod => {
// 每個 page 模組都預設匯出一個 render 函式
mod.render();
})
.catch(err => console.error('載入頁面失敗', err));
}
// 假設有簡易的 hash 監聽
window.addEventListener('hashchange', () => {
navigate(location.hash.slice(1) || '/home');
});
說明:
home.js、about.js只會在對應路由被觸發時才載入,實現 SPA 的「按需載入」概念。
範例 3:條件載入第三方套件
// chartLoader.js
export async function loadChart(containerId, data) {
// 只在需要圖表時才載入 chart.js
const { Chart } = await import('chart.js/auto'); // 透過 npm 安裝的套件
const ctx = document.getElementById(containerId).getContext('2d');
new Chart(ctx, {
type: 'bar',
data,
options: { responsive: true },
});
}
說明:若使用者的操作不需要圖表,
chart.js完全不會被下載,省下大量檔案大小。
範例 4:多語系動態載入
// i18n.js
export async function loadLocale(lang) {
try {
// 依語系載入對應的 JSON 檔案
const messages = await import(`./locales/${lang}.json`);
// 假設使用一個全域的 i18n 物件來儲存訊息
window.i18n = messages.default; // JSON 模組預設匯出為 default
console.log(`已載入 ${lang} 語系`);
} catch (e) {
console.warn('語系檔案載入失敗,回退至 en');
const fallback = await import('./locales/en.json');
window.i18n = fallback.default;
}
}
說明:只在使用者切換語系時才下載對應的語言檔,避免一次載入全部語系造成的浪費。
範例 5:結合 Webpack / Vite 的代碼分割
// componentLoader.js
export async function loadComponent(name) {
// Webpack 的魔法註解或 Vite 的 import.meta.glob
const module = await import(
/* webpackChunkName: "component-[request]" */
`./components/${name}.js`
);
return module.default; // 假設每個元件都 default 匯出
}
// 使用範例
document.getElementById('widgetBtn').addEventListener('click', async () => {
const Widget = await loadComponent('Widget');
const widgetInstance = new Widget();
widgetInstance.mount('#widgetContainer');
});
說明:透過 bundler 的 魔法註解 或
import.meta.glob,可以自動產生多個 chunk,讓每個元件都能獨立載入。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 忘記處理錯誤 | import() 失敗會拋出 rejected Promise,未處理會導致未捕獲的例外。 |
使用 try / catch 或 .catch(),並提供使用者友善的回饋。 |
| 過度細分 | 把每個小函式都拆成獨立模組,會產生過多 HTTP 請求,反而降低效能。 | 依功能或頁面劃分 chunk,保持適當的大小(約 20‑100KB 為佳)。 |
| 相對路徑錯誤 | 動態路徑在編譯時無法解析,若使用變數拼接路徑可能導致 404。 | 使用 模板字串 搭配 bundler 的 魔法註解,或在 Node 中使用 url.fileURLToPath(import.meta.url) 取得基礎路徑。 |
| 預載入失效 | 想提前載入但忘了使用 import(/* webpackPreload: true */ './module.js')。 |
需要預載入時,使用 bundler 提供的 preload / prefetch 設定。 |
| 模組快取 | import() 會快取已載入的模組,若想重新載入必須手動清除快取。 |
在開發環境可使用 import(${url}?t=${Date.now()}) 加上時間戳記,或使用 import.meta.hot(Vite)等工具。 |
最佳實踐
- 懶載入的時機:將懶載入放在使用者明確交互(點擊、切換)或首次需要的地方。
- 搭配
async/await:程式碼更易讀,同時可直接使用try / catch捕捉錯誤。 - 使用 bundler 的代碼分割功能:透過
webpackChunkName、import.meta.glob或 Vite 的dynamic import,讓打包工具自動產生合理的 chunk。 - 設定
preload/prefetch:對於即將使用的模組,可提前告訴瀏覽器預載 (preload) 或預取 (prefetch)。 - 避免在迴圈內頻繁呼叫
import():一次性載入多個模組,或使用Promise.all合併請求。
實際應用場景
1. 電子商務網站的商品詳情頁
商品列表頁只載入列表資料與 UI,當使用者點擊某筆商品時,才動態載入 商品詳情模組(包含圖庫、評論、相關商品等)。這樣可以讓首屏載入時間縮短至 1‑2 秒,提升 SEO 與轉換率。
2. SaaS 平台的功能授權
不同等級的訂閱方案提供不同功能。登入後,根據使用者的授權等級,使用 import() 動態載入 高階報表、資料分析 等模組,避免未授權使用者下載不必要的程式碼。
3. 行動裝置的 PWA
在行動裝置上,網路頻寬與記憶體有限。透過動態載入,我們可以在 離線快取 前先載入核心 UI,其他功能待使用者點擊時才從 Service Worker 抓取,減少一次性下載的流量。
4. 多語系的企業內部系統
企業內部系統常同時支援多種語系。使用 import() 載入對應語系的 JSON 檔案,結合 navigator.language 自動切換,且不會把所有語系檔一次性塞入 bundle。
總結
import() 為 JavaScript 帶來了 執行時的模組載入 能力,使開發者可以根據使用情境、使用者互動或環境條件,彈性地載入所需程式碼。透過正確的 錯誤處理、代碼分割、預載/預取 設定,我們能在保持程式碼可維護性的同時,大幅減少首屏載入時間與網路資源消耗。
在實務開發中,建議先從 路由懶載入、功能條件載入 等簡單情境開始,逐步導入 bundle 設定、preload 等進階技巧。只要遵循本文的最佳實踐與常見陷阱的防範,動態載入將成為提升使用者體驗與應用效能的有力武器。
掌握
import(),讓你的前端專案既靈活又高效!