- 前端跨界開發指南:JavaScript工具庫原理解析與實戰
- 史文強
- 3732字
- 2022-08-12 16:06:18
1.4 Mock.js的基本原理
作為前端工程師,熟練使用某個第三方工具庫只是基本要求,你需要對相關技術的基本原理、優缺點甚至未來的迭代優化方向有自己的理解,建議先嘗試手動實現一些核心特征,看看自己能不能做到,然后再來閱讀源碼,看看工具庫的作者是如何設計整體代碼架構的,反思一下自己實現的那一部分與作者的實現思路是否一致,再仔細研究一下自己寫不出來的那一部分,相較于閱讀相關的技術博客,這種方式更能促進你快速成長。本節就來嘗試手動復現Mock.js的主要功能,并學習如何以不同的方式將Mock數據作為請求的響應數據返回給調用者。
1.4.1 從模板到數據
Mock.js的核心功能是將特殊標記的語法轉換為對應的模擬數據,也可以將其看作一種簡易的DSL(Domain-Specific Language,領域特定語言)轉換器。DSL并不是一種具體的語言,而是泛指任何針對指定領域的語言,它并不一定會有嚴格的標準,也可能僅僅是一種約定規范。在未來的前端開發中還會出現很多類似的任務,比如當下非常熱門的“可視化搭建”技術,就是圍繞DSL解析來實現的。Mock.js使用了一種非常簡單的標記語法來描述數據的類型和結構,對于提供模擬業務數據這樣的需求而言它已經夠用了。阿里媽媽前端團隊出品的開源接口管理工具Rap2[1]在實現Mock功能時也使用了這樣的語法。如果想要描述更復雜的數據結構,比如對表單項或是前端UI進行抽象描述,簡易的模板語法可能會顯得力不從心,此時就可以考慮使用更為通用的JSON Schema[2]格式,如果希望構造出更加強大的模板語法,則還需要學習編譯原理方面的知識,當然,這并不是本章的重點。
從前文中我們已經看到了Mock.js模板語法的格式比較固定,所以解析難度并不大。下面就以常見類型的語法解析為例來講解從模板到數據的轉換過程,示例代碼如下:
//轉換策略單例 Strategies = { 'String':(rule, value)=>{//...字符串類型的轉換處理函數}, 'Number':(rule, value)=>{//...數字類型的轉換處理函數}, 'Boolean':(rule, value)=>{//...布爾類型的轉換處理函數}, 'Array':(rule, value)=>{//...數組類型的轉換處理函數}, 'Placeholder':(rule, value)=>{//...占位符類型的轉換處理函數} //...其他類型的轉換策略 } //模板轉換函數 function parseTemplate(schema = {}){ let result = {}; for ( let prop of Object.keys(schema)){ let [name, rule] = prop.split('|'); let value = tplObj[prop]; let type = value.startsWith('@')? 'Placeholder': Object.prototype.toString.call(value).slice(8,-1); result[name] = Strategies[type](rule, value); } return result; }
上面的代碼并不難理解,首先使用一個對象來封裝模板中不同類型所對應的模板轉換函數,模板轉換函數的返回結果即所生成的虛擬數據,這是一個典型的策略模式應用場景。當你想要增加新的類型時,只需要將新的解析函數以鍵值對的形式加入策略(Strategies)對象中即可,不必修改舊的代碼。這樣我們只需要遍歷模板對象中的每條規則并拆分出關鍵信息即可,如果初始值是以@開頭的字符串,則需要映射為占位符,否則就直接根據值的類型來找到對應的轉換函數,最后將拆分出的信息作為參數傳入對應的轉換函數中就可以得到模擬數據。
拓展知識
“策略模式”是前端領域使用率較高的經典設計模式之一,使用該模式需要先定義一系列的算法,把它們封裝起來,然后在使用過程中根據參數來動態選擇算法,其目的是將算法的使用和算法的實現隔離開來,這樣一來,使用者對于可變部分的修改和擴展就不會影響到不可變的部分,這是讓代碼保持“開放封閉原則”的一種有效手段。
例如在一個結算系統中,代碼中編寫了多個函數用于計算不同類別的會員可以享受的滿減優惠,對于程序的邏輯來說,不變的部分就是結算時需要根據會員類別計算的滿減規則,而變化的部分就是不同的會員類別對應的滿減策略是不同的,那么對滿減策略進行修改或擴充的代碼就不應該影響結算流程的代碼,同樣,當向結算流程中加入其他環節時,也不應該影響滿減策略的相關代碼,這就是開發中常說的“解耦”。在JavaScript編程中使用“策略模式”時,通常會將各種策略函數以“鍵值對”的形式收集到一個對象上,這個對象稱為“策略集”。當然,為了避免全局污染,也可以使用閉包將策略集對象包裹起來形成一個全局單例,然后對外暴露相應的擴展和修改方法,最后只要在結算流程中通過動態傳入參數調用對應的滿減策略就可以了。從代碼風格上來看,策略模式的使用可以在一定程度上避免多重條件選擇,相關代碼的聚合度和程序的可維護性也更高。
1.4.2 為Ajax請求提供Mock數據
使用Node.js很容易創建出一個簡易的Web服務器來響應請求,在項目開發中,使用Express或Koa2,可以很方便地通過路由來分發請求,本節將以使用express-generator腳手架工具生成工程為例,對應路由的示例代碼如下(完整的實現代碼可在本章代碼倉庫中獲得):
var express = require('express'); var router = express.Router(); var Mock = require('mockjs'); //獲取訂單 router.get('/v1/query_orders', function (req, res) { var result = Mock.mock({ //此處為數據模板 }); res.send({ status:1, state:'success', message:'', result }); }); module.exports = router;
在本地Mock服務的支持下,我們可以在服務端代碼中隨時添加新的接口并生成需要的數據。測試的數據并不只是為了輔助UI開發,也可以通過在Mock服務中為某些字段賦予特別長的字符串,或是諸如null、undefined等特殊值來測試前端樣式和邏輯的健壯性。更重要的是,所有這些操作都是在前端工程以外實現的,上線時只需要在打包工具中將請求的基準地址替換為生產環境的地址即可。
當Mock.js從服務端為Ajax請求提供數據時,它只承擔了生成模擬數據的任務,而Web服務的功能是直接借助其他庫來實現的,前文已經介紹過,Mock.js也可以直接在瀏覽器環境中運行,并為請求提供虛擬數據,那么它是如何做到的呢?在前端應用中,開發者通常不會直接使用底層的Ajax對象,而是更多地使用jQuery或axios這樣的第三方庫來管理網絡請求,所以想要實現對網絡請求的攔截和修改,只能對底層Ajax對象本身的表現進行修改,畢竟你無法知道應用層的代碼到底使用了哪個請求庫,又不希望為了使用Mock數據而修改應用層的邏輯代碼。客戶端的Ajax請求是通過XMLHttpRequest的全局對象(IE瀏覽器除外)來實現的,下面的代碼演示了如何使用XMLHttpRequest的全局對象發送一個GET請求并處理它的響應:
let xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function(){ If (xmlhttp.readyState == 4 && xmlhttp.status == 200){ //虛擬的響應處理函數 handleResponse(xmlhttp.responseText); } } xmlhttp.open('GET','/try/Ajax',true); xmlhttp.send();
為了改變JavaScript中原生對象的默認行為,這里需要用到另一個經典設計模式——代理模式。
我們先從一個簡單的場景入手,假設你在代碼中編寫了一個通用的A方法,其他同事在他們編寫的B、C、D方法中都調用了這個方法。現在因為業務升級,你希望在A方法中添加一些新的擴展邏輯,如果不允許修改A方法的現有代碼,那么你要如何來實現呢?我們最容易想到的方法就是新建一個A2方法,在它的函數體中先調用一次A方法,然后再加入新增的邏輯,這樣的確可以達到上述要求,但你也不得不將B、C、D方法中調用的A方法逐個修改為A2方法,所以在代碼實現上最好能夠對調一下A和A2的函數名,這樣對于上層的B、C、D而言,它們調用的仍然是A方法,而A方法的實現其實已經更換成了新的函數,新函數通過調用A2方法(即原來的A方法邏輯)保留了之前的邏輯,然后執行自己函數體中新增加的邏輯,這樣就完成了A方法的功能擴展。其實,JavaScript中代理模式的實現也是這樣一個過程,有趣的是,很多認為自己無法理解代理模式的開發人員,都可以依靠自己的思維完成上面的推演過程。
拓展知識
代理模式也稱為Proxy模式,有時還被稱為“劫持”模式,是前端使用率較高的經典設計模式中的一種,其目的是為其他對象提供一種代理機制,以控制對這個對象的訪問。代理模式使用代理對象來控制具體對象的引用,代理對象幾乎可以是任何對象:文件、資源、內存中的對象,或者是一些難以復制的東西。例如生活中的房屋中介,可以代表賣家把房子賣給買家,這中間賣家提出期望的價錢,買家也可以提出自己心儀的戶型和預算,房產中介可以幫忙處理各類中間環節,從而促成交易,雙方都只面對中介開展業務。從買家的視角來看,房產中介就相當于賣家的代理,盡管他不是賣家本身,但是在交易活動中可以代表賣家。
下面我們來看一段經典的應用“代理模式”的實例代碼,Vue2中為了能夠讓數組具備“響應式”的特點,對典型的數組變異方法(指方法運行后會影響原數組的方法)進行了代理,代碼如下(引用自Vue官方代碼倉庫src/core/observer/array.js[3]):
const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * 插入代理方法并發送事件通知 */ methodsToPatch.forEach(function (method) { // 緩存原方法 const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 將變化通知給監聽者 ob.dep.notify() return result }) })
下面就來簡化一下上述代碼,只保留它的核心結構,簡化后的代碼如下:
methodsToPatch.forEach(function (method) { //保存舊方法 const original = arrayProto[method] //定義新方法 def(arrayMethods, method, function mutator (...args) { //執行舊方法 const result = original.apply(this, args) //新增邏輯,用于增加響應式特性 //... return result; }) })
由示例代碼可以看出,原生的方法被保存在original上(也就是將原生方法更為original),而原生方法名則在def方法中指向了新的函數,它的實現邏輯與我們之前的猜想是一致的。
了解了“代理模式”之后,再來看看如何在客戶端得到Mock數據。當開發人員在程序中使用第三方庫來處理Ajax請求時,實際上已經對原生XMLHttpRequest進行了代理,如果只是希望達到修改響應數據的目的,則完全可以利用第三方庫提供的攔截器或是類似的機制(例如,axios庫提供的interceptors機制)。在響應攔截器中,可以使用Mock.js生成模擬數據,但這樣做的同時,你的庫也會與其他庫產生耦合,這并不是我們希望的結果。為了讓自己的Mock代理程序能夠“無侵入”地與其他請求庫進行協作,我們需要從底層對整個XMLHttpRequest對象進行代理,這樣無論應用層使用哪個庫來進行網絡請求,只要在請求過程中使用XMLHttpRequest,就會用到Mock程序。對于應用層方法來說,并不需要區分響應數據的來源,直觀上來看其實就是進行了兩次代理。具體的代碼實現可以通過閱讀Mock.js源碼[4]來學習,它對整個XMLHttpRequest對象進行了模擬,并在注釋中詳細描述了標準規范、實現思路和參考資料,本章就不對此展開講解了。
[1]官網地址:http://rap2.taobao.org/。
[2]官網地址:http://json-schema.org/。
[3]代碼倉庫地址:https://github.com/vuejs/vue/blob/dev/src/core/observer/array.js。
[4]參考地址:https://github.com/nuysoft/Mock/blob/refactoring/src/mock/xhr/xhr.js。
- 微服務設計(第2版)
- The DevOps 2.3 Toolkit
- Learning RxJava
- NLTK基礎教程:用NLTK和Python庫構建機器學習應用
- Getting Started with LLVM Core Libraries
- 鴻蒙OS應用編程實戰
- Java程序設計教程
- 大學計算機應用基礎(Windows 7+Office 2010)(IC3)
- SFML Game Development
- 數據結構與算法詳解
- Visual C++ 開發從入門到精通
- ServiceDesk Plus 8.x Essentials
- 程序員超強大腦
- Python游戲編程項目開發實戰
- Python深度學習實戰:基于TensorFlow和Keras的聊天機器人以及人臉、物體和語音識別