前言
最近一直在搗鼓畢設,準備做的是一個基于前后端開發的Mock平臺,前期花了很多時間完成了功能模塊的交互。現在進度推到如何設計核心功能,也就是Mock數據的解析。
根據之前的需求設定加上一些思考,用戶可以像寫json一般輕松完成數據的mock,也可以通過在mock數據模型之上進行構建出復雜的數據模型并在項目中引用。
這看似簡單的需求其實需要處理幾個不同的模塊功能以及交互設計。該如何處理解析不同mock數據并進行構造?前端交互中模擬數據該如何處理?數據構造時如何加載用戶設定的數據模型?錯誤捕捉與處理?
這些都暫時沒有一個好的處理結果。因此想要完成核心功能我們需要明確需求,并且通過同類產品是如何處理的,通過閱讀它們的源碼來學習思想并加入。
明確需求
在明確該功能模塊之前我們可以通過模擬流程來明確。
用戶 -> 添加數據模型 - > 實時看到構造結構
用戶 -> 添加接口 -> 構造json格式返回參數 -> 預覽
構造json格式返回參數 不僅包含返回的正文,同時也設定了 header 和 method。
閱讀源碼
符合大部分需求的開源項目有
mock.js
easy-mock
eolinker
YAPI
DOCCLEVER
MOCK.JS篇
首先我們需要明確現階段大部門的 Mock 平臺或多或少都是受到?Mock.js?的思想或者是其增強版。
我們可以用下面簡單的 json 通過?Mock.js來構造數據:
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交互非常符合我的設定,而且作為開源項目可以從它的源碼中學到很多。
直接來看它提供接口編輯的頁面
{
? 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 端輸出和瀏覽器端拿到的數據,基本可以肯定最終輸出就是這個。
然后我們往上翻,可以看到這么一段代碼:
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 拼接。其實用的最終核心思路還是一樣的。
參考資料