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:mockjs2var 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)// 标记当前对象为 MockXMLHttpRequestMockXMLHttpRequest.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})// EventTargetUtil.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 jQueryfunction 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,Typefunction 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.jsconst 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 作者【梁龙先森】的原创文章。
原文链接:【http://xie.infoq.cn/article/b439eac4e5e1fb40e39b0ebee】。文章转载请联系作者。
梁龙先森
寒江孤影,江湖故人,相逢何必曾相识。 2018.03.17 加入
1月的计划是:重学JS,点个关注,一起学习。
评论