Vue 前端設計模式梳理

一、什麼是設計模式?

設計模式是一套被反覆使用、多數人知曉、經過分類編目的、代碼設計經驗的總結。它是爲了可重用代碼,讓代碼更容易的被他人理解並保證代碼的可靠性。

     設計模式實際上是 “拿來主義” 在軟件領域的貫徹實踐,它是一套現成的工具,拿來即用。下面來看一下設計模式的設計原則。

二、設計幾個原則

      單一職責原則、開放封閉原則、裏式替換原則、接口隔離原則 、依賴反轉原則 、最少知識原則。

下面我們一起來看看幾種在前端領域常見的設計模式:

單例模式、工廠模式、策略模式、代理模式、適配器模式、觀察者模式 / 發佈 - 訂閱模式

三、常見的設計模式及實際案例

單例模式

1. 什麼是單例模式?

單例模式 (Singleton Pattern)又稱爲單體模式,保證一個類只有一個實例,並提供一個訪問它的全局訪問點。也就是說,第二次使用同一個類創建新對象的時候,應該得到與第一次創建的對象完全相同的對象。

Vue 中的單例模式

(1)Element UI

Element UI 是使用 Vue 開發的一個前端 UI 框架。ElementUI 中的全屏 Loading 蒙層調用有兩種形式:

●指令形式:Vue.use(Loading.directive)

●服務形式:Vue.prototype.$loading = service

指令形式註冊的使用方式 :

<div :v-loading.fullscreen="true">...</div>;

服務形式註冊的使用方式 :

this.$loading({ fullscreen: true });

用服務方式使用全屏 Loading 是單例的,即在前一個全屏 Loading 關閉前再次調用全屏 Loading,並不會創建一個新的 Loading 實例,而是返回現有全屏 Loading 的實例。

下面是 ElementUI 實現全屏 Loading 的源碼:

import Vue from 'vue'
import loadingVue from './loading.vue'
const LoadingConstructor = Vue.extend(loadingVue)
let fullscreenLoading
const Loading = (options = {}) => {
    if (options.fullscreen && fullscreenLoading) {
        return fullscreenLoading
    }
    let instance = new LoadingConstructor({
        el: document.createElement('div'),
        data: options
    })
    if (options.fullscreen) {
        fullscreenLoading = instance
    }
    return instance
}
export default Loading

這裏的單例是 fullscreenLoading,是存放在閉包中的,如果用戶傳的 options 的 fullscreen 爲 true 且已經創建了單例,則直接返回之前創建的單例,如果之前沒有創建過,則創建單例並賦值給閉包中的 fullscreenLoading 後返回新創建的單例實例。

(2)Vuex

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。Vuex,它們都實現了一個全局的 Store 用於存儲應用的所有狀態。這個 Store 的實現,正是單例模式的典型應用。

Vuex 使用單一狀態樹,用一個對象就包含了全部的應用層級狀態。至此它便作爲一個 “唯一數據源 (SSOT)” 而存在。這也意味着,每個應用將僅僅包含一個 store 實例。單一狀態樹讓我們能夠直接地定位任一特定的狀態片段,在調試的過程中也能輕易地取得整個當前應用狀態的快照。——Vuex 官方文檔

// 安裝vuex插件
Vue.use(Vuex)
// 將store注入到Vue實例中
new Vue({
    el: '#app',
    store
})

通過調用 Vue.use() 方法,安裝了 Vuex 插件。Vuex 插件是一個對象,它在內部實現了一個 install 方法,這個方法會在插件安裝時被調用,從而把 Store 注入到 Vue 實例裏去。也就是說每 install 一次,都會嘗試給 Vue 實例注入一個 Store。

