首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Mock | 拦截ajax的两种实现方式

通常在开发中,后端接口未完成,未不影响项目进度,前后端会先行定义好协议规范,各自开发,最后联调。而前端模拟后端数据,通常采用mockjs,拦截ajax请求,返回mock数据,这篇文章一起看看拦截ajax请求的实现方式。以下是采用mockJs的优点:

1.前后端分离, 2.可随机生成大量的数据 3.用法简单 4.数据类型丰富 5.可扩展数据类型 6.在已有接口文档的情况下,我们可以直接按照接口文档来开发,将相应的字段写好,在接口完成 之后,只需要改变url地址即可。

一、模拟XMLHttpRequest

1、Interface-XMLHttpRequest

[Exposed=(Window,DedicatedWorker,SharedWorker)]
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 {
};

enum XMLHttpRequestResponseType {
  "",
  "arraybuffer",
  "blob",
  "document",
  "json",
  "text"
};

[Exposed=(Window,DedicatedWorker,SharedWorker)]
interface XMLHttpRequest : XMLHttpRequestEventTarget {
  constructor();

  // event handler
  attribute EventHandler onreadystatechange;

  // states
  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();

  // response
  readonly attribute USVString responseURL;
  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;
};

通过XMLHttpRequest的接口文档,可以得知属性readyState、status、statusText、response、responseText、responseXML 是 readonly,所以试图通过修改这些状态,来模拟响应是不可行的。因此唯一的办法是模拟整个XMLHttpRequest。

2、模拟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',
    /*
        timeout: 50,
        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
            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

二、webpack的devServer

1、webpack-dev-server

webpack-dev-server提供一个简单的web服务器,并且能够实时重新加载(live reload)。 contentBase:配置告知webpack-dev-server,在localhost:8080下建立服务,将dist目录下的文件作为可访问文件。

before: 提供在服务内部优先于所有其他中间件之前执行的自定义中间件的能力。

因此便可以在before函数内定义mock处理函数,匹配路由,拦截请求,返回mock数据。

2、webpack中实现mock

devServer:{
   ...
   before: require('./mock/mock-server.js')
}

// mock-server.js

const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
const path = require('path')

const mockDir = path.join(process.cwd(), 'mock')

// 注册路由
function registerRoutes(app) {
  let mockLastIndex
  const { default: mocks } = require('./index.js')
  for (const mock of mocks) {
    // 注册mock
    app[mock.type](mock.url, mock.response)
    mockLastIndex = app._router.stack.length
  }
  const mockRoutesLength = Object.keys(mocks).length
  return {
    mockRoutesLength: mockRoutesLength,
    mockStartIndex: mockLastIndex - mockRoutesLength
  }
}

function unregisterRoutes() {
 /*
  * require.cache
  * 多处引用同一个模块,最终只会产生一次模块执行和一次导出。所以,会在运行时(runtime)中会保存一份缓存。删除此缓存,会产生新的模块执行和新的导出。
  */ 
  Object.keys(require.cache).forEach(i => {
    if (i.includes(mockDir)) {
      delete require.cache[require.resolve(i)]
    }
  })
}

module.exports = app => {
  // es6 polyfill
  require('@babel/register')

  // 解析 app.body
  // https://expressjs.com/en/4x/api.html#req.body
  app.use(bodyParser.json())
  app.use(bodyParser.urlencoded({
    extended: true
  }))

  const mockRoutes = registerRoutes(app)
  var mockRoutesLength = mockRoutes.mockRoutesLength
  var mockStartIndex = mockRoutes.mockStartIndex

  // 监听文件,热加载mock-server
  chokidar.watch(mockDir, {
    ignored: /mock-server/,
    ignoreInitial: true
  }).on('all', (event, path) => {
    if (event === 'change' || event === 'add') {
      try {
        // 删除模拟路由堆栈
        app._router.stack.splice(mockStartIndex, mockRoutesLength)

        // 删除路由缓存
        unregisterRoutes()

        // 注册路由
        const mockRoutes = registerRoutes(app)
        mockRoutesLength = mockRoutes.mockRoutesLength
        mockStartIndex = mockRoutes.mockStartIndex

        console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
      } catch (error) {
        console.log(chalk.redBright(error))
      }
    }
  })
}

三、总结

此便为两种拦截ajax请求的方式,模拟XMLHttpRequest以及在node服务植入高于其他中间件执行的mock函数。

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/b439eac4e5e1fb40e39b0ebee
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券