“This is the 21st day of my participation in the Gwen Challenge in November. See details: The Last Gwen Challenge in 2021”

Usually in the development, the back-end interface is not completed. In order not to affect the progress of the project, the front and back end will first define the protocol specification, develop separately, and finally conduct joint adjustment. There are two main ways to create Mock services on the front end. One is to simulate XmlHttpRequesst, and the other is to intercept requests and respond with the ability of Node services. This article looks at how to Mock by emulating XmlHttpRequest, and the next article will look at how to use Node’s capabilities to generate Mock services and populate the initial data.

MockJs is a class library that mocks XmlHttRequest. Here is a look at how it is implemented.

1. Separation of front and rear ends,

2. Large amount of data can be generated randomly

3. Simple usage

4. Rich data types

5. Extensible data types

6. In the case of the existing interface document, we can directly follow the interface document to develop, write the corresponding field, after the interface is completed, only need to change the URL address.

Interface-xmlhttprequest (XMLHttpRequest

[Exposed = (Window, DedicatedWorker SharedWorker)] / / the triggering event defined interface XMLHttpRequestEventTarget: EventTarget { // event handlers attribute EventHandler onloadstart; attribute EventHandler onprogress; attribute EventHandler onabort; attribute EventHandler onerror; attribute EventHandler onload; attribute EventHandler ontimeout; attribute EventHandler onloadend; }; [Exposed=(Window,DedicatedWorker,SharedWorker)] interface XMLHttpRequestUpload : XMLHttpRequestEventTarget { }; / / response type of enumeration enum XMLHttpRequestResponseType {" ", "arraybuffer", "blob", "document", "json", "text"}; [Exposed=(Window,DedicatedWorker,SharedWorker)] interface XMLHttpRequest : XMLHttpRequestEventTarget { constructor(); // event handler attribute EventHandler onreadystatechange; Const unsigned short UNSENT = 0; const unsigned short UNSENT = 0; const unsigned short OPENED = 1; const unsigned short HEADERS_RECEIVED = 2; const unsigned short LOADING = 3; const unsigned short DONE = 4; readonly attribute unsigned short readyState; // request undefined open(ByteString method, USVString url); undefined open(ByteString method, USVString url, boolean async, optional USVString? username = null, optional USVString? password = null); undefined setRequestHeader(ByteString name, ByteString value); attribute unsigned long timeout; attribute boolean withCredentials; [SameObject] readonly attribute XMLHttpRequestUpload upload; undefined send(optional (Document or XMLHttpRequestBodyInit)? body = null); undefined abort(); Readonly attribute USVString responseURL; // Response Response attributes are read-only and cannot be modified. // The status is read-only and cannot be changed readonly attribute unsigned short status; readonly attribute ByteString statusText; ByteString? getResponseHeader(ByteString name); ByteString getAllResponseHeaders(); undefined overrideMimeType(DOMString mime); attribute XMLHttpRequestResponseType responseType; readonly attribute any response; readonly attribute USVString responseText; [Exposed=Window] readonly attribute Document? responseXML; };Copy the code

ReadyState, Status, statusText, response, responseText, and responseXML are readonly attributes. To simulate the response is not feasible. So the only way is to emulate the entire XMLHttpRequest.

2. Simulate XMLHttpRequest

// 源码出处:https://github.com/sendya/Mock     NPM:mockjs2

var XHR_STATES = {
    // 初始化状态。XMLHttpRequest 对象已创建或已被 abort() 方法重置
    UNSENT: 0,
    // open() 方法已调用,但是 send() 方法未调用。请求还没有被发送
    OPENED: 1,
    // Send() 方法已调用,HTTP 请求已发送到 Web 服务器。未接收到响应
    HEADERS_RECEIVED: 2,
    // 所有响应头部都已经接收到。响应体开始接收但未完成
    LOADING: 3,
    // HTTP 响应已经完全接收
    DONE: 4
}

var XHR_EVENTS = 'readystatechange loadstart progress abort error load timeout loadend'.split(' ')
var XHR_REQUEST_PROPERTIES = 'timeout withCredentials'.split(' ')
var XHR_RESPONSE_PROPERTIES = 'readyState responseURL status statusText responseType response responseText responseXML'.split(' ')

// http状态码
var HTTP_STATUS_CODES = {
    100: "Continue",
    101: "Switching Protocols",
    200: "OK",
    201: "Created",
    202: "Accepted",
    203: "Non-Authoritative Information",
    204: "No Content",
    205: "Reset Content",
    206: "Partial Content",
    300: "Multiple Choice",
    301: "Moved Permanently",
    302: "Found",
    303: "See Other",
    304: "Not Modified",
    305: "Use Proxy",
    307: "Temporary Redirect",
    400: "Bad Request",
    401: "Unauthorized",
    402: "Payment Required",
    403: "Forbidden",
    404: "Not Found",
    405: "Method Not Allowed",
    406: "Not Acceptable",
    407: "Proxy Authentication Required",
    408: "Request Timeout",
    409: "Conflict",
    410: "Gone",
    411: "Length Required",
    412: "Precondition Failed",
    413: "Request Entity Too Large",
    414: "Request-URI Too Long",
    415: "Unsupported Media Type",
    416: "Requested Range Not Satisfiable",
    417: "Expectation Failed",
    422: "Unprocessable Entity",
    500: "Internal Server Error",
    501: "Not Implemented",
    502: "Bad Gateway",
    503: "Service Unavailable",
    504: "Gateway Timeout",
    505: "HTTP Version Not Supported"
}

/*
    MockXMLHttpRequest
*/

function MockXMLHttpRequest() {
    // 初始化 custom 对象,用于存储自定义属性
    this.custom = {
        events: {},
        requestHeaders: {},
        responseHeaders: {}
    }
}

MockXMLHttpRequest._settings = {
    timeout: '10-100',
}

MockXMLHttpRequest.setup = function(settings) {
    Util.extend(MockXMLHttpRequest._settings, settings)
    return MockXMLHttpRequest._settings
}

Util.extend(MockXMLHttpRequest, XHR_STATES)
Util.extend(MockXMLHttpRequest.prototype, XHR_STATES)

// 标记当前对象为 MockXMLHttpRequest
MockXMLHttpRequest.prototype.mock = true

// 是否拦截 Ajax 请求
MockXMLHttpRequest.prototype.match = false

// 初始化 Request 相关的属性和方法
Util.extend(MockXMLHttpRequest.prototype, {
    // 初始化一个请求
    open: function(method, url, async, username, password) {
        var that = this

        Util.extend(this.custom, {
            method: method,
            url: url,
            async: typeof async === 'boolean' ? async : true,
            username: username,
            password: password,
            options: {
                url: url,
                type: method
            }
        })

        this.custom.timeout = function(timeout) {
            if (typeof timeout === 'number') return timeout
            if (typeof timeout === 'string' && !~timeout.indexOf('-')) return parseInt(timeout, 10)
            if (typeof timeout === 'string' && ~timeout.indexOf('-')) {
                var tmp = timeout.split('-')
                var min = parseInt(tmp[0], 10)
                var max = parseInt(tmp[1], 10)
                return Math.round(Math.random() * (max - min)) + min
            }
        }(MockXMLHttpRequest._settings.timeout)

        // 查找与请求参数匹配的数据模板
        var item = find(this.custom.options)

        function handle(event) {
            // 同步属性 NativeXMLHttpRequest => MockXMLHttpRequest
            for (var i = 0; i < XHR_RESPONSE_PROPERTIES.length; i++) {
                try {
                    that[XHR_RESPONSE_PROPERTIES[i]] = xhr[XHR_RESPONSE_PROPERTIES[i]]
                } catch (e) {}
            }
            // 触发 MockXMLHttpRequest 上的同名事件
            that.dispatchEvent(new Event(event.type /*, false, false, that*/ ))
        }

        // 如果未找到匹配的数据模板,则采用原生 XHR 发送请求。
        if (!item) {
            // 创建原生 XHR 对象,调用原生 open(),监听所有原生事件
            var xhr = createNativeXMLHttpRequest()
            this.custom.xhr = xhr

            // 初始化所有事件,用于监听原生 XHR 对象的事件
            for (var i = 0; i < XHR_EVENTS.length; i++) {
                xhr.addEventListener(XHR_EVENTS[i], handle)
            }

            // xhr.open()
            if (username) xhr.open(method, url, async, username, password)
            else xhr.open(method, url, async)

            // 同步属性 MockXMLHttpRequest => NativeXMLHttpRequest
            for (var j = 0; j < XHR_REQUEST_PROPERTIES.length; j++) {
                try {
                    xhr[XHR_REQUEST_PROPERTIES[j]] = that[XHR_REQUEST_PROPERTIES[j]]
                } catch (e) {}
            }

            // 这里的核心问题就是没考虑到在open以后去修改属性 比如axios修改responseType的行为就在open之后
            Object.defineProperty(that, 'responseType', {
                get: function () {
                    return xhr.responseType
                },
                set: function (v) {
                    return (xhr.responseType = v)
                }
            });

            return
        }

        // 找到了匹配的数据模板,开始拦截 XHR 请求
        this.match = true
        this.custom.template = item
        // 当 readyState 属性发生变化时,调用 readystatechange
        this.readyState = MockXMLHttpRequest.OPENED
        this.dispatchEvent(new Event('readystatechange' /*, false, false, this*/ ))
    },
    // 设置HTTP请求头的值。必须在open()之后、send()之前调用
    setRequestHeader: function(name, value) {
        // 原生 XHR
        if (!this.match) {
            this.custom.xhr.setRequestHeader(name, value)
            return
        }

        // 拦截 XHR
        var requestHeaders = this.custom.requestHeaders
        if (requestHeaders[name]) requestHeaders[name] += ',' + value
        else requestHeaders[name] = value
    },
    timeout: 0,
    withCredentials: false,
    upload: {},
    // 发送请求。如果请求是异步的(默认),那么该方法将在请求发送后立即返回
    send: function send(data) {
        var that = this
        this.custom.options.body = data

        // 原生 XHR
        if (!this.match) {
            this.custom.xhr.send(data)
            return
        }

        // 拦截 XHR

        // X-Requested-With header
        this.setRequestHeader('X-Requested-With', 'MockXMLHttpRequest')

        // loadstart The fetch initiates.
        this.dispatchEvent(new Event('loadstart' /*, false, false, this*/ ))

        if (this.custom.async) setTimeout(done, this.custom.timeout) // 异步
        else done() // 同步
        
        function done() {
            // 初始状态
            that.readyState = MockXMLHttpRequest.HEADERS_RECEIVED
            // 派发执行readystatechange事件
            that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
            that.readyState = MockXMLHttpRequest.LOADING
            that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
            // 响应数据的处理
            that.response = that.responseText = JSON.stringify(
                convert(that.custom.template, that.custom.options),
                null, 4
            )

            that.status = that.custom.options.status || 200
            that.statusText = HTTP_STATUS_CODES[that.status]

            that.readyState = MockXMLHttpRequest.DONE
            that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
            that.dispatchEvent(new Event('load' /*, false, false, that*/ ));
            that.dispatchEvent(new Event('loadend' /*, false, false, that*/ ));
        }
    },
    // 当request被停止时触发,例如调用XMLHttpRequest.abort()
    abort: function abort() {
        // 原生 XHR
        if (!this.match) {
            this.custom.xhr.abort()
            return
        }

        // 拦截 XHR
        this.readyState = MockXMLHttpRequest.UNSENT
        this.dispatchEvent(new Event('abort', false, false, this))
        this.dispatchEvent(new Event('error', false, false, this))
    }
})

// 初始化 Response 相关的属性和方法
Util.extend(MockXMLHttpRequest.prototype, {
    // 返回经过序列化(serialized)的响应 URL,如果该 URL 为空,则返回空字符串。
    responseURL: '',
    // 代表请求的响应状态
    status: MockXMLHttpRequest.UNSENT,
    /*
     * 返回一个 DOMString,其中包含 HTTP 服务器返回的响应状态。与 XMLHTTPRequest.status 不同的是,
     * 它包含完整的响应状态文本(例如,"200 OK")。
     */ 
    statusText: '',
    // 返回包含指定响应头的字符串,如果响应尚未收到或响应中不存在该报头,则返回 null。
    getResponseHeader: function(name) {
        // 原生 XHR
        if (!this.match) {
            return this.custom.xhr.getResponseHeader(name)
        }

        // 拦截 XHR
        return this.custom.responseHeaders[name.toLowerCase()]
    },
    // 以字符串的形式返回所有用 CRLF 分隔的响应头,如果没有收到响应,则返回 null。
    getAllResponseHeaders: function() {
        // 原生 XHR
        if (!this.match) {
            return this.custom.xhr.getAllResponseHeaders()
        }

        // 拦截 XHR
        var responseHeaders = this.custom.responseHeaders
        var headers = ''
        for (var h in responseHeaders) {
            if (!responseHeaders.hasOwnProperty(h)) continue
            headers += h + ': ' + responseHeaders[h] + '\r\n'
        }
        return headers
    },
    overrideMimeType: function( /*mime*/ ) {},
    // 一个用于定义响应类型的枚举值
    responseType: '', // '', 'text', 'arraybuffer', 'blob', 'document', 'json'
    // 包含整个响应实体(response entity body)
    response: null,
    // 返回一个 DOMString,该 DOMString 包含对请求的响应,如果请求未成功或尚未发送,则返回 null。
    responseText: '',
    responseXML: null
})

// EventTarget
Util.extend(MockXMLHttpRequest.prototype, {
    addEventListener: function addEventListener(type, handle) {
        var events = this.custom.events
        if (!events[type]) events[type] = []
        events[type].push(handle)
    },
    removeEventListener: function removeEventListener(type, handle) {
        var handles = this.custom.events[type] || []
        for (var i = 0; i < handles.length; i++) {
            if (handles[i] === handle) {
                handles.splice(i--, 1)
            }
        }
    },
    dispatchEvent: function dispatchEvent(event) {
        var handles = this.custom.events[event.type] || []
        for (var i = 0; i < handles.length; i++) {
            handles[i].call(this, event)
        }

        var ontype = 'on' + event.type
        if (this[ontype]) this[ontype](event)
    }
})

// Inspired by jQuery
function createNativeXMLHttpRequest() {
    var isLocal = function() {
        var rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/
        var rurl = /^([\w.+-]+:)(?://([^/?#:]*)(?::(\d+)|)|)/
        var ajaxLocation = location.href
        var ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || []
        return rlocalProtocol.test(ajaxLocParts[1])
    }()

    return window.ActiveXObject ?
        (!isLocal && createStandardXHR() || createActiveXHR()) : createStandardXHR()

    function createStandardXHR() {
        try {
            return new window._XMLHttpRequest();
        } catch (e) {}
    }

    function createActiveXHR() {
        try {
            return new window._ActiveXObject("Microsoft.XMLHTTP");
        } catch (e) {}
    }
}


// 查找与请求参数匹配的数据模板:URL,Type
function find(options) {

    for (var sUrlType in MockXMLHttpRequest.Mock._mocked) {
        var item = MockXMLHttpRequest.Mock._mocked[sUrlType]
        if (
            (!item.rurl || match(item.rurl, options.url)) &&
            (!item.rtype || match(item.rtype, options.type.toLowerCase()))
        ) {
            // console.log('[mock]', options.url, '>', item.rurl)
            return item
        }
    }

    function match(expected, actual) {
        if (Util.type(expected) === 'string') {
            return expected === actual
        }
        if (Util.type(expected) === 'regexp') {
            return expected.test(actual)
        }
    }

}

// 数据模板 => 响应数据
function convert(item, options) {
    if (Util.isFunction(item.template)) {
        var data = item.template(options)
        // 数据模板中的返回参构造处理
        // _status 控制返回状态码
        data._status && data._status !== 0 && (options.status = data._status)
        delete data._status
        return data
    }
    return MockXMLHttpRequest.Mock.mock(item.template)
}

module.exports = MockXMLHttpRequest
Copy the code

Three, usage,

The library’s usage, as shown below, can be viewed in detail on the official website: Mockjs

/ / use the Mock var Mock = the require (' mockjs) var data = Mock the Mock ({/ / the list of attribute values is an array, containing 1 to 10 elements' list | 1-10 ': [{/ / attribute id is the one from the number, the initial value is 1, each time to add 1 'id | + 1:1}]}) / / the output console. The log (JSON. Stringify (data, null, 4))Copy the code

Four,

As you can see from this section, many of the attributes of the XMLHttpRequest interface definition are read-only, so you can only implement the Mock service by emulating it.