let Vue // 這個Vue的作用和樓上的instance作用一樣
...
export function install (_Vue) {
  // 判斷傳入的Vue實例對象是否已經被install過Vuex插件(是否有了唯一的state)
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 若沒有,則爲這個Vue實例對象install一個唯一的Vuex
  Vue = _Vue
  // 將Vuex的初始化邏輯寫進Vue的鉤子函數里
  applyMixin(Vue)
}

     可以保證一個 Vue 實例(即一個 Vue 應用)只會被 install 一次 Vuex 插件,所以每個 Vue 實例只會擁有一個全局的 Store。

工廠模式

1. 什麼是工廠模式?

工廠模式就是根據不用的輸入返回不同的實例,一般用來創建同一類對象,它的主要思想就是將對象的創建與對象的實現分離。
在創建對象時,不暴露具體的邏輯,而是將邏輯封裝在函數中,那麼這個函數就可以被視爲一個工廠。工廠模式根據抽象程度的不同可以分爲:簡單工廠、工廠方法、抽象工廠。

Vue 中的工廠模式

(1)VNode

和原生的 document.createElement 類似,Vue 這種具有虛擬 DOM 樹(Virtual Dom Tree)機制的框架在生成虛擬 DOM 的時候,提供了 createElement 方法用來生成 VNode,用來作爲真實 DOM 節點的映射:

createElement('h3', { class: 'main-title' }, [
    createElement('img', { class: 'avatar', attrs: { src: '../avatar.jpg' } }),
    createElement('p', { class: 'user-desc' }, 'hello world')
])

createElement 函數結構大概如下:

class Vnode (tag, data, children) { ... }
function createElement(tag, data, children) {
      return new Vnode(tag, data, children)
}

(2)vue-route

在 Vue 在路由創建模式中,也多次用到了工廠模式:

export default class VueRouter {
    constructor(options) {
        this.mode = mode    // 路由模式
        
        switch (mode) {           // 簡單工廠
            case 'history':       // history 方式
                this.history = new HTML5History(this, options.base)
                break
            case 'hash':          // hash 方式
                this.history = new HashHistory(this, options.base, this.fallback)
                break
            case 'abstract':      // abstract 方式
                this.history = new AbstractHistory(this, options.base)
                break
            default:
                // ... 初始化失敗報錯
        }
    }
}

mode 是路由創建的模式,這裏有三種 History、Hash、Abstract,其中,History 是 H5 的路由方式,Hash 是路由中帶 # 的路由方式,Abstract 代表非瀏覽器環境中路由方式,比如 Node、weex 等;this.history 用來保存路由實例,vue-router 中使用了工廠模式的思想來獲得響應路由控制類的實例。

策略模式

1. 什麼是策略模式?

策略模式 (Strategy Pattern)又稱政策模式,其定義一系列的算法,把它們一個個封裝起來,並且使它們可以互相替換。封裝的策略算法一般是獨立的,策略模式根據輸入來調整採用哪個算法。關鍵是策略的實現和使用分離。

策略模式的實際應用

(1)表格 formatter

Element UI 的表格控件的 Column 接受一個 formatter 參數,用來格式化內容,其類型爲函數,並且還可以接受幾個特定參數,像這樣:Function(row, column, cellValue, index)。

以文件大小轉化爲例,後端經常會直接傳 bit 單位的文件大小,那麼前端需要根據後端的數據,根據需求轉化爲自己需要的單位的文件大小,比如 KB/MB。

首先實現文件計算的算法:

export const StrategyMap = {
    // Strategy 1: 將文件大小(bit)轉化爲 KB 
    bitToKB: val => {
        const num = Number(val)
        return isNaN(num) ? val : (num / 1024).toFixed(0) + 'KB'
    },
    // Strategy 2: 將文件大小(bit)轉化爲 MB 
    bitToMB: val => {
        const num = Number(val)
        return isNaN(num) ? val : (num / 1024 / 1024).toFixed(1) + 'MB'
    }
}
// Context: 生成el表單 formatter 
const strategyContext = function(type, rowKey){ 
  return function(row, column, cellValue, index){
      StrategyMap[type](row[rowKey])
  }
}
export default strategyContext

