Main points of this paper:

  • Understand the idea of zero intrusion of Dokit applets side business code
  • Learn about the implementation of zero-intrusion business code in location emulation, request injection, and APImock functionality

One, foreword

1.1 Brief classification of Dokit component functions

In the previous front-end beginners to read didi Dokit small program source code series of articles, we introduced the basic syntax of wechat small program, features such as event binding, conditional rendering, list rendering, event communication and other content. We also briefly analyzed two components of Dokit: index component and Debug component. So far, we have basically mastered the basic knowledge of analyzing the source code of Dokit applets. After that, as long as we follow similar analysis methods, combined with the response oF wechat applets API and JavaScript syntax, we can interpret them one by one to understand the specific implementation of each function. So this time, let’s jump out of a specific component. From a macro perspective, the component functions of Dokit can be divided into two types:

  1. The component itself has no or little impact on the output of the business code. Its purpose is to quickly view some information, which means the component is App information, cache management, H5 any gate, etc.
  2. Components themselves affect the output of the business code to test the output of the business code/simulate user input, representing components such as location emulation, request injection, APImock, and so on.

In the Dokit components of wechat small program, the first type of components is mainly realized by encapsulation of system interface functions, while the second type of components is realized by rewriting system interface functions based on the idea of zero intrusion of business code. This article will talk about Dokit business code zero intrusion ideas.

1.2 What is zero intrusion of business code

Let’s start with a simple application scenario: Suppose my APP has a location-related function that looks like this:

wx.getLocation({
    type: 'gcj02',
    success (res) {
      const latitude = res.latitude
      const longitude = res.longitude
      ...
      // Business logic. }})Copy the code

Now I want to test the output of this function when the geographical location is a specific location in Shanghai, if I modify the code to do this:

Dump.getLocation(ShanghaiPos,{
    type: 'gcj02',
    success (res) {
      const latitude = res.latitude
      const longitude = res.longitude
      ...
      // Business logic. }})Copy the code

The obvious testing problem is that it is cumbersome to have to modify the source code after each test, and it is also not conducive to debugging. It is inefficient to have to modify the code and recompile the code after simulating one case at a time. This test method is clearly not what we want, we hope is on the premise of not modify the source code of the test source code, and the positions of the Dokit simulation modules to meet the demand that we only need to open the component, to choose what he or she wants to simulate position, can be tested, the need to modify our own business code, and greatly improve the test efficiency. That is, we need a technical solution that allows developers to test without modifying their own source code, an idea known as “business code zero intrusion.”

Two, technical implementation

2.1 Technical core: Object.defineProperty

Interception intercepts the native API and lets developers call the modified API when they call the API. The main idea of Dokit applet side to achieve zero intrusion of business code is to use static method object.defineProperty in JavaScript to rewrite the interface API provided by wechat, so that the user’s own business code is not affected in the test process. Refer to the related documentation for more details on this function. There are two main ways to use Dokit:

  • Set the getter function for the interface API, which will be called when the user calls the interfacegetFunction to influence the output of the business code.
  • Sets the property descriptor for the interface APIwritable:true, so that the API of this interface can beThe assignment operatorChange the interface and then affect the output of the business code by changing the interface to a function set by Dokit so that the user can call the function when the interface is called again.

2.2 Relevant implementation in position simulation

The key to location simulation is to intercept the wx.getLocation interface and change the actual location returned by the interface to the desired location, with the following code:

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})
                    }
                }
            })
        }
    })
}
Copy the code

As you can see, location emulation is implemented by calling the wx.chooseLocation interface to select the location you want to emulate, and then setting the get function of the Wx. getLocation interface via the Object.defineProperty method. Change the actual location information object that should be returned to the location you want to emulate.



The way to restore the location is simple, use it againObject.definePropertyMethod will bewx.getLocationOf the interfacegetFunction is set to the native interface function previously mounted (saved) on the APP instance, so that when the user calls the interface again, the native interface function will be called, the specific code is as follows:

resetPosition (){
    Object.defineProperty(wx, 'getLocation',
    {
        get() {
            return app.originGetLocation
        }
    });
    wx.showToast({title:'Restore successful! '})
    this.getMyPosition()
}
Copy the code

The interception interface restoration methods mentioned later are similar. The interface is set as the native interface function previously mounted on the APP instance, which will not be described again.

2.3 Request relevant implementation in injection

The key to request injection is to intercept the Wx. request interface, implement injection modification on the received data, and then pass it to the business code for use. The specific code is as follows:

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)
    }
}
Copy the code

