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

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

背景

項目開發過程當中,一般先定義接口格式,先後端並行開發。若是前端先開發完成,由於接口還沒有實現,只能在代碼中寫一些測試數據來測試頁面效果。這致使了測試數據侵入業務代碼,後期上線時還得刪去。如今的中大型應用一般會使用 vuexredux 等狀態管理倉庫,這種狀況下測試數據寫起來麻煩,由於可能須要修改部分邏輯來配合測試數據,後期去除時極易誤刪或遺漏改正測試邏輯。同時,這種測試方式,部分與請求相關的 bug 難以檢測出 。這致使後期聯調時前端壓力重。html

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 一致,詳見文檔vue

示例

  • 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 環境可在控制檯看到以下信息

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

具體操做可查看 demonode

相關文章
相關標籤/搜索