在組件中直接使用:

<template>
    <el-table :data="tableData">
        <el-table-column prop="date" label="日期"></el-table-column>
        <el-table-column prop="name" label="文件名"></el-table-column>
        <!-- 直接調用 strategyContext -->
        <el-table-column prop="sizeKb" label="文件大小(KB)"
                         :formatter='strategyContext("bitToKB", "sizeKb")'>
        </el-table-column>
        <el-table-column prop="sizeMb" label="附件大小(MB)"
                         :formatter='strategyContext("bitToMB", "sizeMb")'>
        </el-table-column>
    </el-table>
</template>
<script type='text/javascript'>
    import strategyContext from './strategyContext.js'
    
    export default {
        name: 'ElTableDemo',
        data() {
            return {
                strategyContext,
                tableData: [
                    { date: '2019-05-02', name: '文件1', sizeKb: 1234, sizeMb: 1234426 },
                    { date: '2019-05-04', name: '文件2', sizeKb: 4213, sizeMb: 8636152 }]
            }
        }
    }
</script>
<style scoped></style>

運行結果如下圖

(2)表單驗證

除了表格中的 formatter 之外,策略模式也經常用在表單驗證的場景。Element UI 的 Form 表單 具有表單驗證功能,用來校驗用戶輸入的表單內容。實際需求中表單驗證項一般會比較複雜,所以需要給每個表單項增加 validator 自定義校驗方法。

實現通用的表單驗證方法:

// src/utils/validates.js
// 姓名校驗 由2-10位漢字組成 
export function validateUsername(str) {
    const reg = /^[\u4e00-\u9fa5]{2,10}$/
    return reg.test(str)
}
// 手機號校驗 由以1開頭的11位數字組成  
export function validateMobile(str) {
    const reg = /^1\d{10}$/
    return reg.test(str)
}
// 郵箱校驗 
export function validateEmail(str) {
    const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
    return reg.test(str)
}

增加一個柯里化方法,用來生成表單驗證函數:

// src/utils/index.js
import * as Validates from './validates.js'
// 生成表格自定義校驗函數 
export const formValidateGene = (key, msg) => (rule, value, cb) => {
    if (Validates[key](value)) {
        cb()
    } else {
        cb(new Error(msg))
    }
}

具體使用:

<template>
    <el-form ref="ruleForm"
             label-width="100px"
             class="demo-ruleForm"
             :rules="rules"
             :model="ruleForm">
        
        <el-form-item label="用戶名" prop="username">
            <el-input v-model="ruleForm.username"></el-input>
        </el-form-item>
        
        <el-form-item label="手機號" prop="mobile">
            <el-input v-model="ruleForm.mobile"></el-input>
        </el-form-item>
        
        <el-form-item label="郵箱" prop="email">
            <el-input v-model="ruleForm.email"></el-input>
        </el-form-item>
    </el-form>
</template>
<script type='text/javascript'>
    import * as Utils from '../utils'
    
    export default {
        name: 'ElTableDemo',
        data() {
            return {
                ruleForm: { pass: '', checkPass: '', age: '' },
                rules: {
                    username: [{
                        validator: Utils.formValidateGene('validateUsername', '姓名由2-10位漢字組成'),
                        trigger: 'blur'
                    }],
                    mobile: [{
                        validator: Utils.formValidateGene('validateMobile', '手機號由以1開頭的11位數字組成'),
                        trigger: 'blur'
                    }],
                    email: [{
                        validator: Utils.formValidateGene('validateEmail', '不是正確的郵箱格式'),
                        trigger: 'blur'
                    }]
                }
            }
        }
    }
</script>

效果如圖:

代理模式

1. 什麼是代理模式?

