【DoKit&北大专题】-DoKit For 小程序源码分析

专题背景

近几年随着开源在国内的蓬勃发展,一些高校也开始探索让开源走进校园,让同学们在学生时期就感受到开源的魅力,这也是高校和国内的头部互联网企业共同尝试的全新教学模式。本专题会记录这段时间内学生们的学习成果。

更多专题背景参考:【DoKit&北大专题】缘起

系列文章

【DoKit&北大专题】缘起

【DoKit&北大专题】-读小程序源代码(一)

【DoKit&北大专题】-读小程序源代码(二)

【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。
image.png
image.png
我觉得“在框架中跳舞”这句话描述得非常恰当,小程序依托于微信客户端,在开发过程中有着诸多限制,如果想要开发一些辅助功能,大概率是通过对微信小程序官方API进行一定的改造来实现,下文对源码的分析中也一直在体现这种思想。

DoKit项目结构

image.png

assets ——资源文件夹,目前存放了一些图标文件
components ——Dokit最核心的部分,包括了八个自定义组件

  1. apimock —— 数据Mock功能组件
  2. appinformation —— App信息查看功能组件
  3. back —— 用来返回的自定义组件,该组件并不是Dokit的功能组件,在其他功能页面通过该组件进行返回
  4. debug —— 主菜单组件,罗列了Dokit的各种功能
  5. h5door —— h5任意门功能组件
  6. httpinjector ——请求注射功能组件
  7. positionsimulation —— 位置模拟功能组件
  8. 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 块,页面上只有一个图标

image.png

bindtap 是监听点击事件,点击图标会调用js文件中的 tooggleComponent 方法,传入事件e。该方法会获取 e.currentTarget.dataset.type || e.detail.componentType 这两个值并更新 curCom 值,可以打印出来进行验证,第一个值是debug,第二个值未定义。

image.png

image.png

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。

image.png

目前的疑惑

我知道bindtap是监听点击事件,但bindtoggle是用来做什么的,以及它们与 e.currentTarget.dataset.type || e.detail.componentType的关系是什么?(下文分析)

back组件

图片[3]-【DoKit&北大专题】-DoKit For 小程序源码分析-一一网图片[3]-【DoKit&北大专题】-DoKit For 小程序源码分析-一一网
该组件被除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组件工作流程图如下
image.png

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。

微信开发文档,developers.weixin.qq.com/miniprogram…

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值,从而实现页面中相应功能组件的切换。
流程图如下
image.png

在上文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)

监听小程序更新失败事件。小程序有新版本,客户端主动触发下载(无需开发者触发),下载失败(可能是网络原因等)后回调

微信开发文档,
developers.weixin.qq.com/miniprogram…

所以onUpdate方法的整体逻辑是先检查是否存在更新版本,若不存在则使用wx.showModal显示模态对话框提示当前已是最新版本,若存在则由微信客户端自动进行小程序的新版本下载,如果下载成功将调用onUpdateReady里的回调——提醒用户重启应用新版本,如果下载失败则调用onUpdateFailed里的回调(源码中该回调函数内容为空,这里我模仿onUpdateReady里的回调也加了一个对话框)。

positionsimulation组件

image.png

用于小程序端位置模拟,包括位置授权,位置查看,位置模拟,恢复位置设置等几大功能,可以通过简单的点击操作实现任意位置模拟和位置还原,该功能的实现原理是通过对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方法,使用微信内置地图查看当前位置。

选择位置

预备知识:

image.png

**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改写回来,从而实现还原。

总结

image.png

apimock组件

mock是什么

mock可以拦截网络请求,并返回一个模拟的服务器响应,使开发人员在后端接口还未实现时也能够完成前端的开发。

效果演示

  1. 在平台端新建一条数据mock

image.png
设定接口名称为test,接口分类为测试,请求路径为/test
在详情页面,还可以增加多个场景,并设定不同场景下的返回值,这里我设定了2个场景,Default场景1
image.png

  1. 小程序上查看mock功能

首先要确保在引入dokit的组件页面上传入了projectId属性,projectId可以在平台端找到

  <dokit projectId="5fcd3ef4b4f88839cd7bff5848bfe3ca">
  </dokit>
复制代码

打开apimock(数据模拟)组件,可以看到我们之前注册的test接口
image.png

  1. 模拟一个网络请求

在首页加了一个button,当点击按钮时,向/test接口发送一个GET请求,如果请求成功的话,打印返回结果
image.png

mock_test(){
    wx.request({
      url: 'https://localhost/test',
      method:"GET",
      success:function(res){
        console.log(res)
      }
    })
  },
复制代码
  1. 测试结果

打开mock开关,设定场景值为Default,点击按钮发送请求,观察控制台,可以看到是我们之前设定的返回值
image.png
设定场景值为场景值1,再次测试,发现返回值的确发生了相应的改变
image.png

  1. 模板功能

对mock接口请求成功后,返回的数据会作为模板数据保存下来,方便上传
image.pngimage.png

代码分析

视图层的原理很简单,之前其他组件的分析中均有涉及,故在此不作赘述,直接分析逻辑层
按照自上而下的方法,首先看一下该组件的生命周期

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的内容请浏览

blog.csdn.net/zzh990822/a…

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
image.png
这里可以推测,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行的写法表示若flagcurMockItem.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

作者信息

作者:七省文状元

来源:掘金

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享