前言
最近一直在搗鼓畢設,準備做的是一個基於前後端開發的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 拼接。其實用的最終核心思路還是一樣的。