写点什么

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

用户头像
梁龙先森
关注
发布于: 2020 年 12 月 09 日
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函数。

发布于: 2020 年 12 月 09 日阅读数: 472
用户头像

梁龙先森

关注

寒江孤影,江湖故人,相逢何必曾相识。 2018.03.17 加入

1月的计划是:重学JS,点个关注,一起学习。

评论

发布
暂无评论
Mock | 拦截ajax的两种实现方式