在了解模塊化、組件化之前,最好先了解一下什么是高內(nèi)聚,低耦合。它能更好地幫助你理解模塊化、組件化。
高內(nèi)聚,低耦合
高內(nèi)聚,低耦合是軟件工程中的概念,它是判斷代碼好壞的一個重要指標。高內(nèi)聚,就是指一個函數(shù)盡量只做一件事。低耦合,就是兩個模塊之間的關聯(lián)程度低。
僅看文字可能不太好理解,下面來看一個簡單的示例。
// math.js
export function add(a, b) {
return a + b
}
export function mul(a, b) {
return a * b
}
// test.js
import { add, mul } from 'math'
add(1, 2)
mul(1, 2)
mul(add(1, 2), add(1, 2))
上面的 math.js 就是高內(nèi)聚,低耦合的典型示例。add()、mul() 一個函數(shù)只做一件事,它們之間也沒有直接聯(lián)系。如果要將這兩個函數(shù)聯(lián)系在一起,也只能通過傳參和返回值來實現(xiàn)。
既然有好的示例,那就有壞的示例,下面再看一個不好的示例。
// 母公司
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
上面的代碼是一個不太好的示例,因為母公司在計算利潤時,直接操作了子公司的數(shù)據(jù)。更好的做法是,子公司直接將利潤返回給母公司,然后母公司做一個匯總。
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
這樣改就好多了,子公司增加了一個 () 方法,母公司在做匯總時直接調(diào)用這個方法。
高內(nèi)聚,低耦合在業(yè)務場景中的運用
理想很美好,現(xiàn)實很殘酷。剛才的示例是高內(nèi)聚、低耦合比較經(jīng)典的例子。但在業(yè)務場景中寫代碼不可能做到這么完美,很多時候會出現(xiàn)一個函數(shù)要處理多個邏輯的情況。
舉個例子,用戶注冊。一般注冊會在按鈕上綁定一個點擊事件回調(diào)函數(shù) (),用于處理注冊邏輯。
function register(data) {
// 1. 驗證用戶數(shù)據(jù)是否合法
/**
* 驗證賬號
* 驗證密碼
* 驗證短信驗證碼
* 驗證身份證
* 驗證郵箱
*/
// 省略一大堆串 if 判斷語句...

// 2. 如果用戶上傳了頭像,則將用戶頭像轉(zhuǎn)成 base64 碼保存
/**
* 新建 FileReader 對象
* 將圖片轉(zhuǎn)換成 base64 碼
*/
// 省略轉(zhuǎn)換代碼...
// 3. 調(diào)用注冊接口
// 省略注冊代碼...
}
這個示例屬于很常見的需求,點擊一個按鈕處理多個邏輯。從代碼中也可以發(fā)現(xiàn),這樣寫的結(jié)果就是三個功能耦合在一起。
按照高內(nèi)聚、低耦合的要求,一個函數(shù)應該盡量只做一件事。所以我們可以將函數(shù)中的另外兩個功能:驗證和轉(zhuǎn)換單獨提取出來,封裝成一個函數(shù)。
function register(data) {
// 1. 驗證用戶數(shù)據(jù)是否合法
verifyUserData()
// 2. 如果用戶上傳了頭像,則將用戶頭像轉(zhuǎn)成 base64 碼保存
toBase64()
// 3. 調(diào)用注冊接口
// 省略注冊代碼...
}
function verifyUserData() {
/**
* 驗證賬號
* 驗證密碼
* 驗證短信驗證碼
* 驗證身份證
* 驗證郵箱
*/
// 省略一大堆串 if 判斷語句...
}
function toBase64() {
/**
* 新建 FileReader 對象
* 將圖片轉(zhuǎn)換成 base64 碼
*/
// 省略轉(zhuǎn)換代碼...
}
這樣修改以后,就比較符合高內(nèi)聚、低耦合的要求了。以后即使要修改或移除、新增功能,也非常方便。
模塊化、組件化模塊化
模塊化,就是把一個個文件看成一個模塊,它們之間作用域相互隔離,互不干擾。一個模塊就是一個功能,它們可以被多次復用。另外,模塊化的設計也體現(xiàn)了分治的思想。什么是分治?維基百科的定義如下:
字面上的解釋是“分而治之”,就是把一個復雜的問題分成兩個或更多的相同或相似的子問題,直到最后子問題可以簡單的直接求解,原問題的解即子問題的解的合并。
從前端方面來看,單獨的 文件、CSS 文件都算是一個模塊。
例如一個 math.js 文件,它就是一個數(shù)學模塊,包含了和數(shù)學運算相關的函數(shù):
// math.js
export function add(a, b) {
return a + b
}
export function mul(a, b) {
return a * b
}
export function abs() { ... }
...
一個 button.css 文件,包含了按鈕相關的樣式:
/* 按鈕樣式 */
button {
...
}
組件化
那什么是組件化呢?我們可以認為組件就是頁面里的 UI 組件,一個頁面可以由很多組件構成。例如一個后臺管理系統(tǒng)頁面,可能包含了 Header、Sidebar、Main 等各種組件。
一個組件又包含了 (html)、script、style 三部分,其中 script、style 可以由一個或多個模塊組成。
從上圖可以看到,一個頁面可以分解成一個個組件,每個組件又可以分解成一個個模塊,充分體現(xiàn)了分治的思想(如果忘了分治的定義,請回頭再看一遍)。
由此可見,頁面成為了一個容器,組件是這個容器的基本元素。組件與組件之間可以自由切換、多次復用,修改頁面只需修改對應的組件即可,大大的提升了開發(fā)效率。
最理想的情況就是一個頁面元素全部由組件構成,這樣前端只需要寫一些交互邏輯代碼。雖然這種情況很難完全實現(xiàn),但我們要盡量往這個方向上去做,爭取實現(xiàn)全面組件化。
Web
得益于技術的發(fā)展,目前三大框架在構建工具(例如 webpack、vite...)的配合下都可以很好的實現(xiàn)組件化。例如 Vue,使用 *.vue 文件就可以把 、script、style 寫在一起,一個 *.vue 文件就是一個組件。
{{ msg }}
<script>
export default {
data() {
return {
msg: 'Hello World!'
}
}
}
</script>
如果不使用框架和構建工具,還能實現(xiàn)組件化嗎?
答案是可以的,組件化是前端未來的發(fā)展方向,Web 就是瀏覽器原生支持的組件化標準。使用 Web API,瀏覽器可以在不引入第三方代碼的情況下實現(xiàn)組件化。
實戰(zhàn)
現(xiàn)在我們來創(chuàng)建一個 Web 按鈕組件,點擊它將會彈出一個消息 Hello World!。點擊這可以看到 DEMO 效果。
Custom (自定義元素)
瀏覽器提供了一個 .define() 方法,允許我們定義一個自定義元素和它的行為,然后在頁面中使用。
class CustomButton extends HTMLElement {
constructor() {
// 必須首先調(diào)用 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)
上面的代碼使用 .define() 方法注冊了一個新的元素,并向其傳遞了元素的名稱 custom-button、指定元素功能的類 。然后我們可以在頁面中這樣使用:
這個自定義元素繼承自 ( 接口表示所有的 HTML 元素),表明這個自定義元素具有 HTML 元素的特性。
使用 設置自定義元素內(nèi)容
從上面的代碼可以發(fā)現(xiàn),我們?yōu)檫@個自定義元素設置了內(nèi)容 自定義按鈕 以及樣式,樣式放在 標簽里。可以說 其實就是一個 HTML 模板。
Shadow DOM(影子DOM)
設置了自定義元素的名稱、內(nèi)容以及樣式,現(xiàn)在就差最后一步了:將內(nèi)容、樣式掛載到自定義元素上。
// 元素的功能代碼寫在這里
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!')
}
元素的功能代碼中有一個 () 方法,它的作用是將影子 DOM 掛到自定義元素上。DOM 我們知道是什么意思,就是指頁面元素。那“影子”是什么意思呢?“影子”的意思就是附加到自定義元素上的 DOM 功能是私有的,不會與頁面其他元素發(fā)生沖突。
() 方法還有一個參數(shù) mode,它有兩個值:
open 代表可以從外部訪問影子 DOM。closed 代表不可以從外部訪問影子 DOM。
// open,返回 shadowRoot
document.querySelector('custom-button').shadowRoot
// closed,返回 null
document.querySelector('custom-button').shadowRoot
生命周期
自定義元素有四個生命周期:
: 當自定義元素第一次被連接到文檔 DOM 時被調(diào)用。: 當自定義元素與文檔 DOM 斷開連接時被調(diào)用。: 當自定義元素被移動到新文檔時被調(diào)用。back: 當自定義元素的一個屬性被增加、移除或更改時被調(diào)用。
生命周期在觸發(fā)時會自動調(diào)用對應的回調(diào)函數(shù),例如本次示例中就設置了 () 鉤子。
最后附上完整代碼:
Web Components
<script>
class CustomButton extends HTMLElement {
constructor() {
// 必須首先調(diào)用 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>
小結(jié)
用過 Vue 的同學可能會發(fā)現(xiàn),Web 標準和 Vue 非常像。我估計 Vue 在設計時有參考過 Web (個人猜想,未考證)。
如果你想了解更多 Web 的信息,請參考 MDN 文檔。
參考資料
《帶你入門前端工程》全文目錄:
技術選型:如何進行技術選型?統(tǒng)一規(guī)范:如何制訂規(guī)范并利用工具保證規(guī)范被嚴格執(zhí)行?前端組件化:什么是模塊化、組件化?測試:如何寫單元測試和 E2E(端到端) 測試?構建工具:構建工具有哪些?都有哪些功能和優(yōu)勢?自動化部署:如何利用 Jenkins、Github Actions 自動化部署項目?前端監(jiān)控:講解前端監(jiān)控原理及如何利用 sentry 對項目實行監(jiān)控。性能優(yōu)化(一):如何檢測網(wǎng)站性能?有哪些實用的性能優(yōu)化規(guī)則?性能優(yōu)化(二):如何檢測網(wǎng)站性能?有哪些實用的性能優(yōu)化規(guī)則?重構:為什么做重構?重構有哪些手法?微服務:微服務是什么?如何搭建微服務項目?: 是什么?如何使用 ?