在瞭解模組化、元件化之前,最好先了解一下什麼是高內聚,低耦合。它能更好地幫助你理解模組化、元件化。
#高內聚,低耦合高內聚,低耦合是軟體工程中的概念,它是判斷程式碼好壞的一個重要指標。高內聚,就是指一個函式儘量只做一件事。低耦合,就是兩個模組之間的關聯程度低。
僅看文字可能不太好理解,下面來看一個簡單的示例。
// math.jsexport function add(a, b) { return a + b}export function mul(a, b) { return a * b}
// test.jsimport { add, mul } from 'math'add(1, 2)mul(1, 2)mul(add(1, 2), add(1, 2))
上面的 math.js 就是高內聚,低耦合的典型示例。add()、mul() 一個函式只做一件事,它們之間也沒有直接聯絡。如果要將這兩個函式聯絡在一起,也只能透過傳參和返回值來實現。
既然有好的示例,那就有壞的示例,下面再看一個不好的示例。
// 母公司class Parent { getProfit(...subs) { let profit = 0 subs.forEach(sub => { profit += sub.revenue - sub.cost }) return profit }}// 子公司class Sub { constructor(revenue, cost) { this.revenue = revenue this.cost = cost }}const p = new Parent()const s1 = new Sub(100, 10)const s2 = new Sub(200, 150)console.log(p.getProfit(s1, s2)) // 140
上面的程式碼是一個不太好的示例,因為母公司在計算利潤時,直接操作了子公司的資料。更好的做法是,子公司直接將利潤返回給母公司,然後母公司做一個彙總。
class Parent { getProfit(...subs) { let profit = 0 subs.forEach(sub => { profit += sub.getProfit() }) return profit }}class Sub { constructor(revenue, cost) { this.revenue = revenue this.cost = cost } getProfit() { return this.revenue - this.cost }}const p = new Parent()const s1 = new Sub(100, 10)const s2 = new Sub(200, 150)console.log(p.getProfit(s1, s2)) // 140
這樣改就好多了,子公司增加了一個 getProfit() 方法,母公司在做彙總時直接呼叫這個方法。
#高內聚,低耦合在業務場景中的運用理想很美好,現實很殘酷。剛才的示例是高內聚、低耦合比較經典的例子。但在業務場景中寫程式碼不可能做到這麼完美,很多時候會出現一個函式要處理多個邏輯的情況。
function register(data) { // 1. 驗證使用者資料是否合法 /** * 驗證賬號 * 驗證密碼 * 驗證簡訊驗證碼 * 驗證身份證 * 驗證郵箱 */ // 省略一大堆串 if 判斷語句... // 2. 如果使用者上傳了頭像,則將使用者頭像轉成 base64 碼儲存 /** * 新建 FileReader 物件 * 將圖片轉換成 base64 碼 */ // 省略轉換程式碼... // 3. 呼叫註冊介面 // 省略註冊程式碼...}
這個示例屬於很常見的需求,點選一個按鈕處理多個邏輯。從程式碼中也可以發現,這樣寫的結果就是三個功能耦合在一起。
按照高內聚、低耦合的要求,一個函式應該儘量只做一件事。所以我們可以將函式中的另外兩個功能:驗證和轉換單獨提取出來,封裝成一個函式。
function register(data) { // 1. 驗證使用者資料是否合法 verifyUserData() // 2. 如果使用者上傳了頭像,則將使用者頭像轉成 base64 碼儲存 toBase64() // 3. 呼叫註冊介面 // 省略註冊程式碼...}function verifyUserData() { /** * 驗證賬號 * 驗證密碼 * 驗證簡訊驗證碼 * 驗證身份證 * 驗證郵箱 */ // 省略一大堆串 if 判斷語句...}function toBase64() { /** * 新建 FileReader 物件 * 將圖片轉換成 base64 碼 */ // 省略轉換程式碼...}
這樣修改以後,就比較符合高內聚、低耦合的要求了。以後即使要修改或移除、新增功能,也非常方便。
#模組化、元件化#模組化模組化,就是把一個個檔案看成一個模組,它們之間作用域相互隔離,互不干擾。一個模組就是一個功能,它們可以被多次複用。另外,模組化的設計也體現了分治的思想。什麼是分治?維基百科 (opens new window)的定義如下:
字面上的解釋是“分而治之”,就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。
從前端方面來看,單獨的 JavaScript 檔案、CSS 檔案都算是一個模組。
例如一個 math.js 檔案,它就是一個數學模組,包含了和數學運算相關的函式:
// math.jsexport function add(a, b) { return a + b}export function mul(a, b) { return a * b}export function abs() { ... }...
一個 button.css 檔案,包含了按鈕相關的樣式:
/* 按鈕樣式 */button { ...}
#元件化
那什麼是元件化呢?我們可以認為元件就是頁面裡的 UI 元件,一個頁面可以由很多元件構成。例如一個後臺管理系統頁面,可能包含了 Header、Sidebar、Main 等各種元件。
一個元件又包含了 template(html)、script、style 三部分,其中 script、style 可以由一個或多個模組組成。
從上圖可以看到,一個頁面可以分解成一個個元件,每個元件又可以分解成一個個模組,充分體現了分治的思想(如果忘了分治的定義,請回頭再看一遍)。
由此可見,頁面成為了一個容器,元件是這個容器的基本元素。元件與元件之間可以自由切換、多次複用,修改頁面只需修改對應的元件即可,大大地提升了開發效率。
最理想的情況就是一個頁面元素全部由元件構成,這樣前端只需要寫一些互動邏輯程式碼。雖然這種情況很難完全實現,但我們要儘量往這個方向上去做,爭取實現全面元件化。
#Web Components得益於技術的發展,目前三大框架在構建工具(例如 webpack、vite...)的配合下都可以很好的實現元件化。例如 Vue,使用 *.vue 檔案就可以把 template、script、style 寫在一起,一個 *.vue 檔案就是一個元件。
<template> <div> {{ msg }} </div></template><script>export default { data() { return { msg: 'Hello World!' } }}</script><style>body { font-size: 14px;}</style>
如果不使用框架和構建工具,還能實現元件化嗎?
答案是可以的,元件化是前端未來的發展方向,Web Components (opens new window)就是瀏覽器原生支援的元件化標準。使用 Web Components API,瀏覽器可以在不引入第三方程式碼的情況下實現元件化。
#Custom elements(自定義元素)瀏覽器提供了一個 customElements.define() 方法,允許我們定義一個自定義元素和它的行為,然後在頁面中使用。
class CustomButton extends HTMLElement { constructor() { // 必須首先呼叫 super方法 super() // 元素的功能程式碼寫在這裡 const templateContent = document.getElementById('custom-button').content const shadowRoot = this.attachShadow({ mode: 'open' }) shadowRoot.appendChild(templateContent.cloneNode(true)) shadowRoot.querySelector('button').onclick = () => { alert('Hello World!') } } connectedCallback() { console.log('connected') }}customElements.define('custom-button', CustomButton)
上面的程式碼使用 customElements.define() 方法註冊了一個新的元素,並向其傳遞了元素的名稱 custom-button、指定元素功能的類 CustomButton。然後我們可以在頁面中這樣使用:
<custom-button></custom-button>
這個自定義元素繼承自 HTMLElement(HTMLElement 介面表示所有的 HTML 元素),表明這個自定義元素具有 HTML 元素的特性。
#使用 <template> 設定自定義元素內容<template id="custom-button"> <button>自定義按鈕</button> <style> button { display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; text-align: center; box-sizing: border-box; outline: none; margin: 0; transition: .1s; font-weight: 500; padding: 12px 20px; font-size: 14px; border-radius: 4px; color: #fff; background-color: #409eff; border-color: #409eff; border: 0; } button:active { background: #3a8ee6; border-color: #3a8ee6; color: #fff; } </style></template>
從上面的程式碼可以發現,我們為這個自定義元素設定了內容 <button>自定義按鈕</button> 以及樣式,樣式放在 <style> 標籤裡。可以說 <template> 其實就是一個 HTML 模板。
#Shadow DOM(影子DOM)設定了自定義元素的名稱、內容以及樣式,現在就差最後一步了:將內容、樣式掛載到自定義元素上。
// 元素的功能程式碼寫在這裡const templateContent = document.getElementById('custom-button').contentconst shadowRoot = this.attachShadow({ mode: 'open' })shadowRoot.appendChild(templateContent.cloneNode(true))shadowRoot.querySelector('button').onclick = () => { alert('Hello World!')}
元素的功能程式碼中有一個 attachShadow() 方法,它的作用是將影子 DOM 掛到自定義元素上。DOM 我們知道是什麼意思,就是指頁面元素。那“影子”是什麼意思呢?“影子”的意思就是附加到自定義元素上的 DOM 功能是私有的,不會與頁面其他元素髮生衝突。
attachShadow() 方法還有一個引數 mode,它有兩個值:
open 代表可以從外部訪問影子 DOM。closed 代表不可以從外部訪問影子 DOM。// open,返回 shadowRootdocument.querySelector('custom-button').shadowRoot// closed,返回 nulldocument.querySelector('custom-button').shadowRoot
#生命週期
自定義元素有四個生命週期:
connectedCallback: 當自定義元素第一次被連線到文件 DOM 時被呼叫。disconnectedCallback: 當自定義元素與文件 DOM 斷開連線時被呼叫。adoptedCallback: 當自定義元素被移動到新文件時被呼叫。attributeChangedCallback: 當自定義元素的一個屬性被增加、移除或更改時被呼叫。生命週期在觸發時會自動呼叫對應的回撥函式,例如本次示例中就設定了 connectedCallback() 鉤子。
最後附上完整程式碼:
<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>Web Components</title></head><body> <custom-button></custom-button> <template id="custom-button"> <button>自定義按鈕</button> <style> button { display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; text-align: center; box-sizing: border-box; outline: none; margin: 0; transition: .1s; font-weight: 500; padding: 12px 20px; font-size: 14px; border-radius: 4px; color: #fff; background-color: #409eff; border-color: #409eff; border: 0; } button:active { background: #3a8ee6; border-color: #3a8ee6; color: #fff; } </style> </template> <script> class CustomButton extends HTMLElement { constructor() { // 必須首先呼叫 super方法 super() // 元素的功能程式碼寫在這裡 const templateContent = document.getElementById('custom-button').content const shadowRoot = this.attachShadow({ mode: 'open' }) shadowRoot.appendChild(templateContent.cloneNode(true)) shadowRoot.querySelector('button').onclick = () => { alert('Hello World!') } } connectedCallback() { console.log('connected') } } customElements.define('custom-button', CustomButton) </script></body></html>
#小結用過 Vue 的同學可能會發現,Web Components 標準和 Vue 非常像。我估計 Vue 在設計時有參考過 Web Components(個人猜想,未考證)。