Vue中的RouteMock實現思路及其問題

前言: 在實際的開發過程當中,前、後端仍然存在過程式子的耦合,即前端人員過分的依賴服務端所提供的數據。這不只會耽誤前端開發的進度,也容易在聯調過程當中致使一些沒必要要的麻煩。所謂,先後端徹底分離,就是要提供一種機制讓前端開發人員再也不依賴服務端的數據,而是接口。

情景再現

在實際的項目中,前端開發人員每每會遇到如下的問題而致使前端進度的緩慢:javascript

  1. 服務端人員由於緩慢(啓動服務耗時較長)或某些流程限制(例如全部人都寫完了統一發布),致使發佈服務數據的進度和時間變得緩慢;
  2. 在項目聯調過程當中,未能及時的處理服務端數據異常(例如timeout超時、404錯誤等)的某些狀況,致使工做量又被追加;
  3. 某些特殊場景下,先後端開發人員的網絡環境有差別(例如遠程辦公、離線開發模式等);

致使這種現象的根本緣由就是前端過分依賴服務端,爲了能解開這種高密度的耦合,咱們首先是要創建公約制接口,而後按照接口進行離線開發。這樣,咱們能夠先按照預先的接口進行模擬,待到雙方條件具有時,按照接口進行聯調測試,就方便許多。前端

實現方式

要想讓前端隔離於服務端,就要在前端去作數據模擬——Mock。數據模擬的方式有不少,大體能夠分爲三類:vue

  1. 代碼注入,即請求接口後,在fail或者catch中模擬數據進行操做;
  2. 攔截注入,即攔截ajax請求後,將模擬數據返回;
  3. 代理服務,即構建MockServer後,用代理方式將本地請求指向代理服務器獲取模擬數據;

第一種方式是不值得提倡的,由於這樣作將破壞ajax原有功能,也會造成許多垃圾代碼,致使後期維護難度大,例如:java

Axios.get('/api/users')
       .then(/* ... */)
       .catch(err => {
           const data = { /* ...*/ };
           /* 對data的處理 */
         })

catch真正應該構建的不該該是數據模擬,而是對頁面上交互流程的處理,例如在數據請求失敗時,給予用戶相應的提示。webpack

對於第三種方式,也是比較容易實現的,在webpack中配置一下代理便可,而後請求時,將原有接口改成代理連接便可。如今,各類第三方MockServer已經很是成熟,但卻依賴緩落環境,不適合離線開發的場景。若是本身有搭建服務器的能力,也能夠搭建本地MockServer,但其使用成本沒有第二種來的方便。ios

最後來講說第二種方式,這也是我我的最傾向的一種方式。在vue體系中,最多見的就是使用MockJS來攔截Axios的請求。但在實際使用過程當中,語法上仍然是比較晦澀的,咱們來看看使用原生的MockJS是如何構建攔截服務的:git

import Mock from 'mockjs';

// 攔截請求-動態路由 get /api/user/:id
Mock.mock(/\/api\/user\/[0-9a-zA-Z]{8}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{4}-[0-9a-zA-Z]{12}/, 'GET', options => {
  return { /* 返回的模擬數據 */};
});

// 攔截請求-參數化路由 get /api/users?
Mock.mock(/\/api\/user+(\?{0,1}(([A-Za-z0-9-~]+\={0,1})([A-Za-z0-9-~]*)\&{0,1})*)$/, GET, options => {
  return { /* 返回的模擬數據 */};
});

看看這樣的代碼,是否是發現特別的晦澀難懂,並且還不太容易維護,開發人員要手寫一段更復雜的mock也會很容易寫錯。架構的本質是讓複雜的事情變得簡單,所以,在此基礎上,咱們須要多Mock進行改進。github

RouteMock的實現思路

我一直在思考一個問題,爲何Mock的寫法不能和AJAX的寫法一一對應呢?AJAX能夠採用RESTFUL的方式來書寫,Mock也應當按照這種方式來書寫。構成這一想法的啓蒙框架就是EXPRESS,EXPRESS提供了RESTFUL方式的路由,方便開發者將項目按照路由地址進行模塊化。所以,咱們就須要有以下的指望:web

// AJAX請求
Axios.get('/api/user/10000')
         .then(res => { /* 處理 res.data */});
// Mock模擬
Mock.get('/api/user/:id', id => {
  return { /* 返回數據 data */ };
})

看看這樣的指望,AJAX發送請求後,能拿到Mock返回的數據data。於此同時,Mock模擬的數據能攔截全部諸如/api/user/:id的請求,這裏佔位符拿到的數據爲id = 10000,而且在Mock的回調函數中咱們還能夠作動態處理。ajax

經過編碼,咱們對Mock進行相應的擴展,其最終功能以下:

// 按照數據模板返回模擬數據
Mock.mock({ /* MockJS template */ });

// 語義化RESTFUL請求模擬
Mock.get('/api/users', () => {});
Mock.post('/api/user', () => {});

// 動態路由模擬
Mock.get('/api/user/:id', id => {});

// get請求參數攔截
Mock.get('/api/user/:id', (id, params) => {});

// post請求參數攔截
Mock.post('/api/user', data => {});

// 同、異步返回
Mock.get('/api/user/:id', id => {
  return { /* 返回數據 data */ };
});
Mock.get('/api/user/:id', id => {
  return Promise.resolve({ /* 返回數據 data */ });
});

