专题背景
系列文章
【DoKit&北大专题】缘起
【DoKit&北大专题】-读小程序源代码(一)
【DoKit&北大专题】-读小程序源代码(二)
【DoKit&北大专题】-读小程序源代码(三)
【DoKit&北大专题】-实现DoKit For Web请求捕获工具(一)产品调研
【DoKit&北大专题】-DoKit For 小程序源码分析
【DoKit&北大专题】-浅谈滴滴Dokit业务代码零侵入思想(小程序端)
原文
本文要点:
- 了解DoKit小程序端业务代码零侵入的思想
- 了解关于位置模拟、请求注射、ApiMock功能中业务代码零侵入的具体实现
一、前言
1.1 DoKit组件功能的简要分类
在之前的前端初学者读滴滴DoKit小程序源代码系列文章中,我们介绍了微信小程序端的基本语法、特色功能如事件绑定、条件渲染、列表渲染、事件通信等内容。我们也简要的分析了Dokit的两个组件:index组件与debug组件。到目前为止我们已经基本掌握了分析DoKit小程序端源码的基本知识,之后只要按着类似的分析方法,结合响应的微信小程序API与JavaScript语法即可逐个解读,了解各个功能具体的实现方式。因此,这次我们跳出具体的某个组件。
宏观的看DoKit的组件功能,可以分为两种:
- 组件自身对业务代码的输出不产生或很少产生影响,其目的为快速查看某些信息,代表组件为App信息、缓存管理、H5任意门等。
- 组件自身对业务代码的输出产生了影响,其目的为测试业务代码的输出结果/模拟用户的输入,代表组件为位置模拟、请求注射、ApiMock等。
在微信小程序端的DoKit组件中第一类组件主要是通过对系统接口函数的封装来实现的,而第二类组件便是基于业务代码零侵入的思想,通过改写系统接口函数来实现的。
本文就来浅谈一下关于DoKit业务代码零侵入的思想。
1.2 什么是业务代码零侵入
我们先来看一个简单的应用场景:假设我的APP有一个与位置有关的功能,代码如下:
wx.getLocation({
type: 'gcj02',
success (res) {
const latitude = res.latitude
const longitude = res.longitude
...
//业务逻辑
...
}
})
现在我想测试当地理位置为上海某个具体位置时的该功能输出结果,如果我通过修改代码来实现:
Dump.getLocation(ShanghaiPos,{
type: 'gcj02',
success (res) {
const latitude = res.latitude
const longitude = res.longitude
...
//业务逻辑
...
}
})
那么这样显而易见的会有一个测试问题:每次测试后都要重新修改源代码,十分麻烦而且也不利于调试,每次只模拟一个情况就要重新修改代码,重新编译,效率十分低下。
这种测试方法显然不是我们所希望的,我们希望的是在不修改源代码的前提下测试源代码,而Dokit中的位置模拟模块就满足了这种需求:我们只需要打开该组件,选择好自己想模拟的位置,即可进行测试,无需修改我们自己的业务代码,大幅度提高测试效率。
也就是说,我们需要一种技术方案,这种方案能够使开发人员不修改自己的源代码即可进行相应的测试,这种思想就被称为“业务代码零侵入”。
二、技术实现
2.1 技术核心:Object.defineProperty
如果用两个字来描述的话,那就是:拦截
将原生API拦截,让开发人员调用API时调用修改后的API。
Dokit小程序端实现业务代码零侵入的主体思路是利用JavaScript中的静态方法Object.defineProperty
对微信提供的接口API进行相应的改写,使得测试过程中用户自己的业务代码不受到影响。
关于该函数的具体介绍可以参考相关文档 ,在Dokit的主要有以下两种使用方式:
- 为接口API设置getter函数,当用户调用该接口时,会调用Dokit设置好的
get
函数,来影响业务代码的输出。 - 设置接口API的属性描述符
writable:true
,使这个接口的API能被赋值运算符进行改变,再通过将该接口修改为Dokit设置的函数,使用户再调用该接口时调用该函数,来影响业务代码的输出。
2.2 位置模拟中的相关实现
位置模拟的关键就是拦截wx.getLocation
接口,将该接口原先返回的实际地理位置改成需要的地理位置,具体代码如下:
choosePosition (){
wx.chooseLocation({
success: res => {
this.setData({ currentLatitude: res.latitude });
this.setData({ currentLongitude: res.longitude })
Object.defineProperty(wx, 'getLocation', {
get() {
return function (obj) {
obj.success({latitude: res.latitude, longitude: res.longitude})
}
}
})
}
})
}
可以看到,位置模拟的实现方式是先调用wx.chooseLocation
接口选择好想模拟的位置,之后通过Object.defineProperty
方法设置了wx.getLocation
接口的get
函数,将本来应该返回的实际地理位置信息对象改为想模拟的地理位置。
还原位置的方法很简单,再次使用Object.defineProperty
方法将wx.getLocation
接口的get
函数设定为之前挂载(保存)在app实例上的原生接口函数,这样等用户再调用该接口的时候就会调用原生接口函数,具体代码如下:
resetPosition (){
Object.defineProperty(wx, 'getLocation',
{
get() {
return app.originGetLocation
}
});
wx.showToast({title:'还原成功!'})
this.getMyPosition()
}
之后提及到的拦截接口还原方式都是类似的,将接口再设定为之前挂载在app实例上的的原生接口函数,不再赘述。
2.3 请求注射中的相关实现
请求注射的关键就是拦截wx.request
接口,将接收到的数据实现进行注射修改,再传给业务代码使用,具体代码如下:
hooksRequest() {
Object.defineProperty(wx, "request" , { writable: true });
const hooksRequestSuccessCallback = this.hooksRequestSuccessCallback
wx.request = function(options){
const originSuccessCallback = options.success
options.success = res => {
originSuccessCallback(hooksRequestSuccessCallback(res))
}
app.originRequest(options)
}
}
可以看到,请求注射的实现方式是通过Object.defineProperty
方法将wx.request
的writable
属性修改为true
,之后重写该接口,将原来options对象中的success
回调函数得到的正常response响应对象通过hooksRequestSuccessCallback()
函数进行注射,再执行原来的网络请求。这样就可以实现业务代码接收到的response对象为注射后的对象。
hooksRequestSuccessCallback()
函数的用途是根据用户填入Dokit中的注射列表来进行相应的key-value键值对的属性修改,详细的逻辑可以参考源代码。
2.4 APImock中的相关实现
与请求注射相同,APImock的关键也是拦截wx.request
接口,若当前网络请求的网址路径在用户Dokit平台端的mock列表中,则进行接口mock:将当前请求拦截,给应用端返回一个Dokit平台端模拟的服务器响应。
APImock组件可能是Dokit小程序端中实现最复杂的一个组件,所以我们来详细分析一下APImock的实现代码,这里先上一个Dokit官方提供的逻辑流程图:
比我自己写的流程图好多了哈
根据流程图添加注释后的代码如下:
addRequestHooks () {
Object.defineProperty(wx, "request" , { writable: true });//拦截wx.request方法
console.group('addRequestHooks success')
const matchUrlRequest = this.matchUrlRequest.bind(this)
const matchUrlTpl = this.matchUrlTpl.bind(this)
wx.request = function (options) { //重写接口函数
const opt = util.deepClone(options)
const originSuccessFn = options.success //保存业务代码中的success回调函数
const sceneId = matchUrlRequest(options) //判断是否满足命中规则
if (sceneId) {
options.url = `${mockBaseUrl}/api/app/scene/${sceneId}`
console.group('request options', options)
console.warn('被拦截了~')
}
options.success = function (res) {
originSuccessFn(matchUrlTpl(opt, res)) //匹配模版规则
}
app.originRequest(options)
}
}
重写的wx.request
接口中先做的事情就是对接口参数options
进行了深拷贝,便于之后上传模版数据,之后通过matchUrlRequest()
函数来判断当前网络请求是否命中拦截规则。我们接下来来看看具体的拦截规则是什么:
matchUrlRequest (options) {
let flag = false, curMockItem, sceneId;
if (!this.data.mockList.length) { return false }
for (let i = 0,len = this.data.mockList.length; i < len; i++) {
curMockItem = this.data.mockList[i]
if (this.requestIsmatch(options, curMockItem)) {
flag = true
break;
}
}
if (curMockItem.sceneList && curMockItem.sceneList.length) {
for (let j=0,jLen=curMockItem.sceneList.length; j<jLen; j++) {
const curSceneItem = curMockItem.sceneList[j]
if (curSceneItem.checked) {
sceneId = curSceneItem._id
break;
}
}
} else {
sceneId = false
}
return flag && curMockItem.checked && sceneId
}
函数中先遍历了用户的mockList
列表查找是否有匹配当前请求的mock响应,如果有匹配的响应(requestIsmatch
函数返回true
→flag = true
),再遍历这个响应的场景列表sceneList
查找用户选择的是什么场景,根据选择的场景来返回响应的sceneId
进一步深入,我们来看看requestIsmatch
函数判断请求是否匹配的具体逻辑:
requestIsmatch (options, mockItem) {
const path = util.getPartUrlByParam(options.url, 'path')
const query = util.getPartUrlByParam(options.url, 'query')
return this.urlMethodIsEqual(path, options.method, mockItem.path, mockItem.method) && this.requestParamsIsEqual(query, options.data, mockItem.query, mockItem.body)
}
requestIsmatch
函数实际上是封装了两个测试函数:urlMethodIsEqual
与requestParamsIsEqual
函数,分别检测请求的路径、方法和请求参数。具体代码如下:
urlMethodIsEqual (reqPath, reqMethod, mockPath, mockMethod) {
reqPath = reqPath ? `/${reqPath}` : ''
reqMethod = reqMethod || 'GET'
return (reqPath == mockPath) && (reqMethod.toUpperCase() == mockMethod.toUpperCase())
}
urlMethodIsEqual
函数判断请求的路径与请求方式(GET、POST或其他)是否与设定好的mock接口一致。
requestParamsIsEqual (reqQuery, reqBody, mockQuery, mockBody) {
reqQuery = util.search2Json(reqQuery)
reqBody = reqBody || {}
try {
return (JSON.stringify(reqQuery) == mockQuery) && (JSON.stringify(reqBody) == mockBody)
} catch (e) {
return false
}
}
requestParamsIsEqual
函数判断请求的参数是否与设定好的mock接口一致(包括Query请求体和Body请求体)
总结一下,具体的判断是否命中拦截的流程如图:
回到addRequestHooks
函数中,命中模版规则后,函数将原请求的网址url改为DoKit的相应路径${mockBaseUrl}/api/app/scene/${sceneId}
,进而返回mock接口的响应。
在这个过程中,DoKit还改写了options参数的success回调函数,用matchUrlTpl
函数来判断收到的响应是否命中模版规则,如果命中的话就将这个响应对象变成模版保存下来。具体代码如下:
matchUrlTpl (options, res) {
let curTplItem,that = this
if (!that.data.tplList.length) { return res }
for (let i=0,len=that.data.tplList.length;i<len;i++) {
curTplItem = that.data.tplList[i]
if (that.requestIsmatch(options, curTplItem) && curTplItem.checked && res.statusCode == 200) {
that.data.tplList[i].templateData = res.data
}
}
wx.setStorageSync('dokit-tpllist', that.data.tplList)
return res
}
模版规则相比拦截规则要简单一些:先利用requestIsmatch
函数判断当前请求是否与模版列表TplList
匹配,如果匹配且响应成功(curTplItem.checked && res.statusCode == 200
),就将其保存下来(wx.setStorageSync
)等待用户的浏览与上传。
在改写的接口函数最后,执行原生接口函数app.originRequest
。整个拦截改写接口流程结束。
在APIMock功能组件的实现中,DoKit利用Object.defineProperty
方法改写request接口,不仅不需要修改业务代码中接口函数的调用,而且对url参数的重写,甚至连业务代码中请求的url
参数都不需要改变,真正的实现了“业务代码零侵入”。
三、总结
本篇文章通过对DoKit小程序端三个组件位置模拟、请求注射、APImock的主体实现的相关代码阅读了解了DoKit“业务代码零侵入”的思想。
在阅读源码的过程中,我们不仅是要简单的阅读某个组件是如何实现的,也要了解DoKit的宏观设计思路,更重要的是了解这种“发现业务痛点→针对性的提出解决方案→最终技术实现”的流程。
说了这么多也只是本人的一点浅显的理解,权当抛砖引玉,如有错误或疏漏还望批评指教。
作者信息
作者:亦庄亦谐
原文链接:juejin.cn/post/695587…
来源:掘金
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!