從零開始打造 Mock 平臺 - 核心篇

前言

最近一直在搗鼓畢設,準備作的是一個基於先後端開發的Mock平臺,前期花了不少時間完成了功能模塊的交互。如今進度推到如何設計核心功能,也就是Mock數據的解析。javascript

根據以前的需求設定加上一些思考,用戶能夠像寫json通常輕鬆完成數據的mock,也能夠經過在mock數據模型之上進行構建出複雜的數據模型並在項目中引用。前端

這看似簡單的需求其實須要處理幾個不一樣的模塊功能以及交互設計。該如何處理解析不一樣mock數據並進行構造?前端交互中模擬數據該如何處理?數據構造時如何加載用戶設定的數據模型?錯誤捕捉與處理?java

這些都暫時沒有一個好的處理結果。所以想要完成核心功能咱們須要明確需求,而且經過同類產品是如何處理的,經過閱讀它們的源碼來學習思想並加入。node

明確需求

在明確該功能模塊以前咱們能夠經過模擬流程來明確。git

用戶 -> 添加數據模型 - > 實時看到構造結構

用戶 -> 添加接口 -> 構造json格式返回參數 -> 預覽github

構造json格式返回參數 不只包含返回的正文,同時也設定了 header 和 method。正則表達式

閱讀源碼

符合大部分需求的開源項目有redis

  1. mock.js
  2. easy-mock
  3. eolinker
  4. YAPI
  5. DOCCLEVER

MOCK.JS篇

首先咱們須要明確現階段大部門的 Mock 平臺或多或少都是受到 Mock.js 的思想或者是其加強版。數據庫

咱們能夠用下面簡單的 json 經過 Mock.js來構造數據:express

example:

{
    "status|0-1": 0, //接口狀態
    "message": "成功", //消息提示
    "data": {
        "counts":"@integer", //統計數量
        "totalSubjectType|1-4": [ //4-10意味着能夠隨機生成4-10組數據
            { 
              "subjectName|regexp": "大數據|機器學習|工具", //主題名
              "subjectType|+1": 1 //類型
            }
        ],
        "data":[
            {
                "name": "@name", //用戶名
                "cname":"@cname",
                "email": "@email", //email
                "time": "@datetime" //時間
            }
        ]}
}

返回結果

{
    "status": 0,
    "message": "成功",
    "data": {
        "counts": 2216619884890228,
        "totalSubjectType": [
            {
                "subjectNameregexp": "大數據|機器學習|工具",
                "subjectType": 1
            },
            {
                "subjectNameregexp": "大數據|機器學習|工具",
                "subjectType": 2
            },
            {
                "subjectNameregexp": "大數據|機器學習|工具",
                "subjectType": 3
            },
            {
                "subjectNameregexp": "大數據|機器學習|工具",
                "subjectType": 4
            }
        ],
        "data": [
            {
                "name": "Ruth Thompson",
                "cname": "魯克",
                "email": "z.white@young.gov",
                "time": "1985-02-06 05:45:21"
            }
        ]
    }
}

並且能夠經過其 Mock.Random.extend() 來擴展自定義佔位符.

example:

Random.extend({
    weekday: function(date) {
        var weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
        return this.pick(weekdays);
    },
    sex: function(date) {
        var sexes = ['男', '女', '中性', '未知'];
        return this.pick(sexes);
    }
});

console.log(Random.weekday());  // 結果: Saturday
console.log(Mock.mock('@weekday'));  // 結果: Tuesday
console.log(Random.sex());  // 結果: 男
console.log(Mock.mock('@sex'));  // 結果: 未知

來延伸所需進的拓展。

這個能夠將自定義數據模型先進行解析,而後經過extend將其加入。

easy-mock

easy-mock 是我參考的主要項目之一,它的UI交互很是符合個人設定,並且做爲開源項目能夠從它的源碼中學到不少。

直接來看它提供接口編輯的頁面

imgn

{
  data: {
    img: function({
      _req,
      Mock
    }) {
      return _req.body.fileName + '_' + Mock.mock('@image')
    }
  }
}

能夠從上得之它既能夠處理Mock數據模擬也能夠處理函數,並且它內部有一套能處理req的內容。

