专题背景
近几年随着开源在国内的蓬勃发展,一些高校也开始探索让开源走进校园,让同学们在学生时期就感受到开源的魅力,这也是高校和国内的头部互联网企业共同尝试的全新教学模式。本专题会记录这段时间内学生们的学习成果。
更多专题背景参考:【DoKit&北大专题】缘起
系列文章
【DoKit&北大专题】-实现DoKit For Web请求捕获工具(一)产品调研
【DoKit&北大专题】-DoKit For 小程序源码分析
原文
DoKit简介
DoKit是一款面向泛前端产品研发全生命周期的效率平台,其作为DiDi旗下Stars最多的项目,现已拥有17k+Stars。Github地址
本文的目的是对DoKit小程序方向的源码进行分析。
项目引入及使用
快速上手
Mock平台
注意:读源码或对DoKit进行开发时用的是src/,使用DoKit时应该引入dist/
src和dist目录的区别:src是源代码,dist是将src里的js文件打包输出后的
写在前面
关于DoKit for miniapp
的整体设计思想,在这里放两张DoKit老师们的PPT。
我觉得“在框架中跳舞”这句话描述得非常恰当,小程序依托于微信客户端,在开发过程中有着诸多限制,如果想要开发一些辅助功能,大概率是通过对微信小程序官方API进行一定的改造来实现,下文对源码的分析中也一直在体现这种思想。
DoKit项目结构
“
assets
——资源文件夹,目前存放了一些图标文件
components
——Dokit最核心的部分,包括了八个自定义组件
- apimock —— 数据Mock功能组件
- appinformation —— App信息查看功能组件
- back —— 用来返回的自定义组件,该组件并不是Dokit的功能组件,在其他功能页面通过该组件进行返回
- debug —— 主菜单组件,罗列了Dokit的各种功能
- h5door —— h5任意门功能组件
- httpinjector ——请求注射功能组件
- positionsimulation —— 位置模拟功能组件
- storage —— 存储管理功能组件
index
—— Dokit入口的自定义组件,将Dokit引入项目中时,就是在目标page页面中引入这个component
logs
—— 与微信小程序样例项目中的logs内容
相同,貌似没什么用
utils
——imgbase64.js
将Dokit各种图标转换成base64格式;util.js
内存储了一些常用工具函数,包括时间输出、跳转页面、深拷贝等
参考文章:juejin.cn/post/694807…,亦庄亦谐
index组件
DoKit工具集的入口,该组件起到一个外壳的作用,其他功能组件就是展示在index组件页面上的。
代码分析
- index.json
{
"component": true,
"navigationBarTitleText": "",
"usingComponents": {
"debug": "../components/debug/debug",
"appinformation": "../components/appinformation/appinformation",
"positionsimulation": "../components/positionsimulation/positionsimulation",
"storage": "../components/storage/storage",
"h5door": "../components/h5door/h5door",
"httpinjector": "../components/httpinjector/httpinjector",
"apimock": "../components/apimock/apimock"
}
}
复制代码
可见index本身是一个自定义组件,同时引入了DoKit所有其他的功能组件
什么是自定义组件?
开发者可以将页面内的功能模块抽象成自定义组件,以便在不同的页面中重复使用;也可以将复杂的页面拆分成多个低耦合的模块,有助于代码维护。自定义组件在使用时与基础组件非常相似。
- index.wxml
<block wx:if="{{ curCom!= 'dokit' }}">
<debug wx:if="{{ curCom === 'debug' }}" bindtoggle="tooggleComponent"></debug>
<appinformation wx:if="{{ curCom === 'appinformation' }}" bindtoggle="tooggleComponent"></appinformation>
<positionsimulation wx:if="{{ curCom === 'positionsimulation' }}" bindtoggle="tooggleComponent"></positionsimulation>
<storage wx:if="{{ curCom === 'storage' }}" bindtoggle="tooggleComponent"></storage>
<h5door wx:if="{{ curCom === 'h5door' }}" bindtoggle="tooggleComponent"></h5door>
<httpinjector wx:if="{{ curCom === 'httpinjector' }}" bindtoggle="tooggleComponent"></httpinjector>
<apimock wx:if="{{ curCom === 'apimock' }}" bindtoggle="tooggleComponent" projectId="{{ projectId }}"></apimock>
</block>
<block wx:else>
<cover-image
bindtap="tooggleComponent"
data-type="debug"
class="dokit-entrance"
src="//pt-starimg.didistatic.com/static/starimg/img/W8OeOO6Pue1561556055823.png"
></cover-image>
</block>
复制代码
wx:if
是wxml文件的列表渲染语法,可见index页面是根据 {{curCom}}
的值来展示不同的功能组件。
- index.js
Component({
properties: {
projectId: {
type: String,
value: '',
}
},
data: {
curCom: 'dokit',
},
methods: {
tooggleComponent(e) {
const componentType = e.currentTarget.dataset.type || e.detail.componentType
this.setData({
curCom: componentType
})
}
}
});
复制代码
初始情况下{{curCom}}
的值为 'dokit'
,因此显示的是 wx:else
块,页面上只有一个图标
bindtap
是监听点击事件,点击图标会调用js文件中的 tooggleComponent
方法,传入事件e。该方法会获取 e.currentTarget.dataset.type || e.detail.componentType
这两个值并更新 curCom
值,可以打印出来进行验证,第一个值是debug,第二个值未定义。
curCom
值的更改会影响到页面上显示的组件,因此点击图标后页面显示的是debug组件,上图控制台中第三行输出就是debug组件进入页面节点树的标志。尝试在wxml文件中更改data-type="debug"
的值为其他组件的名称,再次点击会相应显示出新的组件,因此也证明了e.currentTarget.dataset.type
的值由 data-type
属性决定。
进一步观察,当点击debug组件上的其他功能图标如App信息时,外壳index里的debug组件就会发生更换,控制台的输出如下图所示,说明的确是curCom
值决定了index页面上显示的组件,注意到此时tooggleComponent
方法又被触发了,根据代码,触发的来源应该只有 bindtoogle
了,观察到这次 e.currentTarget.dataset.type || e.detail.componentType
中第一个值变成了未定义,第二个值为appinformation。
目前的疑惑
我知道bindtap是监听点击事件,但bindtoggle是用来做什么的,以及它们与 e.currentTarget.dataset.type || e.detail.componentType
的关系是什么?(下文分析)
back组件
该组件被除index外的其他组件引用,在其他组件的页面中表现为一个dokit的logo,点击logo都会返回到最原始的index页面。
back组件工作原理简单,正好可以用来学习组件间通信和自定义事件。
原理分析
DoKit中点击各个按钮后进行相应组件的切换,是由组件中的自定义事件实现的。要使用自定义组件,就要有监听和触发。其中由子组件对事件进行触发,父组件对事件进行监听。
以每个功能组件都引用的back组件为例
back.wxml中代码如下
<cover-image
bindtap="onbackDokitEntry"
data-type="debug"
class="dokit-back"
src="//pt-starimg.didistatic.com/static/starimg/img/W8OeOO6Pue1561556055823.png"
style="top: {{ top }}">
</cover-image>
复制代码
说明back组件表现为一张图片,图片监听了点击事件,当事件触发后会调用onbackDokitEntry
方法。
(此外,我认为源码中的data-type="debug"
并没有用,通过下文的分析可知,事件的触发并没带上这个数据)
onbackDokitEntry (e) {
// console.log(e)
this.triggerEvent('return')
}
复制代码
可见,onbackDokitEntry
方法触发了一个return事件。
在其他组件的wxml中,这里以appinformation组件为例,对back组件的使用如下
<back bindreturn="onGoBack"></back>
复制代码
这句话表明,appinformation组件里的back组件正是在监听一个名为return
的自定义事件,如果监听到了,就会调用onGoBack
方法,在js文件中其定义如下
onGoBack () {
//触发事件,携带detail对象
this.triggerEvent('toggle', { componentType: 'dokit'})
}
复制代码
此处说明,onGoBack
方法又触发了名为toggle
的事件,并携带了一个detail对象{componentType: 'dokit'}
。
再回到最外面装载其他组件的index外壳页面,可以看到对appinformation组件的使用方式为:
<appinformation wx:if="{{ curCom === 'appinformation' }}"
bindtoggle="tooggleComponent">
</appinformation>
复制代码
可见,此处正是在监听名为toggle
的事件,并调用tooggleComponent
方法,其定义如下
tooggleComponent(e) {
const componentType = e.currentTarget.dataset.type || e.detail.componentType
this.setData({
curCom: componentType,
})
}
复制代码
看到此处就明白了,根据toogle
事件携带的detail对象中的componentType值,curCom
值变更为'dokit'
,前文对index组件的分析中已经提到,curCom
值直接影响到index里组件的显示,当其值为'dokit'
时,index页面就会变成最原始的只有一个logo的样子。因此,这样就解释了back组件的工作原理——通过监听对logo图标的点击事件,从里到外触发了一系列的自定义返回事件,最终体现在index页面中;同时也解释了上文中提到的bindtoggle
的部分作用(该事件不只是用于返回,还有一个功能是从debug组件进入其他功能组件,下文分析)。
back组件工作流程图如下
debug组件
该组件起到一个功能菜单的作用,展示了dokit当前具备的所有功能,点击每个图标会进入相应的功能组件。
功能展示
debug.wxml
<view wx:for="{{tools}}" wx:key="index" class="debug-collections card">
复制代码
debug.js
lifetimes: {
attached () {
this.setData({
tools: this.getTools()
});
}
},
getTools() {
return [
{
"type": "common",
"title": "常用工具",
"tools": [
{
"title": "App信息",
"image": img.appinfoicon,
"type": "appinformation"
},
//此处省略。。。
]
}
]
},
复制代码
attached
是一个组件生命周期方法,在组件实例进入页面节点树时执行,由代码可知,每次debug组件进入index页面时,会通过getTools
方法为组件的tools
变量赋值,getTools
方法返回的就是其他功能组件的信息,之后在wxml中,利用wx:for="{{tools}}"
进行列表渲染,从而循环展示出所有的功能。
功能选择
debug.wxml
<view wx:for="{{item.tools}}"
wx:for-index="idx"
wx:for-item="tool"
wx:key="idx"
data-type="{{tool.type}}"
bindtap="onToggle"
class="card-item">
复制代码
在列表循环中,每个图标在监听点击事件,点击发生后调用onToggle
方法,使用data-type
属性可以给事件带上信息,以此判断用户点击的具体是哪个功能。
在组件节点中可以附加一些自定义数据。这样,在事件中可以获取这些自定义的节点数据,用于事件的逻辑处理。
在 WXML 中,这些自定义数据以data-开头,多个单词由连字符-连接。这种写法中,连字符写法会转换成驼峰写法,而大写字符会自动转成小写字符。如:
- data-element-type,最终会呈现为event.currentTarget.dataset.elementType;
- data-elementType,最终会呈现为event.currentTarget.dataset.elementtype。
degub.js
onToggle (event) {
const type = event.currentTarget.dataset.type;
if(type === 'onUpdate') {
this[type]();
} else {
//触发事件,携带detail对象
this.triggerEvent('toggle', { componentType: type })
}
},
复制代码
由官方文档和代码可知,event.currentTarget.dataset.type
就是wxml中的data-type
属性值。如果用户点击的是更新版本功能,则会调用在此js文件中定义的onUpdate
方法(此方法下文分析);如果是其他功能,则会触发toggle
事件,并带上一个detail对象,再之后就如上文已经分析的那样,会被index页面中的bindtoggle
监听,并根据detail对象更新curCom
值,从而实现页面中相应功能组件的切换。
流程图如下
在上文back组件部分的最后,我提到bindtoggle
有两个作用,现在可以做一个总结:
- 监听所有组件中的点击logo返回事件
- 监听debug组件中的功能选择事件
可以发现,这些层层嵌套的自定义事件的最底层触发条件都是tap
,即点击操作。
版本更新功能
此功能用来检查小程序最新发布的版本是否比当前设备中的版本高
onUpdate () {
const updateManager = wx.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
if(!res.hasUpdate) {
// 请求完新版本信息的回调
wx.showModal({
title: '更新提示',
content: '当前已经是最新版本'
})
}
});
updateManager.onUpdateReady(function () {
//新版本下载成功
wx.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate()
}
}
})
});
updateManager.onUpdateFailed(function () {
// 新版本下载失败
wx.showModal({
title: '更新提示',
content: '下载失败',
success(res) {
}
})
})
},
复制代码
前文已经提到,在debug菜单组件中如果点击了更新版本功能,则会调用上述onUpdate
方法。
wx.getUpdateManager
是微信官方接口,用来获取小程序全局唯一的版本更新管理器UpdateManager
对象,以此来管理小程序的更新,该对象有四个方法。
- UpdateManager.applyUpdate()
强制小程序重启并使用新版本。在小程序新版本下载完成后(即收到 onUpdateReady 回调)调用。
- UpdateManager.onCheckForUpdate(function callback)
监听向微信后台请求检查更新结果事件。微信在小程序冷启动时自动检查更新,不需由开发者主动触发。
- UpdateManager.onUpdateReady(function callback)
监听小程序有版本更新事件。客户端主动触发下载(无需开发者触发),下载成功后回调
- UpdateManager.onUpdateFailed(function callback)
监听小程序更新失败事件。小程序有新版本,客户端主动触发下载(无需开发者触发),下载失败(可能是网络原因等)后回调
所以onUpdate
方法的整体逻辑是先检查是否存在更新版本,若不存在则使用wx.showModal
显示模态对话框提示当前已是最新版本,若存在则由微信客户端自动进行小程序的新版本下载,如果下载成功将调用onUpdateReady
里的回调——提醒用户重启应用新版本,如果下载失败则调用onUpdateFailed
里的回调(源码中该回调函数内容为空,这里我模仿onUpdateReady
里的回调也加了一个对话框)。
positionsimulation组件
用于小程序端位置模拟,包括位置授权,位置查看,位置模拟,恢复位置设置等几大功能,可以通过简单的点击操作实现任意位置模拟和位置还原,该功能的实现原理是通过对wx.getLocation进行方法重写,进而进行位置模拟,位置模拟后,在小程序内所有调用位置查询的方法内都将返回你设定的位置,还原后将恢复原生方法
DoKit文档,github.com/didi/Doraem…
快速授权
<button class="fast-authorization" open-type="openSetting">快速授权</button>
复制代码
使用open-type
微信开放能力,点击button后会打开小程序授权设置页面
查看位置
使用自定义的openMyPosition
方法,定义如下
openMyPosition (){
wx.getLocation({
type: 'gcj02',
success (res) {
wx.openLocation({
latitude:res.latitude,
longitude:res.longitude,
scale: 18
})
}
})
},
复制代码
可见,该方法直接调用了微信官方的wx.getLocation
接口来获取当前位置数据,type
属性为'gcj02'
,这是一种可用于wx.openLocation
的坐标,接口调用成功则将坐标传给wx.openLocation
方法,使用微信内置地图查看当前位置。
选择位置
预备知识:
**Object.defineProperty()**方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
参考:developer.mozilla.org/zh-CN/docs/…
使用自定义的choosePosition
方法,定义如下
choosePosition (){
wx.chooseLocation({
success: res => {
this.setData({ currentLatitude: res.latitude });
this.setData({ currentLongitude: res.longitude })
Object.defineProperty(wx, 'getLocation', {
get(val) {
return function (obj) {
obj.success({latitude: res.latitude, longitude: res.longitude})
}
}
})
}
})
},
复制代码
首先调用微信官方的wx.chooseLocation
方法,以实现打开微信内置地图选择位置,调用成功后,使用Object.defineProperty
方法改写了wx.getLocation
方法。通过预备知识我们知道,getLocation
可以看作对象wx
的一个属性,属性值为一个函数定义,那么当我们修改该属性的get
函数时,其实就相当于重写了getLocation
里的函数定义,在新的函数定义里,我们接收一个对象参数,调用其success
方法,方法参数中传入我们之前选择的坐标数据,从而实现了今后每次调用wx.getLocation
方法时,给success
回调函数传入的都是我们选择的坐标数据。(get
函数中不需要参数,源码这里应该是误写)
总结一下就是,改写了**wx.getLocation**
方法,使其给其回调函数传入的是我们设定的值。
还原
使用自定义的resetPosition
方法
const app = getApp()
app.originGetLocation = wx.getLocation
//省略...
resetPosition (){
Object.defineProperty(wx, 'getLocation',{
get(val) {
return app.originGetLocation
}
});
wx.showToast({title:'还原成功!'})
this.getMyPosition()
},
复制代码
在最开始,使用app.originGetLocation
保存了wx.getLocation
中原始的函数定义,以方便后面恢复。之后,相似的原理,利用Object.defineProperty
方法将wx.getLocation
改写回来,从而实现还原。
总结
apimock组件
mock是什么
mock可以拦截网络请求,并返回一个模拟的服务器响应,使开发人员在后端接口还未实现时也能够完成前端的开发。
效果演示
- 在平台端新建一条数据mock
设定接口名称为test,接口分类为测试,请求路径为/test
在详情页面,还可以增加多个场景,并设定不同场景下的返回值,这里我设定了2个场景,Default
和场景1
- 小程序上查看mock功能
首先要确保在引入dokit的组件页面上传入了projectId
属性,projectId可以在平台端找到
<dokit projectId="5fcd3ef4b4f88839cd7bff5848bfe3ca">
</dokit>
复制代码
打开apimock(数据模拟)组件,可以看到我们之前注册的test接口
- 模拟一个网络请求
在首页加了一个button,当点击按钮时,向/test
接口发送一个GET请求,如果请求成功的话,打印返回结果
mock_test(){
wx.request({
url: 'https://localhost/test',
method:"GET",
success:function(res){
console.log(res)
}
})
},
复制代码
- 测试结果
打开mock开关,设定场景值为Default
,点击按钮发送请求,观察控制台,可以看到是我们之前设定的返回值
设定场景值为场景值1
,再次测试,发现返回值的确发生了相应的改变
- 模板功能
对mock接口请求成功后,返回的数据会作为模板数据保存下来,方便上传
代码分析
视图层的原理很简单,之前其他组件的分析中均有涉及,故在此不作赘述,直接分析逻辑层
按照自上而下的方法,首先看一下该组件的生命周期
lifetimes: {
created () {
},
attached () {
this.pageInit()
},
detached () {
wx.setStorageSync('dokit-mocklist', this.data.mockList)
wx.setStorageSync('dokit-tpllist', this.data.tplList)
}
},
复制代码
- 当该组件进入页面即开始使用时,调用了
pageInit
方法,从名字上看这应该是一个关于初始化的方法。 - 当该组件从页面中移除即退出该功能时,调用了
wx.setStorageSync
方法,该方法的作用是同步设置本地缓存,从key的名字上看是缓存了mock接口和模板的数据。
pageInit
方法定义如下
//页面初始化
pageInit () {
//初始化mock列表
this.initList()
//添加RequestHooks
this.addRequestHooks()
},
复制代码
该方法做了两件事,一个是初始化mock列表,另一个是添加RequestHooks,下面依次进行分析
initList()
//初始化mock列表
initList () {
const that = this
const opt = {
url: `${mockBaseUrl}/api/app/interface`,
method: 'GET',
data: { projectId: this.getProjectId(), isfull: 1 }
}
that.request(opt).then(res => {
const { data } = res.data
if (data && data.datalist && data.datalist.length) {
that.updateMockList(data.datalist)
that.updateTplList(data.datalist)
}
}).catch()
},
//获取projectId
getProjectId () {
if (!this.data.projectId) {
console.warn("您还没有设置 projectId,去快平台端体验吧:https://www.dokit.cn")
return
} else {
return this.data.projectId
}
},
//构造promise对象
request (options) {
return new Promise((resolve, reject) => {
app.originRequest({
...options,
success: res => resolve(res),
fail: err => reject(err)
})
})
},
复制代码
第4-8行设置了一个对象常量opt
,属性url
是一个反引号包起来的模板字符串,其中${mockBaseUrl}
在前面已经设置成一个值为”mock.dokit.cn“的常量,这正是dokit平台端的网址。
属性data
的初始化里调用了getProjectId
方法,该方法的定义在第18行,虽然projectId
是组件的属性,但也通过this.data
访问,其值是在我们引入dokit工具的页面中传入的,在这里首先检测是否已传入projectId
,若未传入则控制台输出警告后返回;若已传入则返回该属性值。
从opt
对象的构造来看,非常像我们在使用wx.request
方法时传入的对象,下文的分析也会印证这个猜想。
第9-15行调用了一个request
方法,该方法的定义在第28行,用来构造并返回一个promise对象。在继续分析之前,有必要简单介绍一下什么是promise,在我看来promise是用来控制异步操作实现同步的一种手段,其避免了层层嵌套的回调函数,将一系列异步操作按照我们期望的顺序执行。
想了解更多关于promise的内容请浏览
www.liaoxuefeng.com/wiki/102291…
Promise中的then第二个参数和catch有什么区别blog.csdn.net/gogo_steven…
第31行的app.originRequest
是在该js文件最前面被设置的(本文未显示),设置的值为wx.request
,为什么这里不直接使用wx.request
呢?
因为apimock组件的实现原理就是通过改写wx.request
方法,如果想确保任何时候都还能使用到正常的wx.request
方法,就得事先将其另存起来。
回到第9-15行,如果已经明白promise工作原理的话,我们就可分析出这段代码的意思——带上我们的projectId向dokit平台端接口发送请求,该接口的作用应该是根据projectId返回我们在平台端设定的mock信息,在确保请求成功并返回后,检验返回数据,如果合法则调用相关方法对mocklist数据进行更新,这里的执行顺序非常重要,而promise的作用就体现在这。值得一提的是,第15行的catch
方法里没有传入异常处理的回调函数,我认为最好有一个,从而使程序更加健壮。
.catch((err)=>{
console.log(err)
})
复制代码
addRequestHooks()
首先,什么是Hooks,根据网络上的解释,是在已经可以正常运作的程序中额外添加流程控制,通俗来说就是拦截指定的消息,用自己的方式处理一下,然后再放出去。
addRequestHooks () {
Object.defineProperty(wx, "request" , { writable: true });
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
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)
}
},
复制代码
第2行的Object.defineProperty
之前我们已经分析过,它的作用是为对象新增或修改一个属性,这里是将wx.request
设置为允许被赋值运算符改变。
观察到第4、5行的方法后都有一个.bind(this)
,这里是为了确保之后调用两个方法时,其函数体内的this指向的是当前这个组件对象。
bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()第一个参数的值,例如,f.bind(obj),实际上可以理解为obj.f(),这时,f函数体内的this自然指向的是obj。
第6-19行是对wx.request
的重写,第9行是将请求options
传入matchUrlRequest
中,返回一个sceneId
,如果该值非空的话,改写请求options
里的URL为dokit平台端一个与sceneId
相关的接口,并在控制台输出改写后的options
信息,第15-17行改写了请求options
里的success
回调函数(下文分析),第18行调用之前保存的原始的微信请求方法,此时传入的是已修改后的options
。
这里可以推测,matchUrlRequest
方法的作用应该是将我们原本要发送的请求与mock接口列表相匹配,如果匹配成功就返回之前为该mock接口选定的场景的Id。观察改写后的新URL我们可以确定dokit平台端就是根据场景Id来确定应该响应什么数据。
具体来看,matchUrlRequest
方法的定义如下
//匹配URL请求
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
},
// judge url is match
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)
},
复制代码
第3行声明了3个变量,flag
记录是否匹配成功,curMockItem
记录mock接口信息,sceneId
记录场景Id。第5-11行是在循环遍历mock列表,其中真正用来匹配的是第7行的requestIsmatch
方法,其定义在26行,通过观察里面调用的方法名大致可以推断用来匹配的依据是:请求路径、请求方法、query参数、请求体。回到第12-24行,若成功匹配到mock信息,则继续检查该mock接口下的哪一个场景被勾选,选中的场景Id存入sceneId
中,第23行的写法表示若flag
和curMockItem.checked
都为true的话,返回sceneId
的值,否则返回false。
现在对addRequestHooks
方法的分析中还剩下一点,就是第15-17行对原有请求options
里的success回调函数的修改,结合第8行来看,好像就是多做了一件事matchUrlTpl(opt, res)
,该方法的定义如下
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
},
复制代码
简单来说,该方法的作用就是,如果本次请求与mock接口列表匹配,则将请求成功返回的数据res
作为模板保存在本地存储中,然后正常返回res
以便继续执行原来的success回调。
总结
至此,数据mock功能最核心的代码已经分析完毕,其原理简单概括就是,重写wx.request
方法,在发送请求前,将请求与用户设置的mock接口进行匹配,若匹配成功则修改请求信息(URL和请求成功的回调函数),之后才真正发送请求。
结语
这是本人第一次尝试阅读并分析开源项目的代码,在此过程中感觉收获良多,感谢指导和帮助过我的DoKit项目老师们和同学们。由于我对小程序和JavaScript的理解也只是入门阶段,文中若有分析不当之处,欢迎指正。
2021/4/27
作者信息
作者:七省文状元
来源:掘金