mockjs 實現前端非侵入式 mock 解決方案

lhmiao發表於2018-12-26

mockjs 實現前端非侵入式 mock 解決方案

背景

專案開發過程中,通常先定義介面格式,前後端並行開發。如果前端先開發完成,因為介面尚未實現,只能在程式碼中寫一些測試資料來測試頁面效果。這導致了測試資料侵入業務程式碼,後期上線時還得刪去。現在的中大型應用通常會使用 vuexredux 等狀態管理倉庫,這種情況下測試資料寫起來麻煩,因為可能需要修改部分邏輯來配合測試資料,後期去除時極易誤刪或遺漏改正測試邏輯。同時,這種測試方式,部分與請求相關的 bug 難以檢測出 。這導致後期聯調時前端壓力重。

mock 方案需解決的問題

  • mock 資料不能侵入業務程式碼
  • mock 資料和工具不能打包到生產環境的程式碼中
  • mock 資料要實現熱載入,方便除錯
  • mock 工具不能對聯調造成影響
  • mock 工具和資料對專案造成的侵徹要儘可能的小,要實現模組化可插拔

解決思路

  • 前端開發環境應進一步劃分為如下兩個
    • 前端聯調環境:不攜帶 mock 工具及資料
    • 前端 mock 環境:攜帶 mock 工具及資料,對於註冊了 mock 規則的介面返回設定好的 mock 資料,對未設定 mock 規則的介面請求正常發出
  • 可通過 node 命令列引數,動態地給 webpack 新增入口來實現是否打包 mock 工具及資料

mockjs 介紹

mockjs 是一個前端本地 mock 工具,其原理是對 XMLHttpRequest 物件進行改寫,在請求發出前如果檢測到請求的介面已註冊 mock 規則,則返回設定好的測試資料,實際並沒有發出 AJAX 請求。對於未註冊 mock 規則的請求介面,則正常發出 AJAX 請求。因為原生 mockjs 不是很方便使用,對於註冊的 mock 規則的請求介面,因為沒有發出 AJAX 請求,無法在控制檯檢測到其網路請求,造成除錯困難。除此之外,原生 mockjs 對於以字串註冊 mock 規則的介面是嚴格匹配的,這導致了 URL 帶查詢字串的請求無法匹配到,需要編寫正則來實現匹配帶查詢字串的 URL 。因此對原生 mockjs 進行改寫,使其返回測試資料時先列印到控制檯,方便除錯,同時將註冊 mock 規則的介面字串轉成能匹配查詢字串的正則。註冊 mock 規則時應使用改寫後的 mock 物件

mockjs 與專案的結合

  • 通過 node 命令列引數動態給 webpack 新增入口的程式碼如下,也可單獨配一個配置檔案來實現相同的效果
// config 為 webpack 配置物件
// 獲取命令列引數
const processArgvs = process.argv.slice(2)
// 判斷是否有 mock 引數,有則在原入口的基礎上帶上 mock 工具與資料
if (processArgvs.includes('mock')) {
  let entry = config.entry
  if (Array.isArray(entry)) {
    entry.push('./src/mock')
  } else if (typeof entry === 'object') {
    Object.keys(entry).forEach(name => {
      if (Array.isArray(entry[name])) {
        entry[name].psuh('./src/mock')
      } else {
        entry[name] = [entry[name], './src/mock']
      }
    })
  } else {
    config.entry = [entry, './src/mock']
  }
}

// 以上程式碼加在啟動 dev 環境的 webpack 配置檔案中
// 通過 npm run dev mock 來啟動前端 mock 環境
// npm run dev 啟動前端聯調環境
複製程式碼
  • mock 工具及資料在專案資料夾中位置
src
  |__ mock
  	|__ index.js // 入口檔案,註冊 mock 規則的檔案全部 import 到這裡
  	|__ utils
  	|     |__ mock.js // 改寫後的 mockjs,註冊 mock 規則應使用該物件
  	|     |__ formatOptions.js // 格式化註冊 mock 時的回撥函式的引數的函式,在 mock.js 中使用
  	|__ user.js // 按業務劃分的 mock 規則註冊檔案
        |__ business.js // 按業務劃分的 mock 規則註冊檔案
複製程式碼
  • mock.js 程式碼如下
import Mock from 'mockjs'
import formatOptions from './formatOptions'

