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 模組化的套件。 |
最佳實踐
- 以功能為單位拆分 Chunk:每個懶載入的檔案盡量只包含單一功能,方便快取與更新。
- 搭配
webpackChunkName:在大型專案中,使用魔術註解自訂 chunk 名稱,提升除錯與 CDN 緩存效率。 - 先行測試網路環境:在低速或離線情境下提供備援或預快取,避免使用者體驗卡住。
- 保持路徑一致性:在
tsconfig.json/jsconfig.json中設定 path alias,讓開發與打包都使用相同別名。 - 監控載入時間:使用 Performance API(
performance.mark、performance.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/catch或async/await無縫結合,程式碼可讀性大幅提升。 - 正確使用 魔術註解、路徑別名 以及 錯誤處理,即可在 Webpack、Vite 等打包工具中保持 程式碼分割 的效益。
- 在實務開發中,懶載入大型套件、條件載入不同環境的模組、以及支援多語系與離線快取 都是
import()的典型應用。 - 注意常見陷阱(如動態路徑無法被分析、重複載入、循環依賴等),並遵循最佳實踐(功能拆分、命名 Chunk、監控載入時間),即可在專案中安全、有效地運用動態模組載入。
透過本文的概念說明與實作範例,你現在應該已經掌握了 何時、如何以及為什麼 使用 import(),並能在自己的 JavaScript 專案中即刻實踐,讓應用程式更快、更省資源,也更具擴充彈性。祝開發順利!