首頁>技術>

在瞭解模組化、元件化之前,最好先了解一下什麼是高內聚,低耦合。它能更好地幫助你理解模組化、元件化。

#高內聚,低耦合

高內聚,低耦合是軟體工程中的概念,它是判斷程式碼好壞的一個重要指標。高內聚,就是指一個函式儘量只做一件事。低耦合,就是兩個模組之間的關聯程度低。

僅看文字可能不太好理解,下面來看一個簡單的示例。

// 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(個人猜想,未考證)。

10
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • yum源配置方法跟linux常用命令