<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          設(shè)計(jì)模式在前端項(xiàng)目中的應(yīng)用

          共 10624字,需瀏覽 22分鐘

           ·

          2021-05-06 17:49

          來源 | https://juejin.cn/post/6956825832073461768


          前端的設(shè)計(jì)模式是什么

          設(shè)計(jì)模式一個(gè)比較宏觀的概念,通俗來講,它是軟件開發(fā)人員在軟件開發(fā)過程中面臨的一些具有代表性問題的解決方案。
          當(dāng)然,在實(shí)際開發(fā)中不用設(shè)計(jì)模式同樣也是可以實(shí)現(xiàn)需求的,只是在業(yè)務(wù)邏輯比較復(fù)雜的情況下,代碼可讀性及可維護(hù)性變差。
          所以隨著業(yè)務(wù)邏輯的擴(kuò)展,了解常用設(shè)計(jì)模式解決問題是非常有必要的。

          前端的設(shè)計(jì)模式的基本準(zhǔn)則

          • 單一職責(zé)原則:每個(gè)類只需要負(fù)責(zé)自己的那部分,類的復(fù)雜度降低。
          • 開閉原則:一個(gè)實(shí)體,如類、模塊和函數(shù)應(yīng)該對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉,讓程序更穩(wěn)定更靈活。
          • 里式替換原則:所有引用基類的地方必須能透明地使用其子類的對(duì)象,也就是說子類對(duì)象可以替換其父類對(duì)象,而程序執(zhí)行效果不變。便于構(gòu)建擴(kuò)展性更好的系統(tǒng)。
          • 依賴倒置原則:上層模塊不應(yīng)該依賴底層模塊,它們都應(yīng)該依賴于抽象;抽象不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴于抽象。這可以讓項(xiàng)目擁有變化的能力。
          • 接口隔離原則:多個(gè)特定的客戶端接口要好于一個(gè)通用性的總接口,系統(tǒng)有更高的靈活性。
          • 迪米特原則(最少知識(shí)原則):一個(gè)類對(duì)于其他類知道的越少越好,也就是說一個(gè)對(duì)象應(yīng)當(dāng)對(duì)其他對(duì)象有盡可能少的了解。

          設(shè)計(jì)模式的種類

          1、 創(chuàng)建型模式
          一般用于創(chuàng)建對(duì)象。
          包括:?jiǎn)卫J?工廠方法模式,抽象工廠模式,建造者模式,原型模式。
          2、結(jié)構(gòu)型模式
          重點(diǎn)為“繼承”關(guān)系,有著一層繼承關(guān)系,且一般都有“代理”。
          包括:適配器模式,橋接模式,組合模式,裝飾器模式,外觀模式,享元模式,代理模式,過濾器模式。
          3、行為型模式
          職責(zé)的劃分,各自為政,減少外部的干擾。
          包括:命令模式,解釋器模式,迭代器模式,中介者模式,備忘錄模式,觀察者模式,狀態(tài)模式,策略模式,模板方法模式,訪問者模式,責(zé)任鏈模式。

          前端常用的計(jì)模式應(yīng)用實(shí)例

          1、單例模式
          單例模式又稱為單體模式,保證一個(gè)類只有一個(gè)實(shí)例,并提供一個(gè)訪問它的全局訪問點(diǎn)。一個(gè)極有可能重復(fù)出現(xiàn)的“實(shí)例”, 如果重復(fù)創(chuàng)建,將會(huì)產(chǎn)生性能消耗。如果借助第一次的實(shí)例,后續(xù)只是對(duì)該實(shí)例的重復(fù)使用,這樣就達(dá)到了我們節(jié)省性能的目的。
          全局彈窗是前端開發(fā)中一個(gè)比較常規(guī)的需求,一般情況下,同一時(shí)間只會(huì)存在一個(gè)全局彈窗,我們可以實(shí)現(xiàn)單例模式,保證每次實(shí)例化時(shí)返回的實(shí)際上是同一個(gè)方法。
          class MessageBox {    show() {        console.log("show");    }    hide() {}
          static getInstance() { if (!MessageBox.instance) { MessageBox.instance = new MessageBox(); } return MessageBox.instance; }}
          let box3 = MessageBox.getInstance();let box4 = MessageBox.getInstance();
          console.log(box3 === box4); // true

          上面這種是比較常見的單例模式實(shí)現(xiàn),但是這種方式存在一些弊端。因?yàn)樗枰屨{(diào)用方了解到通過Message.getInstance來獲取單例。

          又或者假設(shè)需求變更,可以通過存在二次彈窗,則需要改動(dòng)不少地方,因?yàn)镸essageBox除了實(shí)現(xiàn)常規(guī)的彈窗邏輯之外,還需要負(fù)責(zé)維護(hù)單例的邏輯。

          因此,可以將初始化單例的邏輯單獨(dú)維護(hù),實(shí)現(xiàn)一個(gè)通用的、返回某個(gè)類對(duì)應(yīng)單例的方法。

          function getSingleton(ClassName) {    let instance;    return () => {        if (!instance) {            instance = new ClassName();        }        return instance;    };}
          const createMessageBox = getSingleton(MessageBox);let box5 = createMessageBox();let box6 = createMessageBox();console.log(box5 === box6);

          這樣,通過createMessageBox返回的始終是同一個(gè)實(shí)例。如果在某些場(chǎng)景下需要生成另外的實(shí)例,則可以重新生成一個(gè)createMessageBox方法,或者直接調(diào)用new MessageBox(),這樣就對(duì)之前的邏輯不會(huì)有任何影響。

          2、工廠模式

          工廠模式提供了一種創(chuàng)建對(duì)象的方法,對(duì)使用方隱藏了對(duì)象的具體實(shí)現(xiàn)細(xì)節(jié),并使用一個(gè)公共的接口來創(chuàng)建對(duì)象。

          前端本地存儲(chǔ)目前最常見的方案就是使用localStorage,為了避免在業(yè)務(wù)代碼中各種getItem和setItem,我們可以做一下最簡(jiǎn)單的封裝。

          let themeModel = {    name: "local_theme",    get() {        let val = localStorage.getItem(this.name);        return val && jsON.parse(val);    },    set(val) {        localStorage.setItem(this.name, jsON.stringify(val));    },    remove() {        localStorage.removeItem(this.name);    },};themeModel.get();themeModel.set({ darkMode: true });

          這樣,通過themeModel暴露的get、set接口,我們無需再維護(hù)local_theme。但上面的封裝也存在一些可見的問題,如果需要新增多個(gè) name,那么上面的模板代碼需要重新寫多遍嗎?為了解決這個(gè)問題,我們可以創(chuàng)建Model對(duì)象的邏輯進(jìn)行封裝。

          const storageMap = new Map()function createStorageModel(key, storage = localStorage) {    // 相同key返回單例    if (storageMap.has(key)) {        return storageMap.get(key);    }
          const model = { key, set(val) { storage.setItem(this.key, JSON.stringify(val);); }, get() { let val = storage.getItem(this.key); return val && JSON.parse(val); }, remove() { storage.removeItem(this.key); }, }; storageMap.set(key, model); return model;}
          const themeModel = createStorageModel('local_theme', localStorage)const utmSourceModel = createStorageModel('utm_source', sessionStorage)

          這樣,我們就可以通過createStorageModel這個(gè)公共的接口來創(chuàng)建各種不同本地存儲(chǔ)的對(duì)象,而無需關(guān)注創(chuàng)建對(duì)象的具體細(xì)節(jié)。

          3、策略模式

          策略模式,可以針對(duì)不同的狀態(tài),給出不同的算法或者結(jié)果。將層級(jí)相同的邏輯封裝成可以組合和替換的策略方法,減少if...else代碼,方便擴(kuò)展后續(xù)功能。

          表單校驗(yàn)是我們最常見的場(chǎng)景了,我們一般都會(huì)想到用if...else來判斷。

          function onFormSubmit(params) {    if (!params.name) {        return showError("請(qǐng)?zhí)顚戧欠Q");    }    if (params.name.length > 6) {        return showError("昵稱最多6位字符");    }    if (!/^1\d{10}$/.test(params.phone))        return showError("請(qǐng)?zhí)顚懻_的手機(jī)號(hào)");    }    // ...    sendSubmit(params)}

          將所有字段的校驗(yàn)規(guī)則都堆疊在一起,代碼量大,排查問題也是一個(gè)大麻煩。在遇見錯(cuò)誤時(shí),直接通過 return 跳過了后面的判斷;如果我們希望直接展示每個(gè)字段的錯(cuò)誤呢,那么改動(dòng)的工作量又不少。

          不過,在antd、ELementUI等框架盛行的年代,我們已經(jīng)不再需要寫這些復(fù)雜的表單校驗(yàn),但是對(duì)于他們的實(shí)現(xiàn)原理,我們可以簡(jiǎn)單模擬一下。

          // 定義一個(gè)校驗(yàn)的類,主要暴露了構(gòu)造參數(shù)和validate兩個(gè)接口class Schema {    constructor(descriptor) {        this.descriptor = descriptor; // 傳入定義的校驗(yàn)規(guī)則    }   // 拆分出一些更通用的規(guī)則,比如required(必填)、len(長(zhǎng)度)、min/max(最值)等,可以盡可能地復(fù)用    handleRule(val, rule) {        const { key, params, message } = rule;        let ruleMap = {            required() {                return !val;            },            max() {                return val > params;            },            validator() {                return params(val);            },        };
          let handler = ruleMap[key]; if (handler && handler()) { throw message; } }
          validate(data) { return new Promise((resolve, reject) => { let keys = Object.keys(data); let errors = []; for (let key of keys) { const ruleList = this.descriptor[key]; if (!Array.isArray(ruleList) || !ruleList.length) continue;
          const val = data[key]; for (let rule of ruleList) { try { this.handleRule(val, rule); } catch (e) { errors.push(e.toString()); } } } if (errors.length) { reject(errors); } else { resolve(); } }); }}

          // 聲明每個(gè)字段的校驗(yàn)邏輯const descriptor = { nickname: [ { key: "required", message: "請(qǐng)?zhí)顚戧欠Q" }, { key: "max", params: 6, message: "昵稱最多6位字符" }, ], phone: [ { key: "required", message: "請(qǐng)?zhí)顚戨娫捥?hào)碼" }, { key: "validator", params(val) { return !/^1\d{10}$/.test(val); }, message: "請(qǐng)?zhí)顚懻_的電話號(hào)碼", }, ],};

          // 開始對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)const validator = new Schema(descriptor);const params = { nickname: "", phone: "123000" };validator.validate(params).then(() => { console.log("success");}).catch((e) => { console.log(e);});

          Schema主要暴露了構(gòu)造參數(shù)和validate兩個(gè)接口,是一個(gè)通用的工具類,而params是表單提交的數(shù)據(jù)源,因此主要的校驗(yàn)邏輯實(shí)際上是在descriptor中聲明的。將常見的校驗(yàn)規(guī)則都放在ruleMap中,比之前各種不可復(fù)用的if..else判斷更容易維護(hù)和迭代。

          4、狀態(tài)模式

          狀態(tài)模式允許一個(gè)對(duì)象在其內(nèi)部狀態(tài)改變的時(shí)候改變它的行為。狀態(tài)模式的思路是:首先創(chuàng)建一個(gè)狀態(tài)對(duì)象保存狀態(tài)變量,然后封裝好每種動(dòng)作對(duì)應(yīng)的狀態(tài),然后狀態(tài)對(duì)象返回一個(gè)接口對(duì)象,它可以對(duì)內(nèi)部的狀態(tài)修改或者調(diào)用。

          常見的使用場(chǎng)景,比如滾動(dòng)加載,包含了初始化加載、加載成功、加載失敗、滾動(dòng)加載等狀態(tài),任意時(shí)間它只會(huì)處于一種狀態(tài)。

          // 定義一個(gè)狀態(tài)機(jī)class rollingLoad {  constructor() {    this._currentState = 'init'    this.states = {        init: { failed: 'error' },        init: { complete: 'normal' },        normal: { rolling: 'loading' },        loading: { complete: 'normal' },        loading: { failed: 'error' },    }    this.actions = {        init() {          console.log('初始化加載,大loading')        },        normal() {          console.log('加載成功,正常展示')        },        error() {          console.log('加載失敗')        },        loading() {          console.log('滾動(dòng)加載')        }        // .....    }  }
          change(state) { // 更改當(dāng)前狀態(tài) let to = this.states[this._currentState][state] if(to){ this._currentState = to this.go() return true } return false } go() { this.actions[this._currentState]() return this }}
          // 狀態(tài)更改的操作const rollingLoad = new rollingLoad()rollingLoad.go()rollingLoad.change('complete')rollingLoad.change('loading')

          這樣,我們就可以通過狀態(tài)變更,運(yùn)行相應(yīng)的函數(shù),且狀態(tài)之間存在聯(lián)系。那么,看起來是不是和策略模式很像呢?其實(shí)不然,策略類的各個(gè)屬性之間是平等平行的,它們之間沒有任何聯(lián)系。而狀態(tài)機(jī)中的各個(gè)狀態(tài)之間存在相互切換,且是被規(guī)定好了的。

          5、發(fā)布-訂閱模式

          發(fā)布—訂閱模式又叫觀察者模式,它定義對(duì)象間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都將得到通知。

          發(fā)布訂閱模式大概是前端同學(xué)最熟悉的設(shè)計(jì)模式之一了,常見的事件監(jiān)聽addEventListener,各種屬性方法onload、onchange,vue響應(yīng)式數(shù)據(jù),組件通信redux、eventBus等。

          常見的獲取登錄信息,假設(shè)我們開發(fā)一個(gè)商城網(wǎng)站,網(wǎng)站里有 header 頭部、nav 導(dǎo)航、消息列表、購物車等模塊。

          這幾個(gè)模塊的渲染有一個(gè)共同的前提條件,就是必須先用 ajax 異步請(qǐng)求獲取用戶的登錄信息。

          比如用戶的名字和頭像要顯示在 header 模塊里,而這兩個(gè)字段都來自用戶登錄后返回的信息。異步的問題通常也可以用回調(diào)函數(shù)來解決:

          login.succ(function(data){ header.setAvatar( data.avatar); // 設(shè)置 header 模塊的頭像 nav.setAvatar( data.avatar ); // 設(shè)置導(dǎo)航模塊的頭像 message.refresh(); // 刷新消息列表 cart.refresh(); // 刷新購物車列表});

          我們還必須了解 header 模塊里設(shè)置頭像的方法叫setAvatar、購物車模塊里刷新的方法叫refresh,這種強(qiáng)耦合性會(huì)使程序變得不易拓展。

          那么回頭看看我們的發(fā)布—訂閱模式,這種模式下,對(duì)用戶信息感興趣的業(yè)務(wù)模塊可以自行訂閱登錄成功的消息事件。

          當(dāng)?shù)卿洺晒r(shí),登錄模塊只需要發(fā)布登錄成功的消息,而業(yè)務(wù)方接受到消息之后,就會(huì)開始進(jìn)行各自的業(yè)務(wù)處理,登錄模塊并不關(guān)心業(yè)務(wù)方究竟要做什么。

          // 發(fā)布登錄成功的消息$.ajax( 'http://xxx.com?login', function(data){ // 登錄成功 login.trigger( 'loginSucc', data); // 發(fā)布登錄成功的消息});
          // 各模塊監(jiān)聽登錄成功的消息var header = (function(){ // header 模塊 login.listen( 'loginSucc', function(data){ header.setAvatar( data.avatar ); }); return { setAvatar: function( data ){ console.log( '設(shè)置 header 模塊的頭像' ); } }})();var nav = (function(){ // nav 模塊 login.listen( 'loginSucc', function( data ){ nav.setAvatar( data.avatar ); }); return { setAvatar: function( avatar ){ console.log( '設(shè)置 nav 模塊的頭像' ); } }})();

          發(fā)布—訂閱模式可以廣泛應(yīng)用于異步編程中,這是一種替代傳遞回調(diào)函數(shù)的方案。比如,我們可以訂閱ajax請(qǐng)求的error、succ等事件。

          或者如果想在動(dòng)畫的每一幀完成之后做一些事情,那我們可以訂閱一個(gè)事件,然后在動(dòng)畫的每一幀完成之后發(fā)布這個(gè)事件。

          在異步編程中使用發(fā)布—訂閱模式,我們就無需過多關(guān)注對(duì)象在異步運(yùn)行期間的內(nèi)部狀態(tài),而只需要訂閱感興趣的事件發(fā)生點(diǎn)。

          6、迭代器模式

          迭代器模式是指提供一種方法順序訪問一個(gè)聚合對(duì)象中的各個(gè)元素,而又不需要暴露該對(duì)象的內(nèi)部表示。

          迭代器模式可以把迭代的過程從業(yè)務(wù)邏輯中分離出來,在使用迭代器模式之后,即使不關(guān)心對(duì)象的內(nèi)部構(gòu)造,也可以按順序訪問其中的每個(gè)元素。

          JS 也內(nèi)置了多種遍歷數(shù)組的方法如forEach、reduce等。對(duì)于數(shù)組的循環(huán)大家都輕車熟路了,在實(shí)際開發(fā)中,也可以通過循環(huán)來優(yōu)化代碼。

          一個(gè)常見的開發(fā)場(chǎng)景是:通過 ua 判斷當(dāng)前頁面的運(yùn)行平臺(tái),方便執(zhí)行不同的業(yè)務(wù)邏輯,最基本的寫法當(dāng)然是if...else。

          const PAGE_TYPE = {    app: "app", // app    wx: "wx", // 微信    tiktok: "tiktok", // 抖音    bili: "bili", // B站    kwai: "kwai", // 快手};function getPageType() {    const ua = navigator.userAgent;    let pageType;    // 移動(dòng)端、桌面端微信瀏覽器    if (/xxx_app/i.test(ua)) {        pageType = app;    } else if (/MicroMessenger/i.test(ua)) {        pageType = wx;    } else if (/aweme/i.test(ua)) {        pageType = tiktok;    } else if (/BiliApp/i.test(ua)) {        pageType = bili;    } else if (/Kwai/i.test(ua)) {        pageType = kwai;    } else {        // ...    }    return pageType;}

          參考策略模式的思路,我們可以減少分支判斷的出現(xiàn),將每個(gè)平臺(tái)的判斷拆分成單獨(dú)的策略:

          function isApp(ua) {    return /xxx_app/i.test(ua);}
          function isWx(ua) { return /MicroMessenger/i.test(ua);}
          function isTiktok(ua) { return /aweme/i.test(ua);}
          function isBili(ua) { return /BiliApp/i.test(ua);}
          function isKwai(ua) { return /Kwai/i.test(ua);}
          let platformList = [ { name: "app", validator: isApp }, { name: "wx", validator: isWx }, { name: "tiktok", validator: isTiktok }, { name: "bili", validator: isBili }, { name: "kwai", validator: isKwai },];function getPageType() { // 每個(gè)平臺(tái)的名稱與檢測(cè)方法 const ua = navigator.userAgent; // 遍歷 for (let { name, validator } in platformList) { if (validator(ua)) { return name; } }}

          這樣,整個(gè)getPageType方法就變得非常簡(jiǎn)潔:按順序遍歷platformList,返回第一個(gè)匹配上的平臺(tái)名稱作為pageType。

          這樣即使后面需要增加或移除平臺(tái)判斷,需要修改的僅僅也只是platformList這個(gè)地方而已。

          迭代器模式是一種相對(duì)簡(jiǎn)單的模式,簡(jiǎn)單到很多時(shí)候我們都不認(rèn)為它是一種設(shè)計(jì)模式。目前的絕大部分語言都內(nèi)置了迭代器。

          總結(jié)

          在將函數(shù)作為一等對(duì)象的語言中,有許多需要利用對(duì)象多態(tài)性的設(shè)計(jì)模式,這些模式的結(jié)構(gòu)與傳統(tǒng)面向?qū)ο笳Z言的結(jié)構(gòu)大相徑庭,實(shí)際上已經(jīng)融入到了語言之中,我們可能經(jīng)常使用它們,只是不知道它們的名字而已。

          深入理解他們,并有意識(shí)地去使用設(shè)計(jì)模式來優(yōu)化代碼,提升效率,使我們的系統(tǒng)有更好的拓展性才是我們追求的。

          學(xué)習(xí)更多技能

          請(qǐng)點(diǎn)擊下方公眾號(hào)



          瀏覽 66
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  爱爱视频免费看 | 内射极品在线观看免费 | 欧美乱婬妺妺躁爽A片 | 天天艹天天日 | 99视频精品全部免费看 |