代理模式 (Proxy Pattern)又稱委託模式,它爲目標對象創造了一個代理對象,以控制對目標對象的訪問。
代理模式把代理對象插入到訪問者和目標對象之間,從而爲訪問者對目標對象的訪問引入一定的間接性。正是這種間接性,給了代理對象很多操作空間,比如在調用目標對象前和調用後進行一些預操作和後操作,從而實現新的功能或者擴展目標的功能。

代理模式在實戰中的應用

(1)攔截器

在項目中經常使用 Axios 的實例來進行 HTTP 的請求,使用攔截器 interceptor 可以提前對 request 請求和 response 返回進行一些預處理,比如:

1、request 請求頭的設置,和 Cookie 信息的設置;
2、權限信息的預處理,常見的比如驗權操作或者 Token 驗證;
3、數據格式的格式化,比如對組件綁定的 Date 類型的數據在請求前進行一些格式約定好的序列化操作;
4、空字段的格式預處理,根據後端進行一些過濾操作;
5、response 的一些通用報錯處理,比如使用 Message 控件拋出錯誤;

除了 HTTP 相關的攔截器之外,還有路由跳轉的攔截器,可以進行一些路由跳轉的預處理等操作。

(2)前端框架的數據響應式化

Vue 2.x 中通過 Object.defineProperty 來劫持各個屬性的 setter/getter,在數據變動時,通過發佈 - 訂閱模式發佈消息給訂閱者,觸發相應的監聽回調,從而實現數據的響應式化,也就是數據到視圖的雙向綁定。
爲什麼 Vue 2.x 到 3.x 要從 Object.defineProperty 改用 Proxy 呢,是因爲前者的一些侷限性,導致的以下缺陷:

1、無法監聽利用索引直接設置數組的一個項,例如:vm.items[indexOfItem] = newValue;
2、無法監聽數組的長度的修改,例如:vm.items.length = newLength;
3、無法監聽 ES6 的 Set、WeakSet、Map、WeakMap 的變化;
4、無法監聽 Class 類型的數據;
5、無法監聽對象屬性的新加或者刪除;

適配器模式

1. 什麼是適配器模式?

適配器模式(Adapter Pattern)又稱包裝器模式,將一個類(對象)的接口(方法、屬性)轉化爲用戶需要的另一個接口,解決類(對象)之間接口不兼容的問題。
主要功能是進行轉換匹配,目的是複用已有的功能,而不是來實現新的接口。也就是說,訪問者需要的功能應該是已經實現好了的,不需要適配器模式來實現,適配器模式主要是負責把不兼容的接口轉換成訪問者期望的格式而已。

適配器的實際案例

(1)Vue 計算屬性

Vue 中的計算屬性也是一個適配器模式的實例,以官網的例子爲例:

<template>
    <div id="example">
        <p>Original message: "{{ message }}"</p>  <!-- Hello -->
        <p>Computed reversed message: "{{ reversedMessage }}"</p>  <!-- olleH -->
    </div>
</template>
<script type='text/javascript'>
    export default {
        name: 'demo',
        data() {
            return {
                message: 'Hello'
            }
        },
        computed: {
            reversedMessage: function() {
                return this.message.split('').reverse().join('')
            }
        }
    }
</script>

對原有數據並沒有改變,只改變了原有數據的表現形式。

(2) 源碼中的適配器模式

Axios 的用來發送請求的 adapter 本質上是封裝瀏覽器提供的 API XMLHttpRequest。

module.exports = function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
        var requestData = config.data
        var requestHeaders = config.headers
        
        var request = new XMLHttpRequest()
        
        // 初始化一個請求
        request.open(config.method.toUpperCase(),
          buildURL(config.url, config.params, config.paramsSerializer), true)
        
        // 設置最大超時時間
        request.timeout = config.timeout
        
        // readyState 屬性發生變化時的回調
        request.onreadystatechange = function handleLoad() { ... }
        
        // 瀏覽器請求退出時的回調
        request.onabort = function handleAbort() { ... }
        
        // 當請求報錯時的回調
        request.onerror = function handleError() { ... }
        
        // 當請求超時調用的回調
        request.ontimeout = function handleTimeout() { ... }
        
        // 設置HTTP請求頭的值
        if ('setRequestHeader' in request) {
            request.setRequestHeader(key, val)
        }
        
        // 跨域的請求是否應該使用證書
        if (config.withCredentials) {
            request.withCredentials = true
        }
        
        // 響應類型
        if (config.responseType) {
            request.responseType = config.responseType
        }
        
        // 發送請求
        request.send(requestData)
    })
}

