來,我們手寫一個簡易版的mock.js吧(模擬fetch && Ajax請求)

  • 2019 年 11 月 13 日
  • 筆記

預期的mock的使用方式

首先我們從使用的角度出發,思考編碼過程

  • M1. 通過配置文件配置url和response

  • M2. 自動檢測環境為開發環境時啟動Mock.js

  • M3. mock程式碼能直接覆蓋global.fetch方法或者XMLHttpRequest構造函數,實現開發無感知

  • M4. mock配置不影響實際的請求,可無縫切換為實際請求

M1. 通過配置文件配置url和response

比較符合我們使用習慣的,也許是下面這種mock方式,有一個專門的配置文件,管理請求的url和返回值。每個請求對應輸出數組中的一個對象,對象的rule屬性可以是一個字元串或者一個正則表達式,用來匹配url,對象的res屬性則是我們希望的從中請求中拿到的返回的數據 (也許這裡面還應該加個type表示請求的類型,但是我這個是mock的最簡化版,所以就不加了)

// api.js  module.exports = [    {      rule: '/mock',      res: {        a: 'data',        b: [{c: 1}, {d: 1}],      },    },    {      rule: '/mock2',      res: {        j: {           k: 'XXX'        },      },    },  ];

 

M2. 自動檢測環境為開發環境時啟動Mock.js

// __DEV__ 可能是webpack等配置的全局變數  if (__DEV__) {    require ('./ajaxMock.js');    require ('./fetchMock.js');  }

 

M3. mock程式碼能直接覆蓋global.fetch方法或者XMLHttpRequest構造函數,實現開發無感知

// fetchMock.js  window.fetch = function (url) {      // 覆蓋默認fetch  }  // ajaxMock.js  class XMLHttpRequest {   // ...覆蓋默認XHR  }  window.XMLHttpRequest = XMLHttpRequest;

 

M4.mock配置不影響實際的請求,可無縫切換為實際請求

mock配置不影響實際的請求,當請求沒有命中mock配置文件中的url時,自動切換為實際請求,例如

// fetch  window.fetch = (url, cfg) => {    if (命中config文件中的url) {        // 覆蓋默認fetch    } else {        return originFetch (url, cfg);    }  };  // Ajax  const RealXHR = window.XMLHttpRequest;  class XMLHttpRequest {    open (type, url, bool) {      if (命中config文件中的url) {        // 覆蓋Ajax      } else {        // 使用系統原有的Ajax        this.xhr = new RealXHR ();        this.xhr.open (type, url, bool);      }    }    send (args) {      if (命中config文件中的url) {        // 覆蓋Ajax      } else {        // 使用系統原有的Ajax        this.xhr.send (args);      }    }  }  window.XMLHttpRequest = XMLHttpRequest;

 

模擬fetch

直接上程式碼

// 保存系統原生的fetch  const originFetch = window.fetch;    // 根據fetch的要求返回的response  const normalize = resp => {    return {      ok: true,      status: 200,      text () {        return Promise.resolve (resp);      },      json () {        return Promise.resolve (resp);      },    };  };    // 覆蓋fetch  window.fetch = (url, cfg) => {    // url所對應的JSON對象    let res;    // 表示是否config文件中是否有和url對應的配置    let hit = false;    // 遍歷配置文件中輸出的數組,檢測並嘗試獲取匹配url的res對象    fakeApi.forEach (item => {      let rule = item.rule;      if (typeof rule === 'string') {        rule = new RegExp (rule);      }      if (rule && rule.test (url)) {        res = item.res;        hit = true;        return false;      }    });    // 如果命中,那麼返回一個Promise,並且傳遞上面和url匹配的JSON對象    if (hit) {      return new Promise (resolve => {        setTimeout (() => {          resolve (normalize (res));        }, 1000);      });    }    // 如果沒有命中,那麼使用系統原有的fetch的API,實現無縫切換    return originFetch (url, cfg);  };

 

 

模擬ajax

直接上程式碼