As you can see, request injection is implemented by changing the writable property of wx.request to true via object.defineProperty and then overwriting the interface. The success of the original options object callback function to get the normal response of the response object through hooksRequestSuccessCallback injection () function to request to perform the original network. In this way, the business code can receive the response object as the injected object. HooksRequestSuccessCallback () function according to the user is the purpose of the fill in the injection of Dokit list to the corresponding key – value key values to the properties of the modified detailed logic can refer to the source code.

2.4 Related implementations in APImock

As with request injection, the key to APImock is to intercept the Wx. request interface. If the url path of the current network request is in the mock list on the user’s Dokit platform side, the interface mock will intercept the current request and return a server response simulated on the Dokit platform side to the application side. The APImock component is probably the most complex of the Dokit applets, so let’s take a look at the implementation code of the APImock component in detail. Here is a logic flow chart provided by Dokit:

This is much better than the flow chart I wrote myself.

addRequestHooks () {
   Object.defineProperty(wx,  "request" , { writable:  true });// Intercept the wx.request method
   console.group('addRequestHooks success') 
   const matchUrlRequest = this.matchUrlRequest.bind(this) 
   const matchUrlTpl = this.matchUrlTpl.bind(this) 
   wx.request = function (options) { // Override the interface function
       const opt = util.deepClone(options)
       const originSuccessFn = options.success  // Save the SUCCESS callback function in the business code
       const sceneId = matchUrlRequest(options) // Determine whether the matching rule is met
       if (sceneId) {
           options.url = `${mockBaseUrl}/api/app/scene/${sceneId}`
           console.group('request options', options)
           console.warn('Intercepted.')
       }
       options.success = function (res) {
                   originSuccessFn(matchUrlTpl(opt, res)) // Match template rules
       }
       app.originRequest(options)
   }
}
Copy the code

The first thing to do in the rewritten wx.request interface is to make a deep copy of the interface parameters, options, so that template data can be uploaded later, and then use the matchUrlRequest() function to determine whether the current network request matches the interception rule. Let’s take a look at the specific interception rules:

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
}
Copy the code

If there is a matching mock response (requestIsmatch returns true→flag = true), then sceneList is iterated through the sceneList of the response to find the scene selected by the user. Further, let’s look at the logic of the requestIsmatch function to determine whether the request matches:

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)
}
Copy the code

The requestIsmatch function encapsulates two test functions: urlMethodIsEqual and requestParamsIsEqual, which check the request path, method, and request parameters, respectively. The specific code is as follows:

urlMethodIsEqual (reqPath, reqMethod, mockPath, mockMethod) {
    reqPath = reqPath ? ` /${reqPath}` : ' '
    reqMethod = reqMethod || 'GET'
    return (reqPath == mockPath) && (reqMethod.toUpperCase() == mockMethod.toUpperCase())
}
Copy the code

The urlMethodIsEqual function determines whether the request path and request method (GET, POST, or other) match the mock interface.

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}}Copy the code

The requestParamsIsEqual function determines whether the parameters of the request match the mock interface (including the Query request Body and the Body).



Go back toaddRequestHooksAfter matching the template rule, the function changes the url of the original request to the corresponding path of Dokit${mockBaseUrl}/api/app/scene/${sceneId}To return the mock interface’s response.

In the process, Dokit also overwrites the success callback function for the options argument with thematchUrlTplFunction to determine if the received response matches the template rule, and if so, to save the response object as a template. The specific code is as follows:

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
}
Copy the code

Template rules are simpler than interception rules: The requestIsmatch function is used to determine whether the current request matches the template list TplList. If it matches and the response is successful (curtplItem.checked && res.statusCode == 200), Save it (wx.setStoragesync) and wait for the user to browse and upload. At the end of the rewritten interface function, the native interface function app.originRequest is executed. The process of intercepting and rewriting the interface is complete. In the implementation of the APIMock function component, Dokit uses object.defineproperty method to rewrite the request interface, not only does not need to modify the call of the interface function in the business code, but also rewriting the url parameters, and even the url parameters of the request in the business code. The real realization of “zero intrusion of business code”.

Third, summary

This article understands Dokit’s idea of “zero intrusion of business code” by reading relevant codes of location simulation, request injection and the main implementation of APImock. In the process of reading the source code, we should not only simply read how a component is realized, but also understand the macro design ideas of Dokit. More importantly, we should understand the process of “finding business pain points → proposing targeted solutions → realizing the final technology”. Said so much also just a little shallow understanding of myself, right to throw a brick to introduce jade, if there are mistakes or omissions also hope criticism and advice.