今天要來介紹 Module 模式,Module 模式是 GoF 提出的模式之一,我會以 JavaScript 為主要程式語言來說明以及舉例,並盡量以情境(context)、問題(problem)、權衡(force)、解決方案(solution)等描述模式的要素來說明~
隨著專案與程式碼的擴大,程式碼的組織性與可維護性越來越重要,需要一種方法來組織、封裝程式碼及功能,降低功能間的依賴性,並保持全域作用域的乾淨整潔,避免全域作用域被污染。
沒有封裝過的 JavaScript 程式碼很容易造成變數命名衝突,變數的值也很容易被修改,因為所有變數和函數都在全域作用域中,很難保持程式碼隱私性與結構性,也讓程式碼難以維護和預測。
如何封裝程式碼,將不必要的私有變數隱藏,並公開外部能存取的變數和函式?
(備註:其實問題不限於 JavaScript 程式碼,應該說所有程式語言都有類似這樣的問題,只是我們都以 JavaScript 來做說明和舉例)
解決方案需平衡上述權衡。例如,過度封裝可能會導致程式碼的公開接口不夠靈活,而過度最小化相互作用則可能導致過度抽象,而導致外部難以使用。
JavaScript 的模組(module)可解決此問題,ES2015(ES6)之後 JavaScript 有了內建的模組功能,訂定 import
跟 export
的模組語法,我們可用模組來組織物件、函式…等,以便輕鬆匯入和匯出,接下來稍微介紹一下如何使用模組。
若要使用模組,可分為兩種情境:
script
時指定要使用模組類型
<script>
中用 type 屬性來指定要使用模組nomodule
則是告訴瀏覽器不須將腳本作為模組載入,對不使用模組語法的 fallback 腳本很有用.js
<script type='module' src='main.mjs'></script>
<script nomodule src='fallback.js'></script>
如果有使用框架來開發前端應用(例如:React),通常都會使用 transpiler (如:Babel)來轉譯模組語法,因此我們可以在應用程式中直接使用 import
和 export
的語法。package.json
加上 { "type": "module" }
.mjs
補充一點,.mjs
是用於 JavaScript 模組的副檔名,用來區分模組和一般腳本(.js
),.mjs
這個副檔名可讓 runtime 和建構工具(如:Node.js、Babel)將其解析為模組。
如果希望某些變數或函式可以讓外部其他地方使用,就需要使用匯出語法匯出模組,外部才能存取,而沒有使用匯出 export
語法的變數,外部是無法存取的,也因此可保有內部實現的隱私。
匯出方式可分為實名匯出(named export)和預設匯出(default export)。
// 私人變數,不使用 export
const privateNumber = 0;
// 直接在要匯出的變數前面加上 export
export const userName = 'Foo';
export const price = 100;
export function logUserName(userName) {
console.log('userName is ', userName);
}
// 私人變數,不使用 export
const privateNumber = 0;
// 先定義好變數,在檔案最後再 export 要匯出的變數
const userName = 'Foo';
const price = 100;
function logUserName(userName) {
console.log('userName is ', userName);
}
// 統一匯出,但這裡的 {} 並不是物件的意思
export { userName, price, logUserName}
// 私人變數,不使用 export
const privateNumber = 0;
function logUserName(userName) {
console.log('userName is ', userName);
}
// 用 export default 來表示這是預設匯出
export default logUserName
匯入方式會依匯出方式而定,因此也分為兩種。
import { userName, price, logUserName} from './utils';
as
來幫變數重新命名
import { userName as name} from './utils';
import showUserName from './utils'; // 這裡匯入的是我剛剛 export default 的 logUserName,我自己再命名為 showUserName
如果沒有使用 export
語法匯出變數,在其他檔案是無法 import
使用的,例如上面範例的 privateNumber
,如果要在其他地方 import
存取,會發生錯誤。
另外補充,匯入的模組都是存取同一個參考,因此若要修改匯入的變數,要謹慎~因為其他有匯入此變數的地方也都會被影響,因為模組是 singleton 的,關於 singleton 會在之後文章介紹。
接下來稍微補充匯入的類型。
除了匯入我們在自己專案內定義的模組,也可以匯入遠端模組,例如第三方定義的函式庫。
// 從外部位置載入模組
import { logUserName } from 'https://javascriptpattern.com/modules/utils.js';
logUserName('Foo');
前面提到的匯入方式都屬於靜態匯入(static import),在主要程式碼之前,會需要預先載入程式碼,這也會導致關鍵功能被延後執行,相對於靜態匯入,另一種是動態匯入(dynamic import)。
動態匯入是需要時再載入,import(url)
會回傳請求模組的 Promise,Promise 成功後再使用模組功能。
const btn = document.getElementById('btn');
btn.addEventListener('click', e => {
import('/modules/utils.js')
.then((module)=>{
// 請求成功後就可用模組的功能
const { logUserName } = module; // 假設 logUserName 是用實名匯出
logUserName('Foo');
})
})
// 或使用 async/await 匯入
let module = await import('/modules/utils.js')
互動匯入(import on interaction)和可見性匯入(import on visibility)都可利用動態匯入的功能來達成。互動匯入指的是在使用者和網頁特定功能互動時才匯入,例如上方例子,當使用者點擊按鈕時,才動態匯入對應函式庫;可見性匯入指的是在使用者往下捲動頁面時,當頁面可看見該元件時再動態匯入,可搭配 IntersectionObserver API 偵測元件可見時機,再動態載入模組。
以模組作為解決方案的優點如下:
export
匯出的資料以模組作為解決方案的缺點如下:
這部分不是描述模式的要素之一,如果要說的話應該屬於 known use(?之類的,是一些我想到這模式在目前應用程式如何被使用的小小補充~
模組在現在複雜的前端應用幾乎是不可或缺的,其實幾乎每天開發都會用到😆,以我自己是用 React 開發來說,當我撰寫一個客製化元件要讓外部其他程式碼可以存取時,我就會需要 export
我的元件,然後在需要的地方 import
該元件;又或是我要使用 React 官方提供的 hooks 時,我會需要 import { useState } from 'react'
,匯入後就可以使用這個 hook 功能,而 React 也可以透過模組的方式來選擇要公開哪些接口讓我們存取,以及要隱藏哪些狀態管理的實作細節,讓我們無法直接存取 React 內部的狀態資料等,因此模組幾乎是我們天天都在用的呀!只是以前沒有這麼清楚知道模組要解決的問題、以及帶來的優缺點等等,希望透過這次機會也讓大家更認識模組模式~