先是在源碼中找了一下,找到幾個疑似點,可是不肯定,仍是在本地裝好環境,主要是須要按照redis.而後啓動服務去打幾個斷點輸出。

根據經驗先肯定 controllers\mock.js 應該是處理數據模擬的地方。經過瀏覽源碼並分析,最終定位於 297行處的代碼

await redis.lpush('mock.count', api._id)
    if (jsonpCallback) {
      ctx.type = 'text/javascript'
      ctx.body = `${jsonpCallback}(${JSON.stringify(apiData, null, 2)})`
        .replace(/\u2028/g, '\\u2028')
        .replace(/\u2029/g, '\\u2029') // JSON parse vs eval fix. https://github.com/rack/rack-contrib/pull/37
    } else {
      ctx.body = apiData
    }

首先是看到最終返回的 apiData 。用過 koa 或者 express 都應該清楚 ctx.body 的含義。而後我在上面寫了句 console.log(apiData)

而後在瀏覽器端發送請求。看下 node 端輸出和瀏覽器端拿到的數據,基本能夠確定最終輸出就是這個。

imgn

而後咱們往上翻,能夠看到這麼一段代碼:

const vm = new VM({
        timeout: 1000,
        sandbox: {
          Mock: Mock,
          mode: api.mode,
          template: new Function(`return ${api.mode}`) // eslint-disable-line
        }
      })
      console.log('數據驗證')
      console.log(mode)
      vm.run('Mock.mock(new Function("return " + mode)())') // 數據驗證,檢測 setTimeout 等方法
      apiData = vm.run('Mock.mock(template())') // 解決正則表達式失效的問題

經過查詢瞭解到 VM 是一個沙盒,能夠運行不受信任的代碼。

大概就能瞭解 easy-mock 經過 vm 沙盒模式運行 mode 代碼解析後返回結果。

核心代碼就是 Mock.mock( template ) 這麼一句。根據數據模板生成模擬數據。

經過查文檔瞭解 template 是能夠直接內部寫函數而後執行的。

這樣解析的難度大大降低,發現原來並無特別複雜的,依舊是依賴了 Mock.js 的原生方法。

而後咱們能夠看到 easy-mock 另外一的操做就是能夠獲取 請求參數_req。也就是能夠經過如下代碼來根據請求參數返回指定數據。

{
  success: true,
  data: {
    default: "hah",
    _req: function({
      _req
    }) {
      return _req
    },
    name: function({
      _req
    }) {
      return _req.query.name || this.default
    }
  }
}

_req 一看就是從請求參數中得到的對象。

Mock.js是沒有這個對象的,咱們來找找源碼中是哪裏注入了這個對象。

仍是在 mock.js 這個文件中第234行處找到

Mock.Handler.function = function (options) {
      const mockUrl = api.url.replace(/{/g, ':').replace(/}/g, '') // /api/{user}/{id} => /api/:user/:id
      options.Mock = Mock
      options._req = ctx.request
      options._req.params = util.params(mockUrl, mockURL)
      options._req.cookies = ctx.cookies.get.bind(ctx)
      return options.template.call(options.context.currentContext, options)
    }

經過閱讀 MockJS 的源碼,瞭解到 Handler是處理數據模板的地方,打個斷點再輸出一次能夠發現實際上是在 Mock.mock(new Function("return " + mode)())' 以後傳入的參數。

options._req = ctx.request 這句代碼告訴了咱們所謂的 _req是從哪裏來的。

所以這個技術點咱們也瞭解了是怎麼作的,那麼剩下一個靈活的支持 restful 經過閱讀源碼發現其實也沒怎麼處理,只是用 pathToRegexp 進行了一次驗證。它先是在 middlewares/index.js 中 的 mockFilter 進行了路徑正則。

static mockFilter (ctx, next) {
    console.log(ctx.path)
    const pathNode = pathToRegexp('/mock/:projectId(.{24})/:mockURL*').exec(ctx.path)
    console.log(pathNode)
    if (!pathNode) ctx.throw(404)
    if (blackProjects.indexOf(pathNode[1]) !== -1) {
      ctx.body = ctx.util.refail('接口請求頻率太快,已被限制訪問')
      return
    }
    console.log('經過篩選')

    ctx.pathNode = {
      projectId: pathNode[1],
      mockURL: '/' + (pathNode[2] || '')
    }

    return next()
  }

