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":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章