mockjs
Version:
生成随机数据 & 拦截 Ajax 请求
442 lines (384 loc) • 14.9 kB
JavaScript
/* global window, document, location, Event, setTimeout */
/*
## MockXMLHttpRequest
期望的功能:
1. 完整地覆盖原生 XHR 的行为
2. 完整地模拟原生 XHR 的行为
3. 在发起请求时,自动检测是否需要拦截
4. 如果不必拦截,则执行原生 XHR 的行为
5. 如果需要拦截,则执行虚拟 XHR 的行为
6. 兼容 XMLHttpRequest 和 ActiveXObject
new window.XMLHttpRequest()
new window.ActiveXObject("Microsoft.XMLHTTP")
关键方法的逻辑:
* new 此时尚无法确定是否需要拦截,所以创建原生 XHR 对象是必须的。
* open 此时可以取到 URL,可以决定是否进行拦截。
* send 此时已经确定了请求方式。
规范:
http://xhr.spec.whatwg.org/
http://www.w3.org/TR/XMLHttpRequest2/
参考实现:
https://github.com/philikon/MockHttpRequest/blob/master/lib/mock.js
https://github.com/trek/FakeXMLHttpRequest/blob/master/fake_xml_http_request.js
https://github.com/ilinsky/xmlhttprequest/blob/master/XMLHttpRequest.js
https://github.com/firebug/firebug-lite/blob/master/content/lite/xhr.js
https://github.com/thx/RAP/blob/master/lab/rap.plugin.xinglie.js
**需不需要全面重写 XMLHttpRequest?**
http://xhr.spec.whatwg.org/#interface-xmlhttprequest
关键属性 readyState、status、statusText、response、responseText、responseXML 是 readonly,所以,试图通过修改这些状态,来模拟响应是不可行的。
因此,唯一的办法是模拟整个 XMLHttpRequest,就像 jQuery 对事件模型的封装。
// Event handlers
onloadstart loadstart
onprogress progress
onabort abort
onerror error
onload load
ontimeout timeout
onloadend loadend
onreadystatechange readystatechange
*/
var Util = require('../util')
// 备份原生 XMLHttpRequest
window._XMLHttpRequest = window.XMLHttpRequest
window._ActiveXObject = window.ActiveXObject
/*
PhantomJS
TypeError: '[object EventConstructor]' is not a constructor (evaluating 'new Event("readystatechange")')
https://github.com/bluerail/twitter-bootstrap-rails-confirm/issues/18
https://github.com/ariya/phantomjs/issues/11289
*/
try {
new window.Event('custom')
} catch (exception) {
window.Event = function(type, bubbles, cancelable, detail) {
var event = document.createEvent('CustomEvent') // MUST be 'CustomEvent'
event.initCustomEvent(type, bubbles, cancelable, detail)
return event
}
}
var XHR_STATES = {
// The object has been constructed.
UNSENT: 0,
// The open() method has been successfully invoked.
OPENED: 1,
// All redirects (if any) have been followed and all HTTP headers of the response have been received.
HEADERS_RECEIVED: 2,
// The response's body is being received.
LOADING: 3,
// The data transfer has been completed or something went wrong during the transfer (e.g. infinite redirects).
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(' ')
// https://github.com/trek/FakeXMLHttpRequest/blob/master/fake_xml_http_request.js#L32
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, {
// https://xhr.spec.whatwg.org/#the-open()-method
// Sets the request method, request URL, and synchronous flag.
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) {}
}
return
}
// 找到了匹配的数据模板,开始拦截 XHR 请求
this.match = true
this.custom.template = item
this.readyState = MockXMLHttpRequest.OPENED
this.dispatchEvent(new Event('readystatechange' /*, false, false, this*/ ))
},
// https://xhr.spec.whatwg.org/#the-setrequestheader()-method
// Combines a header in author request headers.
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: {},
// https://xhr.spec.whatwg.org/#the-send()-method
// Initiates the request.
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.status = 200
that.statusText = HTTP_STATUS_CODES[200]
// fix #92 #93 by @qddegtya
that.response = that.responseText = JSON.stringify(
convert(that.custom.template, that.custom.options),
null, 4
)
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*/ ));
}
},
// https://xhr.spec.whatwg.org/#the-abort()-method
// Cancels any network activity.
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, {
responseURL: '',
status: MockXMLHttpRequest.UNSENT,
statusText: '',
// https://xhr.spec.whatwg.org/#the-getresponseheader()-method
getResponseHeader: function(name) {
// 原生 XHR
if (!this.match) {
return this.custom.xhr.getResponseHeader(name)
}
// 拦截 XHR
return this.custom.responseHeaders[name.toLowerCase()]
},
// https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method
// http://www.utf8-chartable.de/
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: 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) {
return Util.isFunction(item.template) ?
item.template(options) : MockXMLHttpRequest.Mock.mock(item.template)
}
module.exports = MockXMLHttpRequest