這個模塊主要是對請求頭、請求配置和一些回調的設置,並沒有對原生的 API 有改動,所以也可以在其他地方正常使用。這個適配器可以看作是對 XMLHttpRequest 的適配,是用戶對 Axios 調用層到原生 XMLHttpRequest 這個 API 之間的適配層。

觀察者模式 / 發佈 - 訂閱模式

1. 什麼是觀察者模式?

觀察者模式(Observer Pattern)定義了一種一對多的關係,讓多個訂閱者對象同時監聽某一個發佈者,或者叫主題對象,這個主題對象的狀態發生變化時就會通知所有訂閱自己的訂閱者對象,使得它們能夠自動更新自己。

2. 什麼是發佈 - 訂閱模式?

其實它是發佈訂閱模式的一個別名,但兩者又有所不同。這個別名非常形象地詮釋了觀察者模式裏兩個核心的角色要素——發佈者和訂閱者。

發佈 - 訂閱模式有一個調度中心

觀察者模式是由具體目標調度的,而發佈 - 訂閱模式是統一由調度中心調的

Vue 中的發佈 - 訂閱模式

(1)EventBus

在 Vue 中有一套事件機制,其中一個用法是 EventBus。可以使用 EventBus 來解決組件間的數據通信問題。

1. 創建事件中心管理組件之間的通信

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

2. 發送事件

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>
<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
  components: { firstCom, secondCom }
}
</script>

firstCom 組件中發送事件:

<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>
<script>
import {EventBus} from './event-bus.js' // 引入事件中心
export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

3. 接收事件

在 secondCom 組件中發送事件:

<template>
  <div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

(2)Vue 源碼

發佈 - 訂閱模式在源碼中應用很多,比如雙向綁定機制的場景

響應式化大致就是使用 Object.defineProperty 把數據轉爲 getter/setter,併爲每個數據添加一個訂閱者列表的過程。這個列表是 getter 閉包中的屬性,將會記錄所有依賴這個數據的組件。也就是說,響應式化後的數據相當於發佈者。

每個組件都對應一個 Watcher 訂閱者。當每個組件的渲染函數被執行時,都會將本組件的 Watcher 放到自己所依賴的響應式數據的訂閱者列表裏,這就相當於完成了訂閱,一般這個過程被稱爲依賴收集(Dependency Collect)。

組件渲染函數執行的結果是生成虛擬 DOM 樹(Virtual DOM Tree),這個樹生成後將被映射爲瀏覽器上的真實的 DOM 樹,也就是用戶所看到的頁面視圖。

當響應式數據發生變化的時候,也就是觸發了 setter 時,setter 會負責通知(Notify)該數據的訂閱者列表裏的 Watcher,Watcher 會觸發組件重渲染(Trigger re-render)來更新(update)視圖。

Vue 的源碼:

// src/core/observer/index.js 響應式化過程
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
        // ...
        const value = getter ? getter.call(obj) : val // 如果原本對象擁有getter方法則執行
        dep.depend()                     // 進行依賴收集,dep.addSub
        return value
    },
    set: function reactiveSetter(newVal) {
        // ...
        if (setter) { setter.call(obj, newVal) }    // 如果原本對象擁有setter方法則執行
        dep.notify()               // 如果發生變更,則通知更新
    }
})

轉載自:https://www.cnblogs.com/cczlovexw/p/16899624.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ma_4ClowaqUeHEtThqFK_w