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

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常在开发中,后端接口未完成,未不影响项目进度,前后端会先行定义好协议规范,各自开发,最后联调。而前端模拟后端数据,通常采用mockjs,拦截ajax请求,返回mock数据,这篇文章一起看看拦截ajax请求的实现方式。以下是采用mockJs的优点:","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.前后端分离,","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.可随机生成大量的数据","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.用法简单","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.数据类型丰富","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5.可扩展数据类型","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"6.在已有接口文档的情况下,我们可以直接按照接口文档来开发,将相应的字段写好,在接口完成 之后,只需要改变url地址即可。","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"一、模拟XMLHttpRequest","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1、Interface-XMLHttpRequest","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"[Exposed=(Window,DedicatedWorker,SharedWorker)]\ninterface XMLHttpRequestEventTarget : EventTarget {\n // event handlers\n attribute EventHandler onloadstart;\n attribute EventHandler onprogress;\n attribute EventHandler onabort;\n attribute EventHandler onerror;\n attribute EventHandler onload;\n attribute EventHandler ontimeout;\n attribute EventHandler onloadend;\n};\n\n[Exposed=(Window,DedicatedWorker,SharedWorker)]\ninterface XMLHttpRequestUpload : XMLHttpRequestEventTarget {\n};\n\nenum XMLHttpRequestResponseType {\n \"\",\n \"arraybuffer\",\n \"blob\",\n \"document\",\n \"json\",\n \"text\"\n};\n\n[Exposed=(Window,DedicatedWorker,SharedWorker)]\ninterface XMLHttpRequest : XMLHttpRequestEventTarget {\n constructor();\n\n // event handler\n attribute EventHandler onreadystatechange;\n\n // states\n const unsigned short UNSENT = 0;\n const unsigned short OPENED = 1;\n const unsigned short HEADERS_RECEIVED = 2;\n const unsigned short LOADING = 3;\n const unsigned short DONE = 4;\n readonly attribute unsigned short readyState;\n\n // request\n undefined open(ByteString method, USVString url);\n undefined open(ByteString method, USVString url, boolean async, optional USVString? username = null, optional USVString? password = null);\n undefined setRequestHeader(ByteString name, ByteString value);\n attribute unsigned long timeout;\n attribute boolean withCredentials;\n [SameObject] readonly attribute XMLHttpRequestUpload upload;\n undefined send(optional (Document or XMLHttpRequestBodyInit)? body = null);\n undefined abort();\n\n // response\n readonly attribute USVString responseURL;\n readonly attribute unsigned short status;\n readonly attribute ByteString statusText;\n ByteString? getResponseHeader(ByteString name);\n ByteString getAllResponseHeaders();\n undefined overrideMimeType(DOMString mime);\n attribute XMLHttpRequestResponseType responseType;\n readonly attribute any response;\n readonly attribute USVString responseText;\n [Exposed=Window] readonly attribute Document? responseXML;\n};","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过XMLHttpRequest的接口文档,可以得知属性readyState、status、statusText、response、responseText、responseXML 是 readonly,所以试图通过修改这些状态,来模拟响应是不可行的。因此唯一的办法是模拟整个XMLHttpRequest。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2、模拟XMLHttpRequest","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// 源码出处:https://github.com/sendya/Mock NPM:mockjs2\n\nvar XHR_STATES = {\n // 初始化状态。XMLHttpRequest 对象已创建或已被 abort() 方法重置\n UNSENT: 0,\n // open() 方法已调用,但是 send() 方法未调用。请求还没有被发送\n OPENED: 1,\n // Send() 方法已调用,HTTP 请求已发送到 Web 服务器。未接收到响应\n HEADERS_RECEIVED: 2,\n // 所有响应头部都已经接收到。响应体开始接收但未完成\n LOADING: 3,\n // HTTP 响应已经完全接收\n DONE: 4\n}\n\nvar XHR_EVENTS = 'readystatechange loadstart progress abort error load timeout loadend'.split(' ')\nvar XHR_REQUEST_PROPERTIES = 'timeout withCredentials'.split(' ')\nvar XHR_RESPONSE_PROPERTIES = 'readyState responseURL status statusText responseType response responseText responseXML'.split(' ')\n\n// http状态码\nvar HTTP_STATUS_CODES = {\n 100: \"Continue\",\n 101: \"Switching Protocols\",\n 200: \"OK\",\n 201: \"Created\",\n 202: \"Accepted\",\n 203: \"Non-Authoritative Information\",\n 204: \"No Content\",\n 205: \"Reset Content\",\n 206: \"Partial Content\",\n 300: \"Multiple Choice\",\n 301: \"Moved Permanently\",\n 302: \"Found\",\n 303: \"See Other\",\n 304: \"Not Modified\",\n 305: \"Use Proxy\",\n 307: \"Temporary Redirect\",\n 400: \"Bad Request\",\n 401: \"Unauthorized\",\n 402: \"Payment Required\",\n 403: \"Forbidden\",\n 404: \"Not Found\",\n 405: \"Method Not Allowed\",\n 406: \"Not Acceptable\",\n 407: \"Proxy Authentication Required\",\n 408: \"Request Timeout\",\n 409: \"Conflict\",\n 410: \"Gone\",\n 411: \"Length Required\",\n 412: \"Precondition Failed\",\n 413: \"Request Entity Too Large\",\n 414: \"Request-URI Too Long\",\n 415: \"Unsupported Media Type\",\n 416: \"Requested Range Not Satisfiable\",\n 417: \"Expectation Failed\",\n 422: \"Unprocessable Entity\",\n 500: \"Internal Server Error\",\n 501: \"Not Implemented\",\n 502: \"Bad Gateway\",\n 503: \"Service Unavailable\",\n 504: \"Gateway Timeout\",\n 505: \"HTTP Version Not Supported\"\n}\n\n/*\n MockXMLHttpRequest\n*/\n\nfunction MockXMLHttpRequest() {\n // 初始化 custom 对象,用于存储自定义属性\n this.custom = {\n events: {},\n requestHeaders: {},\n responseHeaders: {}\n }\n}\n\nMockXMLHttpRequest._settings = {\n timeout: '10-100',\n /*\n timeout: 50,\n timeout: '10-100',\n */\n}\n\nMockXMLHttpRequest.setup = function(settings) {\n Util.extend(MockXMLHttpRequest._settings, settings)\n return MockXMLHttpRequest._settings\n}\n\nUtil.extend(MockXMLHttpRequest, XHR_STATES)\nUtil.extend(MockXMLHttpRequest.prototype, XHR_STATES)\n\n// 标记当前对象为 MockXMLHttpRequest\nMockXMLHttpRequest.prototype.mock = true\n\n// 是否拦截 Ajax 请求\nMockXMLHttpRequest.prototype.match = false\n\n// 初始化 Request 相关的属性和方法\nUtil.extend(MockXMLHttpRequest.prototype, {\n // 初始化一个请求\n open: function(method, url, async, username, password) {\n var that = this\n\n Util.extend(this.custom, {\n method: method,\n url: url,\n async: typeof async === 'boolean' ? async : true,\n username: username,\n password: password,\n options: {\n url: url,\n type: method\n }\n })\n\n this.custom.timeout = function(timeout) {\n if (typeof timeout === 'number') return timeout\n if (typeof timeout === 'string' && !~timeout.indexOf('-')) return parseInt(timeout, 10)\n if (typeof timeout === 'string' && ~timeout.indexOf('-')) {\n var tmp = timeout.split('-')\n var min = parseInt(tmp[0], 10)\n var max = parseInt(tmp[1], 10)\n return Math.round(Math.random() * (max - min)) + min\n }\n }(MockXMLHttpRequest._settings.timeout)\n\n // 查找与请求参数匹配的数据模板\n var item = find(this.custom.options)\n\n function handle(event) {\n // 同步属性 NativeXMLHttpRequest => MockXMLHttpRequest\n for (var i = 0; i < XHR_RESPONSE_PROPERTIES.length; i++) {\n try {\n that[XHR_RESPONSE_PROPERTIES[i]] = xhr[XHR_RESPONSE_PROPERTIES[i]]\n } catch (e) {}\n }\n // 触发 MockXMLHttpRequest 上的同名事件\n that.dispatchEvent(new Event(event.type /*, false, false, that*/ ))\n }\n\n // 如果未找到匹配的数据模板,则采用原生 XHR 发送请求。\n if (!item) {\n // 创建原生 XHR 对象,调用原生 open(),监听所有原生事件\n var xhr = createNativeXMLHttpRequest()\n this.custom.xhr = xhr\n\n // 初始化所有事件,用于监听原生 XHR 对象的事件\n for (var i = 0; i < XHR_EVENTS.length; i++) {\n xhr.addEventListener(XHR_EVENTS[i], handle)\n }\n\n // xhr.open()\n if (username) xhr.open(method, url, async, username, password)\n else xhr.open(method, url, async)\n\n // 同步属性 MockXMLHttpRequest => NativeXMLHttpRequest\n for (var j = 0; j < XHR_REQUEST_PROPERTIES.length; j++) {\n try {\n xhr[XHR_REQUEST_PROPERTIES[j]] = that[XHR_REQUEST_PROPERTIES[j]]\n } catch (e) {}\n }\n\n // 这里的核心问题就是没考虑到在open以后去修改属性 比如axios修改responseType的行为就在open之后\n Object.defineProperty(that, 'responseType', {\n get: function () {\n return xhr.responseType\n },\n set: function (v) {\n return (xhr.responseType = v)\n }\n });\n\n return\n }\n\n // 找到了匹配的数据模板,开始拦截 XHR 请求\n this.match = true\n this.custom.template = item\n // 当 readyState 属性发生变化时,调用 readystatechange\n this.readyState = MockXMLHttpRequest.OPENED\n this.dispatchEvent(new Event('readystatechange' /*, false, false, this*/ ))\n },\n // 设置HTTP请求头的值。必须在open()之后、send()之前调用\n setRequestHeader: function(name, value) {\n // 原生 XHR\n if (!this.match) {\n this.custom.xhr.setRequestHeader(name, value)\n return\n }\n\n // 拦截 XHR\n var requestHeaders = this.custom.requestHeaders\n if (requestHeaders[name]) requestHeaders[name] += ',' + value\n else requestHeaders[name] = value\n },\n timeout: 0,\n withCredentials: false,\n upload: {},\n // 发送请求。如果请求是异步的(默认),那么该方法将在请求发送后立即返回\n send: function send(data) {\n var that = this\n this.custom.options.body = data\n\n // 原生 XHR\n if (!this.match) {\n this.custom.xhr.send(data)\n return\n }\n\n // 拦截 XHR\n\n // X-Requested-With header\n this.setRequestHeader('X-Requested-With', 'MockXMLHttpRequest')\n\n // loadstart The fetch initiates.\n this.dispatchEvent(new Event('loadstart' /*, false, false, this*/ ))\n\n if (this.custom.async) setTimeout(done, this.custom.timeout) // 异步\n else done() // 同步\n\n function done() {\n that.readyState = MockXMLHttpRequest.HEADERS_RECEIVED\n that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))\n that.readyState = MockXMLHttpRequest.LOADING\n that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))\n\n that.response = that.responseText = JSON.stringify(\n convert(that.custom.template, that.custom.options),\n null, 4\n )\n\n that.status = that.custom.options.status || 200\n that.statusText = HTTP_STATUS_CODES[that.status]\n\n that.readyState = MockXMLHttpRequest.DONE\n that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))\n that.dispatchEvent(new Event('load' /*, false, false, that*/ ));\n that.dispatchEvent(new Event('loadend' /*, false, false, that*/ ));\n }\n },\n // 当request被停止时触发,例如调用XMLHttpRequest.abort()\n abort: function abort() {\n // 原生 XHR\n if (!this.match) {\n this.custom.xhr.abort()\n return\n }\n\n // 拦截 XHR\n this.readyState = MockXMLHttpRequest.UNSENT\n this.dispatchEvent(new Event('abort', false, false, this))\n this.dispatchEvent(new Event('error', false, false, this))\n }\n})\n\n// 初始化 Response 相关的属性和方法\nUtil.extend(MockXMLHttpRequest.prototype, {\n // 返回经过序列化(serialized)的响应 URL,如果该 URL 为空,则返回空字符串。\n responseURL: '',\n // 代表请求的响应状态\n status: MockXMLHttpRequest.UNSENT,\n /*\n * 返回一个 DOMString,其中包含 HTTP 服务器返回的响应状态。与 XMLHTTPRequest.status 不同的是,\n * 它包含完整的响应状态文本(例如,\"200 OK\")。\n */ \n statusText: '',\n // 返回包含指定响应头的字符串,如果响应尚未收到或响应中不存在该报头,则返回 null。\n getResponseHeader: function(name) {\n // 原生 XHR\n if (!this.match) {\n return this.custom.xhr.getResponseHeader(name)\n }\n\n // 拦截 XHR\n return this.custom.responseHeaders[name.toLowerCase()]\n },\n // 以字符串的形式返回所有用 CRLF 分隔的响应头,如果没有收到响应,则返回 null。\n getAllResponseHeaders: function() {\n // 原生 XHR\n if (!this.match) {\n return this.custom.xhr.getAllResponseHeaders()\n }\n\n // 拦截 XHR\n var responseHeaders = this.custom.responseHeaders\n var headers = ''\n for (var h in responseHeaders) {\n if (!responseHeaders.hasOwnProperty(h)) continue\n headers += h + ': ' + responseHeaders[h] + '\\r\\n'\n }\n return headers\n },\n overrideMimeType: function( /*mime*/ ) {},\n // 一个用于定义响应类型的枚举值\n responseType: '', // '', 'text', 'arraybuffer', 'blob', 'document', 'json'\n // 包含整个响应实体(response entity body)\n response: null,\n // 返回一个 DOMString,该 DOMString 包含对请求的响应,如果请求未成功或尚未发送,则返回 null。\n responseText: '',\n responseXML: null\n})\n\n// EventTarget\nUtil.extend(MockXMLHttpRequest.prototype, {\n addEventListener: function addEventListener(type, handle) {\n var events = this.custom.events\n if (!events[type]) events[type] = []\n events[type].push(handle)\n },\n removeEventListener: function removeEventListener(type, handle) {\n var handles = this.custom.events[type] || []\n for (var i = 0; i < handles.length; i++) {\n if (handles[i] === handle) {\n handles.splice(i--, 1)\n }\n }\n },\n dispatchEvent: function dispatchEvent(event) {\n var handles = this.custom.events[event.type] || []\n for (var i = 0; i < handles.length; i++) {\n handles[i].call(this, event)\n }\n\n var ontype = 'on' + event.type\n if (this[ontype]) this[ontype](event)\n }\n})\n\n// Inspired by jQuery\nfunction createNativeXMLHttpRequest() {\n var isLocal = function() {\n var rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/\n var rurl = /^([\\w.+-]+:)(?:\\/\\/([^\\/?#:]*)(?::(\\d+)|)|)/\n var ajaxLocation = location.href\n var ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || []\n return rlocalProtocol.test(ajaxLocParts[1])\n }()\n\n return window.ActiveXObject ?\n (!isLocal && createStandardXHR() || createActiveXHR()) : createStandardXHR()\n\n function createStandardXHR() {\n try {\n return new window._XMLHttpRequest();\n } catch (e) {}\n }\n\n function createActiveXHR() {\n try {\n return new window._ActiveXObject(\"Microsoft.XMLHTTP\");\n } catch (e) {}\n }\n}\n\n\n// 查找与请求参数匹配的数据模板:URL,Type\nfunction find(options) {\n\n for (var sUrlType in MockXMLHttpRequest.Mock._mocked) {\n var item = MockXMLHttpRequest.Mock._mocked[sUrlType]\n if (\n (!item.rurl || match(item.rurl, options.url)) &&\n (!item.rtype || match(item.rtype, options.type.toLowerCase()))\n ) {\n // console.log('[mock]', options.url, '>', item.rurl)\n return item\n }\n }\n\n function match(expected, actual) {\n if (Util.type(expected) === 'string') {\n return expected === actual\n }\n if (Util.type(expected) === 'regexp') {\n return expected.test(actual)\n }\n }\n\n}\n\n// 数据模板 => 响应数据\nfunction convert(item, options) {\n if (Util.isFunction(item.template)) {\n var data = item.template(options)\n // 数据模板中的返回参构造处理\n // _status 控制返回状态码\n data._status && data._status !== 0 && (options.status = data._status)\n delete data._status\n return data\n }\n return MockXMLHttpRequest.Mock.mock(item.template)\n}\n\nmodule.exports = MockXMLHttpRequest\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二、webpack的devServer","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1、webpack-dev-server","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"webpack-dev-server提供一个简单的web服务器,并且能够实时重新加载(live reload)。 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"contentBase:","attrs":{}},{"type":"text","text":"配置告知webpack-dev-server,在localhost:8080下建立服务,将dist目录下的文件作为可访问文件。 ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"before:","attrs":{}},{"type":"text","text":" 提供在服务内部优先于所有其他中间件之前执行的自定义中间件的能力。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此便可以在before函数内定义mock处理函数,匹配路由,拦截请求,返回mock数据。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"2、webpack中实现mock","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"devServer:{\n ...\n before: require('./mock/mock-server.js')\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"// mock-server.js\n\nconst chokidar = require('chokidar')\nconst bodyParser = require('body-parser')\nconst chalk = require('chalk')\nconst path = require('path')\n\nconst mockDir = path.join(process.cwd(), 'mock')\n\n// 注册路由\nfunction registerRoutes(app) {\n let mockLastIndex\n const { default: mocks } = require('./index.js')\n for (const mock of mocks) {\n // 注册mock\n app[mock.type](mock.url, mock.response)\n mockLastIndex = app._router.stack.length\n }\n const mockRoutesLength = Object.keys(mocks).length\n return {\n mockRoutesLength: mockRoutesLength,\n mockStartIndex: mockLastIndex - mockRoutesLength\n }\n}\n\nfunction unregisterRoutes() {\n /*\n * require.cache\n * 多处引用同一个模块,最终只会产生一次模块执行和一次导出。所以,会在运行时(runtime)中会保存一份缓存。删除此缓存,会产生新的模块执行和新的导出。\n */ \n Object.keys(require.cache).forEach(i => {\n if (i.includes(mockDir)) {\n delete require.cache[require.resolve(i)]\n }\n })\n}\n\nmodule.exports = app => {\n // es6 polyfill\n require('@babel/register')\n\n // 解析 app.body\n // https://expressjs.com/en/4x/api.html#req.body\n app.use(bodyParser.json())\n app.use(bodyParser.urlencoded({\n extended: true\n }))\n\n const mockRoutes = registerRoutes(app)\n var mockRoutesLength = mockRoutes.mockRoutesLength\n var mockStartIndex = mockRoutes.mockStartIndex\n\n // 监听文件,热加载mock-server\n chokidar.watch(mockDir, {\n ignored: /mock-server/,\n ignoreInitial: true\n }).on('all', (event, path) => {\n if (event === 'change' || event === 'add') {\n try {\n // 删除模拟路由堆栈\n app._router.stack.splice(mockStartIndex, mockRoutesLength)\n\n // 删除路由缓存\n unregisterRoutes()\n\n // 注册路由\n const mockRoutes = registerRoutes(app)\n mockRoutesLength = mockRoutes.mockRoutesLength\n mockStartIndex = mockRoutes.mockStartIndex\n\n console.log(chalk.magentaBright(`\\n > Mock Server hot reload success! changed ${path}`))\n } catch (error) {\n console.log(chalk.redBright(error))\n }\n }\n })\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"三、总结","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此便为两种拦截ajax请求的方式,模拟XMLHttpRequest以及在node服务植入高于其他中间件执行的mock函数。","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章