而後經過存在 redis 裏的接口內容再進行了驗證匹配。

const { query, body } = ctx.request
    const method = ctx.method.toLowerCase()
    const jsonpCallback = query.jsonp_param_name && (query[query.jsonp_param_name] || 'callback')
    let { projectId, mockURL } = ctx.pathNode
    console.log('ctx.pathNode', ctx.pathNode)
    const redisKey = 'project:' + projectId
    let apiData, apis, api
    console.log('經過URL匹配檢驗')
    apis = await redis.get(redisKey)
    console.log(apis)
    if (apis) {
      apis = JSON.parse(apis)
      console.log('pure apis', apis)
    } else {
      apis = await MockProxy.find({ project: projectId })
      console.log('find projectId', apis)
      if (apis[0]) await redis.set(redisKey, JSON.stringify(apis), 'EX', 60 * 30)
    }

    if (apis[0] && apis[0].project.url !== '/') {
      mockURL = mockURL.replace(apis[0].project.url, '') || '/'
    }

    api = apis.filter((item) => {
      const url = item.url.replace(/{/g, ':').replace(/}/g, '') // /api/{user}/{id} => /api/:user/:id
      return item.method === method && pathToRegexp(url).test(mockURL)
    })[0]
    console.log('api',api)

    if (!api) ctx.throw(404)

基本不匹配的路徑請求都是在 item.method === method && pathToRegexp(url).test(mockURL) 這句代碼裏被攔截的。

很是優秀的代碼。通讀下來,加上斷點對其思路邏輯學到了不少。

eolinker

它的後端代碼是 PHP 的,這就略過不看了。

YAPI

它的核心後端處理代碼是在 mockServer.js

有了以前的閱讀經驗很快找處處理 Mock 數據的地方

