最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【DoKit&北大专题】-浅谈滴滴DoKit业务代码零侵入思想(小程序端)

    正文概述 掘金(DoKit)   2021-04-28   486

    专题背景

    系列文章

    【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的组件功能,可以分为两种:

    1. 组件自身对业务代码的输出不产生或很少产生影响,其目的为快速查看某些信息,代表组件为App信息、缓存管理、H5任意门等。
    2. 组件自身对业务代码的输出产生了影响,其目的为测试业务代码的输出结果/模拟用户的输入,代表组件为位置模拟、请求注射、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设置的函数,使用户再调用该接口时调用该函数,来影响业务代码的输出。

    【DoKit&北大专题】-浅谈滴滴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函数,将本来应该返回的实际地理位置信息对象改为想模拟的地理位置。

    【DoKit&北大专题】-浅谈滴滴DoKit业务代码零侵入思想(小程序端)

    还原位置的方法很简单,再次使用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.requestwritable属性修改为true,之后重写该接口,将原来options对象中的success回调函数得到的正常response响应对象通过hooksRequestSuccessCallback()函数进行注射,再执行原来的网络请求。这样就可以实现业务代码接收到的response对象为注射后的对象。
    hooksRequestSuccessCallback()函数的用途是根据用户填入Dokit中的注射列表来进行相应的key-value键值对的属性修改,详细的逻辑可以参考源代码。

    【DoKit&北大专题】-浅谈滴滴DoKit业务代码零侵入思想(小程序端)

    2.4 APImock中的相关实现

    与请求注射相同,APImock的关键也是拦截wx.request接口,若当前网络请求的网址路径在用户Dokit平台端的mock列表中,则进行接口mock:将当前请求拦截,给应用端返回一个Dokit平台端模拟的服务器响应。
    APImock组件可能是Dokit小程序端中实现最复杂的一个组件,所以我们来详细分析一下APImock的实现代码,这里先上一个Dokit官方提供的逻辑流程图:

    【DoKit&北大专题】-浅谈滴滴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函数返回trueflag = 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函数实际上是封装了两个测试函数:urlMethodIsEqualrequestParamsIsEqual函数,分别检测请求的路径、方法和请求参数。具体代码如下:

    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请求体)
    总结一下,具体的判断是否命中拦截的流程如图:

    【DoKit&北大专题】-浅谈滴滴DoKit业务代码零侵入思想(小程序端)
    回到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…

    来源:掘金


    起源地下载网 » 【DoKit&北大专题】-浅谈滴滴DoKit业务代码零侵入思想(小程序端)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元