Mock._mock = Mock.mock
Mock.mock = function (url, method, resFunc) {
  if (arguments.length === 1) {
    return this._mock(url)
  }
  if (arguments.length === 2) {
    console.error('Function Mock.mock require three params: url, method, resFunc!!!')
    return
  }
  if (arguments.length === 3) {
    let methods = ['get', 'post', 'put', 'delete']
    if (!methods.includes(method.toLowerCase())) {
      console.error('Function Mock.mock\'s second param should be get, post, put, delete!!!')
      return
    }
    if (typeof resFunc !== 'function') {
      console.error('Function Mock.mock\'s third param should be a function!!!')
      return
    }
  }
  // 將註冊的 url 轉成能匹配查詢字串的正則
  if (typeof url === 'string') {
    url = url.replace(/\//g, '\\/')
    url += '(|\\?.*)$'
    url = new RegExp(url)
  } else if (!(url instanceof RegExp)) {
    console.error('Function Mock.mock\'s first param should be a string or regexp!!!')
    return
  }
  this._mock(url, method, function (options) {
    // 格式化 options 物件
    options = formatOptions(options)
    let res = null
    try {
      res = resFunc(options)
    } catch (err) {
      res = err
    }
    // 將返回的測試資料列印到控制檯
    console.groupCollapsed(`%c${options.type.toLowerCase()} | ${options.url}`, 'color: green;')
    console.log('%cparams: ', 'color: #38f')
    console.log(options.params)
    console.log('%cresponseData: ', 'color: #38f')
    console.log(res)
    console.groupEnd()
    console.log('---------------')
    return res
  })
}

export default Mock

複製程式碼
  • formatOptions.js 程式碼如下
// qs 用於序列化表單物件
import qs from 'qs'

export default function formatOptions (options) {
  let { url, type, body } = options
  let params = null
  if (type === 'GET' || type === 'DELETE') {
    let index = url.indexOf('?')
    let paramsString = index > -1 ? url.slice(index + 1) : ''
    if (paramsString !== '') {
      params = qs.parse(paramsString)
    }
  } else {
    params = {}
    if (body instanceof FormData) {
      for (let [key, value] of body.entries()) {
        params[decodeURIComponent(key)] = decodeURIComponent(value)
      }
    } else {
      try {
        params = JSON.parse(body)
      } catch (e) {
        params = qs.parse(body)
      }
    }
  }
  if (params !== null && Object.keys(params).length === 0) {
    params = null
  }
  return { url, type, params }
}

複製程式碼

改寫後的 mockjs 用法

  • Mock.mock(url, method, resFunc)
    • url (String):需要進行 mock 的介面路徑,也支援傳入正則(但要自己考慮匹配帶查詢字串的情況)
    • method (String): 請求的型別: get , post , put , delete ,忽略大小寫
    • resFunc (Function): 生產測試資料的函式,回撥引數為一個與請求有關的 options 物件,如下
{
  url: String, // 請求的路徑
  type: String, // 請求的型別,GET, POST, PUT, DELETE
  params: Object // 請求的引數,如果是 post 和 put 請求為 body 的內容,get 和 delete 為查詢字串解析出的物件,沒有則為 null
}
複製程式碼

注:Mock.mock() 方法也支援傳入一個模板來生成隨機的測試資料,具體使用與原生 mockjs 一致,詳見文件

示例

  • mock 資料夾下的 user.js 檔案
// 當前檔案為 src/mock/user.js
import Mock from './utils/mock'

// 註冊 post 請求
Mock.mock('/api/user/login', 'post', options => {
  let { params } = options // options物件包含請求的 url,型別和攜帶的引數
  if (params.username && params.password) {
    return {
      data: '',
      code: 200,
      message: '登入成功'
    }
  } else {
    return {
      data: '',
      code: 300,
      message: '賬號或密碼未輸入'
    }
  }
})

// 註冊 get 請求
Mock.mock('/api/user/logout', 'get', options => {
  return {
    data: '',
    code: 200,
    message: '登出成功'
  }
})

// 註冊帶查詢引數的 get 請求
Mock.mock('/api/user/query', 'get', options => {
  return {
    data: options.params,
    code: 200,
    message: 'ok'
  }
})

複製程式碼
  • src/mock/index.js 資料夾
import './user'

console.log('%c前端 mock 環境啟動成功', 'color: #38f;font-weight: bold')

複製程式碼
  • 以上配置好後,開啟 mock 環境可在控制檯看到如下資訊

mockjs 實現前端非侵入式 mock 解決方案

  • 註冊了 mock 規則的請求發出後可在控制檯看到

mockjs 實現前端非侵入式 mock 解決方案

具體操作可檢視 demo

相關文章