// 延時返回數據, 4秒後傳回數據
Mock.get('/api/users', () => {
  return { /* 返回數據 data */ };
}, {timeout: 4000});

// 異常code返回
Mock.get('/api/users', () => {
  return { /* 返回數據 data */ };
}, {code: 500});

經過這樣的封裝,咱們基本已經能夠快速模擬客戶端的全部請求,並模擬大部分可能性狀況對客戶端作出響應。在實現過程當中,作了兩個特別的處理:

  1. get請求和post請求的回調函數裏有兩種參數:佔位符參數和請求參數。佔位符參數即URL定義中的:id,請求參數分爲data和params,data參數針對post請求,params針對get請求,每一種請求只能攔截一種。佔位符參數永遠優先與請求參數,且data參數和params不會共存。
  2. 增長了支持promise的返回,至於爲何要支持異步返回,是爲了要配合前端數據庫。由於僅僅依靠RouteMock模塊只能模擬數據的狀態,不能模擬數據的流向,業務是流動的,業務的流動必然會致使數據的變化。而對於客戶端來講,沒有什麼是比數據庫操做更加便捷的方式了,所以咱們的架構體系中是擁有前端數據庫的,以便於開發人員能快速的實現數據的CRUD。而且咱們支持多種前端數據庫,大部分能夠連接到IndexDB、WebSQL,此時RouteMock對異步返回的數據有很好的支持的。

功能代碼

啥也不說,咱們上代碼:

// EXPRESS所用到的路徑轉正則庫
import pathTo from 'path-to-regexp';
// https://github.com/ctimmerm/axios-mock-adapter
import MockAdapter from 'axios-mock-adapter';
import Mock from 'mockjs';
// 封裝的AJAX庫
import { instance } from '@/lib/ajax';
import url from 'url';
import qs from 'qs';

// 實例化數據模擬器
const AdapterMock = new MockAdapter(instance);
const mock = {
  mock: template => Mock.mock(template)
};
const httpMethods = ['get', 'post', 'patch', 'put', 'delete', 'head', 'options'];

httpMethods.forEach((type) => {
  const mockMethod = 'on' + type.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
  mock[type] = function (urlReqExpString, callback, {code = 200, timeout = 0} = {}) {
    // 將url匹配表達式進行轉換, 例如: /path/:id
    const rurl = pathTo(urlReqExpString, [], {
      // 不忽略大小寫
      sensitive: true,
      strict: true
    });
    // 對get請求進行甄別,由於get請求有`?`參數
    if (type === 'get') {
      // 將url正則兩邊的`/`去掉,並追加`?`參數的正則校驗
      const urlString = rurl
        .toString()
        .slice(1, -1)
        .replace('$', '\\/{0,1}(\\?{0}|\\?{1}((\\S+\\={1})(\\S+)\\&{0,1})*)$');
      // 從新編譯正則規則
      rurl.compile(urlString);
    }
    AdapterMock[mockMethod](rurl).reply(async config => {
      const urlSchema = url.parse(config.url);
      // 攔截的佔位符參數,去除`?`校驗所帶的四組括號,以及第一項url
      const argsArr = rurl.exec(urlSchema.pathname);
      const args = argsArr.slice(1, type === 'get' ? argsArr.length - 4 : argsArr.length);
      // get 提交參數
      const params = config.params || qs.parse(urlSchema.query);
      // post 提交參數
      let datas;
      try {
        // 對報文進行JSON轉換,若是轉換失敗,說明不是JSON格式,直接返回
        datas = JSON.parse(config.data);
      } catch (e) {
        datas = config.data;
      }
      args.push(type === 'get' ? params : datas);
      const result = Mock.mock(await callback.call(config, ...args));
      // return result === undefined ? {} : result;
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve([code, result]);
        }, timeout);
      });
    });
  };
});

export default mock;

問題及思考

雖然模塊的構建已經完成,然而在推廣的時候遭遇到巨大的阻力,這也反映了Mock自己所帶來的問題——功能雞肋。由於開發過程當中,某些接口並非一開始就定義好的,即使定義好了也會有修改的可能。一旦有所變化,其代碼修改的工做量也是挺大的,模擬數據的地方和接入數據的地方都要改動。

咱們將開發過程分爲五個模式:

  1. 本地開發模式,注入RouteMock模塊,由該路由攔截本地請求響應數據;
  2. 本地聯調模式,在dev生成的網頁中更改配置,切換爲聯調模式,將經過代理方式訪問測試服務器或其餘服務器數據;
  3. 生產演示模式,該模式下mock部分依然注入,但優先鏈接生產測試庫,若數據服務中斷則自動切換爲離線mock數據,在一些網絡很差的發佈、演示場景很是有用;
  4. 生產測試模式,無代理、無mock模式;
  5. 生產模式,同上;

對於本地開發模式,其實用性仍然比較強的,而且對於本地聯調和生產演示都是可複用的,改動量不大的。而接口的改動的確在所不免,這在開發過程當中須要明確以及協調。前端是一個工做量相對於服務端要大的多的工做環境,除了業務邏輯、界面樣式、交互流程,還要等待服務端數據進行實時對接,下降這總服務間的耦合,可讓前端將關注點真正放在前端工程的構建上,而不是數據。

相關文章
相關標籤/搜索