| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- /* 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
|