// 保存系統原生的XMLHttpRequest對象  const RealXHR = window.XMLHttpRequest;    class XMLHttpRequest {    constructor () {      this.url = null;      this.type = null;      this.hit = false;      // 真實的xhr      this.xhr = null;    }    open (type, url, bool) {      // 遍歷配置文件中輸出的數組,檢測並嘗試獲取匹配url的res對象      fakeApi.forEach (item => {        let rule = item.rule;        if (typeof rule === 'string') {          rule = new RegExp (rule);        }        if (rule && rule.test (url)) {          this.res = item.res;          this.hit = true;          return false;        }      });      // 如果沒有命中,那麼使用系統原有的Ajax的API,實現無縫切換      if (!this.hit) {        this.xhr = new RealXHR ();        this.xhr.open (type, url, bool);      }    }    send (args) {      // 如果命中,就覆蓋Ajax的API      if (this.hit && this.onreadystatechange) {        this.readyState = 4;        this.status = 200;        this.responseText = JSON.stringify (this.res);        this.onreadystatechange ();      } else {        // 如果沒有命中,那麼使用系統原有的Ajax的API,實現無縫切換        this.xhr.send (args);      }    }  }  // 覆蓋  window.XMLHttpRequest = XMLHttpRequest;

  

測試

配置文件 

export default [    {      rule: '/mock',      res: {        a: 'data',        b: [{c: 1}, {d: 1}],      },    }  ];

 

測試程式碼

const xhr = new XMLHttpRequest ();  xhr.onreadystatechange = function () {    if (xhr.readyState === 4 && xhr.status === 200) {      console.log (JSON.parse (xhr.responseText));    }  };  xhr.open ('GET', '/mock');  xhr.send ();

 

測試結果

 

額外擴展

除了上面的功能外,我們還能做什麼?

  • 加個type類型,區分同一url下的不同請求類型,例如get,post

  • 加個布爾值err,表示失敗的請求

上面這兩個功能再做了我覺得就已經很足夠了,當然,如果你還不滿足,那你還可以嘗試:

  • 處理xhr.open的第三個參數:async值,控制同步和非同步

  • 處理xhr的progress,load,error,abort等事件監聽

  • 處理fetch返回的response的其他方法,例如Body.formData()等等

再談mock.js

早在之前我就寫過一篇關於mock.js的文章。這個庫目前在github是13k, 當然我覺得這個庫是很強大的,因為它覆蓋了從名字,地名,文章甚至是圖片資源的mock數據,但是在實際使用中卻多少有那麼一點點“雞肋”的感覺,為什麼我會有這樣一種感覺呢

這是因為它有一套自己的獨立的模板語法,以及API,需要你學習和遵循 

// 模擬JSON數據  Mock.mock({    "array|1-10": [      "Hello",      "Mock.js",      "!"    ]  })  // 模擬大段的文章或句子  Random.paragraph( min?, max? )

 

當然mock.js有它自己的好處,例如:

  • 當你需要動態地造大數據量的mock數據的時候很方便,例如mock.js的Random.paragraph的API能很方便的幫你造出來

  • 當你有一些特殊的需求點的時候,例如一個長度寬度變化的圖片的時候,mock.js也可以很強大的勝任Random.image( size?, background?)

  • 造出來的數據看起來“很漂亮很真實”,單純看完全發現不了是假的數據

 

但問題在於,我在實際的開發中發現,我們大多數的數據場景根本就沒這麼複雜

我們大多數時候需要的僅僅只是:寫一個響應數據的模版,例如一個json文件,然後使得發一個請求過去的時候能在ajax的onreadystatechange或者fetch(url).then中拿到數據就可以了

如果符合我們預期的mock的“完美需求”是100%的話

mock.js這個社區應用實現了80%到99%的需求的過程

但是它的使用方式卻額外增加了30% ~ 40%的成本,

因為,我們大多數時候也許不太需要這麼多的模板和“看起來很漂亮的數據”

這是我寫這個簡易版的mock的實現的原因


才疏學淺,還多指教,本文完