終於來到鐵人賽的倒數第4天,今天要介紹 JavaScript 滿常見的 動態載入(Dynamic import)
,是基於JavaScript 模組(module)
這個觀念衍伸而來,開發上其實常常用到,一起來做個小回顧吧~~
模組(module) 的觀念
動態載入(Dynamic import)-import()
Vue -程式碼如何進行打包分割(code spliting)
JavaScript 起初設計時並沒有內建命名空間的概念,也缺乏組織與程式碼分離的原生支援。隨著應用程式規模不斷擴大,程式碼中的變數與函式名稱容易發生衝突
,所以一些JavaScript 不同的執行環境有發展出類似模組的觀念,來達到程式碼隔離
。
早期JavaScript沒有內建的模組化支持,會利用 立即函式(IIFE)
作為一種隔離程式碼解決方案,在程式中隔離變數和函式。
立即函式其實滿明顯是用 JavaScript 閉包(closure)
來控制私有變數和狀態,創建一個函式並且馬上執行建立局部的作用域
,避免變數間遭到全局變數污染。
缺點:
CommonJS 是一種模組規範,主要用於伺服器端的 JavaScript 執行環境(Node.js)
,CommonJS 使用 require
和 module.exports
關鍵字來進行模組的導入與導出。
隨著 Node.js
的流行 CommonJS 成為了伺服器端 JavaScript 的主流模組化方案。 但是瀏覽器端 JavaScript 還需要依賴工具(Browserify)來轉換 CommonJS 模組,讓瀏覽器可以運行使用。
ES6 Modules (簡稱ESM) 是 JavaScript 的標準化模組系統(ES6)
,是透過 import
和 export
關鍵字來管理模組。ESM 本身有是非同步加載的特性,更適合瀏覽器環境。
(圖片出處)
ES6模組系統使用上相當簡單,大致上只有比較重要的三個重點回顧一下:
<script type="module">
a = 5; // error
</script>
可以在不同檔案中形成獨立模組,模組間彼此是隔離的
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
// 📁 main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
在 ES6 模組系統中,每個模組只會被加載和執行一次
。在應用中多次導入相同模組時,會取得相同的模組實例(單一來源狀態)。因此模組內的狀態(變數或物件)會被共享
。
這跟使用狀態管理工具來管理應用程式的全局資料狀態(Pinia)
,可以確保應用內的多個元件在訪問該模組時共享相同的狀態的原因。
// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
// 2.js 本身模組的變數也會被改變
這種模組特性稱為模組的單例性(module singleton behavior)
,滿適合用於應用程式當中配置(configuration)設定,像是 Vue 的app.config
設定
// 📁 admin.js 初始化設定檔
export let config = { };
export function sayHi() {
alert(`Ready to serve, ${config.user}!`);
}
然後我們呼叫admin設定檔進行一些參數配置
// 📁 init.js
import {config} from './admin.js';
config.user = "Pete";
當應用程式再次呼叫sayHi()時,裡面config引用資料已經指向新的資料,滿重要的單例觀念,因為如果大家不指向單一數據資料來源,程式可靠性就會降低。
<script>
document.querySelector; //這樣會捕抓不到dom元素,因為HTML還未解析完畢,可以補上type="module"
</script>
<button id="button">Button</button>
像是GA點集資料的收集,本身和網頁上的元素互動比較無關,可以自行載入後開始執行,而模組預設的 defer 是延遲到DOM解析完畢才執行。
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
import()是在 ES2020(也稱為 ES11)中引入的。這個動態 import 函式允許在程式碼執行過程中以非同步方式載入模組,並回傳一個 Promise
,和傳統的靜態 import 語法不同,靜態 import 語法在 ES6(ES2015)年中就已經被引入。
動態載入(Dynamic Import)
在程式運行時根據需求非同步載入模組,而不是在程式啟動時預先載入所有模組。靜態(import)
則是在需要在程式碼的最上方引入所有依賴模組,並需要在執行任何其他程式碼之前載入這些模組。
import()函式本身會回傳一個 Promise
,可以用 then()
或 async/await語法
進行後續動作
import * as mod from "/my-module.js";
import("/my-module.js").then((mod2) => {
console.log(mod === mod2); // true
});
在原生JavaScript 我們可以透過事件監聽器,點擊後再掛載某些模組
<script>
const button = document.querySelector('.button')
button.addEventListener('click', async function(){
const module = await import("./modules.js")
module.default()
})
</script>
有了ES2020動態載入的特性,就可以做的將不同JS代碼進行分割,而在Vue的使用當中分割的層級,主要可以分為兩種:
在使用 Vue Router 時。可以為每個路由設置懶加載(lazy loading)
,這樣當用戶進入特定路由時才會加載該頁面所需的程式碼,減少應用程式初次載入的大小,提升載入速度並改善用戶體驗:
當用戶訪問 /about 路由
時,About.vue 的頁面元件代碼才會被載入。。
const routes = [
{
path: '/home',
component: () => import('./views/Home.vue'), // 動態載入 Home 頁面
},
{
path: '/about',
component: () => import('./views/About.vue'), // 動態載入 About 頁面
}
];
Vue 支援按元件進行代碼分割,可以利用 defineAsyncComponent
,這種方式非常適合用於初始畫面中不會立刻出現的部分,例如 彈窗 (Modal)
或 大型表單 (Form)
。透過延遲載入這些元件,僅在真正需要它們時才進行加載,可以顯著提升網站的首次內容加載速度(First Contentful Paint, FCP)
,減少用戶等待時間,並改善整體體驗。
<template>
<button id="show-modal" @click="showModal = true">Show Modal</button>
<Modal v-if="showModal" :show="showModal" @close="showModal = false" />
</template>
<script setup>
import { ref, defineAsyncComponent } from "vue";
const Modal = defineAsyncComponent(()=>import('./Modal.vue'))
const showModal = ref(false);
</script>
上面是一般Vite打包時的程式碼分割策略,但有時候有些使用行為很集中的套件或是元件,我們想要自己定義分配到同一支分割檔(chunk.js),在 Vite當中可以客製化程式碼打包,透過 rollup 打包設定參數 manualChunks
來設置手動分割程式碼並集中到同一支模組中:
分割設定名稱可以自己定義(ex: group-user),設定上可以根據模組功能組合,打包出來名稱會在後面加上一串hash code。
該怎麼手動設定分類:
按業務功能劃分: 將功能相似或互相依賴的模組放在同一個 chunk 中。例如,所有與用戶相關的頁面或元件可以放在一個 user chunk 中。
第三方庫分割: 常用的第三方庫(如 lodash、axios)可以分別設置為獨立的 chunk,這樣可以在應用程式中多次重用,並確保用戶瀏覽不同頁面時不重複下載相同的代碼。
大型模組分割: 如大型圖表庫、地圖 API 等,只在需要時才載入這些大型模組,減少初次載入時間。
下次可以開啟自己的專案來檢查那些套件包過於肥大,是否有必要第一次畫面進入就引入,可以設定看看~!
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'group-user': [
'./src/UserDetails',
'./src/UserDashboard',
'./src/UserProfileEdit',
],
},
},
},
},
})
JavaScript 開發者先後使用了 IIFE
、CommonJS
和 ES6 模組(ESM)
等不同的模組化方案。直到近期 ES6 模組成為現代 JavaScript 的標準,支持靜態分析、非同步加載等,並且在瀏覽器環境中廣泛使用。
而在使用 Vite 開發中,我們可以利用 動態載入
和 手動分割 chunk
,將程式碼依照使用邏輯情境集中到特定的 chunk 檔案中,進一步提升優化載入效率。