let res;

        res = interfaceData.res_body;
        try {
            if (interfaceData.res_body_type === 'json') {
                res = mockExtra(
                    yapi.commons.json_parse(interfaceData.res_body),
                    {
                        query: ctx.request.query,
                        body: ctx.request.body,
                        params: Object.assign({}, ctx.request.query, ctx.request.body)                     
                    }
                );
                try {
                    res = Mock.mock(res);
                } catch (e) {
                    yapi.commons.log(e, 'error')
                }
            }

很是簡單粗暴的處理方法。。。

對加強功能比較好奇在, 因而在 common\mock-extra.js 裏找到了 mock(mockJSON, context) 方法。根據參數其實就能瞭解綁定上下文而後作了一些動做。這裏就不展開詳細。等以後開發的時候用到再去細讀。由於這是作了其本身的加強的Mock功能,而暫時不須要這方面的考慮。

DOClecer

這個項目是國內一個創業團隊作的,我也加入了其官方羣。雖然尚未用過。不過不妨礙閱讀其源碼瞭解思路。不過講道理這個代碼組織風格是挺糟糕的。。。

並且源碼中不止一次出現了eval... 因而放棄參考。

寫個小模塊開心一下

經過閱讀以上項目的源碼,其實主要是前三個,感受能夠完成本身想要的需求了。那麼先寫一個小的來做爲基礎模塊。

export const mock = async(ctx: any) => {
  console.log('mock')
  console.log(ctx)
  console.log(ctx.params)
  const method = ctx.request.method.toLowerCase()
  // let { projectId, mockURL } = ctx.pathNode
  // 獲取接口路徑內容
  console.log('ctx.pathNode', ctx.pathNode)
  // 匹配內容是否一致
  console.log('驗證內容中...')
  // 模擬數據
  Mock.Handler.function = function (options: any) {
    console.log('start Handle')
    options.Mock = Mock
    // 傳入 request cookies,方便使用
    options._req = ctx.request
    return options.template.call(options.context.currentContext, options)
  }
  console.log('Mock.Handler', Mock.Handler.function)
//   const testMode = `{
//     'title': 'Syntax Demo',
//     'string1|1-10': '★',
//     'string2|3': 'value',
//     'number1|+1': 100,
//     'number2|1-100': 100,
//     'number3|1-100.1-10': 1,
//     'number4|123.1-10': 1,
//     'number5|123.3': 1,
//     'number6|123.10': 1.123,
//     'boolean1|1': true,
//     'boolean2|1-2': true,
//     'object1|2-4': {
//         '110000': '北京市',
//         '120000': '天津市',
//         '130000': '河北省',
//         '140000': '山西省'
//     },
//     'object2|2': {
//         '310000': '上海市',
//         '320000': '江蘇省',
//         '330000': '浙江省',
//         '340000': '安徽省'
//     },
//     'array1|1': ['AMD', 'CMD', 'KMD', 'UMD'],
//     'array2|1-10': ['Mock.js'],
//     'array3|3': ['Mock.js'],
//     'function': function() {
//         return this.title
//     }
// }`
const testMode = `{success :true, data: { default: "hah", _req: function({ _req }) { return _req }, name: function({ _req }) { return _req.query.name || this.default }}}`
  const vm = new VM({
    timeout: 1000,
    sandbox: {
      Mock: Mock,
      mode: testMode,
      template: new Function(`return ${testMode}`)
    }
  })
  vm.run('Mock.mock(new Function("return " + mode)())') // 數據驗證,檢測 setTimeout 等方法, 順便將內部的函數執行了
  // console.log(Mock.Handler.function(new Function('return ' + testMode)()))
  const apiData = vm.run('Mock.mock(template())')
  console.log('apiData2333' , apiData)
  let result
  switch (method) {
    case 'get':
      result = success({'msg': '你調用了get方法'})
      break;
    case 'post':
      result = success({'msg': '你調用了post方法'})
      break;
    case 'put' :
      result = success({'msg': '你調用了put方法'})
      break;
    case 'patch' :
      result = success({'msg': '你調用了patch方法'})
      break;
    case 'delete' :
      result = success({'msg': '你調用了delete方法'})
      break;
    default:
      result = error()
  }
  // console.log(result)
  return ctx.body = result
}

這裏調試的遇到一些問題,主要是一開始測試的時候發現 Mock 只將規則的數據模擬出,發現 function 類型的函數都沒執行,一開始定位覺得是Mock.Handler.function 在 ts 中未執行。因而在裏面寫了一個輸出,發現的確沒有。通過各類猜測和測試,發現是模擬mode有問題。

一開始我是這麼寫的

const testcode = {
    'array1|1': ['AMD', 'CMD', 'KMD', 'UMD'],
    'array2|1-10': ['Mock.js'],
    'array3|3': ['Mock.js'],
    'function': function() {
        return this.title
    }
}

事實上應該這麼寫

const testcode = `{
    'array1|1': ['AMD', 'CMD', 'KMD', 'UMD'],
    'array2|1-10': ['Mock.js'],
    'array3|3': ['Mock.js'],
    'function': function() {
        return this.title
    }
}`

參照 easy-mock 的思路能夠實現一個基礎的 Mock數據解析器,並且能夠根據 koa 的特性同時支持 _req 的一些參數,這裏先不加進去。

如何支持自定義的數據模型也有了基本的思路,在以前沒有考慮 redis 狀況下仍是用傳統的數據庫查詢。具體實現等後期再搗鼓出來再寫出來。

結尾

經過這兩天的學習,總算把一個Mock的核心模塊該如何實現的思路給理順了。

其實不管你是用戶自定義數據,好比

{
  'user': User, // User是用戶自定義的數據類型
   'string2|3': 'value',
   'number1|+1': 100,
    _req: function({
      _req
    }) {
      return _req
    },
    name: function({
      _req
    }) {
      return _req.query.name || this.default
    }
}

仍是 Mock.js 原生的語法,你最終轉換過來須要執行的是同樣的內容,無非是在其轉換前須要作必定的處理。只有搞懂了基本的數據模擬實現,基本上你能夠將各個參數都作定製化。好比有的平臺會將用戶本身編寫的函數一塊兒和 json 拼接。其實用的最終核心思路仍是同樣的。

參考資料

Mock.js使用

mockjs官方文檔

相關文章
相關標籤/搜索