iT邦幫忙

2024 iThome 鐵人賽

DAY 27
1
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 27

Day 27: JavaScript 模組(module) 和 Vue的程式碼分割 (code spliting)

  • 分享至 

  • xImage
  •  

終於來到鐵人賽的倒數第4天,今天要介紹 JavaScript 滿常見的 動態載入(Dynamic import),是基於JavaScript 模組(module) 這個觀念衍伸而來,開發上其實常常用到,一起來做個小回顧吧~~

今日學習目標

  1. 理解認識JavaScript 模組(module) 的觀念
  2. 模組的特性介紹回顧
  3. JavaScript 動態載入(Dynamic import)-import()
  4. Vue -程式碼如何進行打包分割(code spliting)

JavaScript 模組

JavaScript 起初設計時並沒有內建命名空間的概念,也缺乏組織與程式碼分離的原生支援。隨著應用程式規模不斷擴大,程式碼中的變數與函式名稱容易發生衝突,所以一些JavaScript 不同的執行環境有發展出類似模組的觀念,來達到程式碼隔離

早期JavaScript沒有內建的模組化支持,會利用 立即函式(IIFE) 作為一種隔離程式碼解決方案,在程式中隔離變數和函式。

IIFE(Immediately Invoked Function Expressions)

立即函式其實滿明顯是用 JavaScript 閉包(closure) 來控制私有變數和狀態,創建一個函式並且馬上執行建立局部的作用域,避免變數間遭到全局變數污染。

缺點:

  • 程式碼不易維護:當程式碼變得更加複雜時,IIFE 的程式碼容易變得龐大,不易於維護和閱讀。
  • 不利於重複使用:IIFE 的程式碼通常是一次性的,如果大家要共享一份資料,多次調用比較不方便。

CommonJS

CommonJS 是一種模組規範,主要用於伺服器端的 JavaScript 執行環境(Node.js),CommonJS 使用 requiremodule.exports 關鍵字來進行模組的導入與導出。

隨著 Node.js的流行 CommonJS 成為了伺服器端 JavaScript 的主流模組化方案。 但是瀏覽器端 JavaScript 還需要依賴工具(Browserify)來轉換 CommonJS 模組,讓瀏覽器可以運行使用。

ES6 Modules (ESM)

ES6 Modules (簡稱ESM) 是 JavaScript 的標準化模組系統(ES6),是透過 importexport 關鍵字來管理模組。ESM 本身有是非同步加載的特性,更適合瀏覽器環境。


(圖片出處)


ES6 Module 使用特性

ES6模組系統使用上相當簡單,大致上只有比較重要的三個重點回顧一下:

  • ES6的模組程式碼會自動變成strict-mode(嚴格模式)
    檔案中變數需要確實宣告
<script type="module">
  a = 5; // error
</script>
  • ES6的模組是一個檔案(file)一個模組,並使用export(輸出)與import(輸入)

可以在不同檔案中形成獨立模組,模組間彼此是隔離的

// 📁 sayHi.js
export function sayHi(user) {
  alert(`Hello, ${user}!`);
}

// 📁 main.js
import {sayHi} from './sayHi.js';

alert(sayHi); // function...
sayHi('John'); // Hello, John!
  • ES6模組(module)還有另一個重要的單例(singleton)特性

在 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引用資料已經指向新的資料,滿重要的單例觀念,因為如果大家不指向單一數據資料來源,程式可靠性就會降低。


模組在瀏覽器上的解析特性

  • 並行加載(defer/async)
    模組和其他資源(例如 HTML、CSS)會同時間加載,不會阻塞 HTML 在瀏覽器上的解析行為。
  • 延遲執行
    模組會等待整個 HTML 文件完全解析完畢後才執行,即使它們比 HTML加載得更快。
  • 順序執行
    模組按它們在文檔中的順序執行,較早出現的模組腳本會優先執行(先import先執行)。
<script>
  document.querySelector; //這樣會捕抓不到dom元素,因為HTML還未解析完畢,可以補上type="module"
</script>

<button id="button">Button</button>

如果module本身沒有參與網頁互動邏輯,可以考慮async

像是GA點集資料的收集,本身和網頁上的元素互動比較無關,可以自行載入後開始執行,而模組預設的 defer 是延遲到DOM解析完畢才執行。

<script async type="module">
  import {counter} from './analytics.js';

  counter.count();
</script>

JavaScript 動態載入(Dynamic import)-import()

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>

Vue 的程式碼分割(code spliting)

有了ES2020動態載入的特性,就可以做的將不同JS代碼進行分割,而在Vue的使用當中分割的層級,主要可以分為兩種:

  • 以路由(router)為單位分割
  • 以元件(component)為單位

以路由 (Router) 為單位進行分割

在使用 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 頁面
  }
];

以元件為單位分割 - defineAsyncComponent

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>


chunk分割檔案客製化

上面是一般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 開發者先後使用了 IIFECommonJSES6 模組(ESM)等不同的模組化方案。直到近期 ES6 模組成為現代 JavaScript 的標準,支持靜態分析、非同步加載等,並且在瀏覽器環境中廣泛使用。

而在使用 Vite 開發中,我們可以利用 動態載入手動分割 chunk,將程式碼依照使用邏輯情境集中到特定的 chunk 檔案中,進一步提升優化載入效率。

  • 模組化的優點:
  1. 增加檔案間的可讀性和維護性。
  2. 避免命名衝突避免全域變數污染。
  3. 支持延遲載入(lazy loading),提升應用的初次加載速度和性能。
  4. 提供單例行為(singleton),確保模組的資料狀態在整個程式應用上的一致性。

學習資源

  1. https://pjchender.dev/javascript/js-es-module/
  2. https://javascript.info/modules-intro#a-module-code-is-evaluated-only-the-first-time-when-imported
  3. https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/module_system.html
  4. https://www.explainthis.io/zh-hant/swe/what-is-frontend-module
  5. https://vuejs.org/guide/components/async.html
  6. https://www.youtube.com/watch?v=KCrXgy8qtjM (Vite 100秒介紹)
  7. https://www.youtube.com/watch?v=93R57sLATM4 (ES2020-動態載入)

上一篇
Day 26: JavaScript 的錯誤處理和 Vue元件錯誤捕捉 - onErrorCaptured
下一篇
Day 28: Vue 的元件更新優化-重新渲染問題 (Re-render)
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言