2a7117d4576efcfa0a4e5704018e3e9b42de9d054a59d11f96e09ffad42201f81f0e6d56d6c76c7e08272d96b5cb8272cdb97949b3f57f8c540d71d02dad66 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. /* global window, document, location, Event, setTimeout */
  2. /*
  3. ## MockXMLHttpRequest
  4. 期望的功能:
  5. 1. 完整地覆盖原生 XHR 的行为
  6. 2. 完整地模拟原生 XHR 的行为
  7. 3. 在发起请求时,自动检测是否需要拦截
  8. 4. 如果不必拦截,则执行原生 XHR 的行为
  9. 5. 如果需要拦截,则执行虚拟 XHR 的行为
  10. 6. 兼容 XMLHttpRequest 和 ActiveXObject
  11. new window.XMLHttpRequest()
  12. new window.ActiveXObject("Microsoft.XMLHTTP")
  13. 关键方法的逻辑:
  14. * new 此时尚无法确定是否需要拦截,所以创建原生 XHR 对象是必须的。
  15. * open 此时可以取到 URL,可以决定是否进行拦截。
  16. * send 此时已经确定了请求方式。
  17. 规范:
  18. http://xhr.spec.whatwg.org/
  19. http://www.w3.org/TR/XMLHttpRequest2/
  20. 参考实现:
  21. https://github.com/philikon/MockHttpRequest/blob/master/lib/mock.js
  22. https://github.com/trek/FakeXMLHttpRequest/blob/master/fake_xml_http_request.js
  23. https://github.com/ilinsky/xmlhttprequest/blob/master/XMLHttpRequest.js
  24. https://github.com/firebug/firebug-lite/blob/master/content/lite/xhr.js
  25. https://github.com/thx/RAP/blob/master/lab/rap.plugin.xinglie.js
  26. **需不需要全面重写 XMLHttpRequest?**
  27. http://xhr.spec.whatwg.org/#interface-xmlhttprequest
  28. 关键属性 readyState、status、statusText、response、responseText、responseXML 是 readonly,所以,试图通过修改这些状态,来模拟响应是不可行的。
  29. 因此,唯一的办法是模拟整个 XMLHttpRequest,就像 jQuery 对事件模型的封装。
  30. // Event handlers
  31. onloadstart loadstart
  32. onprogress progress
  33. onabort abort
  34. onerror error
  35. onload load
  36. ontimeout timeout
  37. onloadend loadend
  38. onreadystatechange readystatechange
  39. */
  40. var Util = require('../util')
  41. // 备份原生 XMLHttpRequest
  42. window._XMLHttpRequest = window.XMLHttpRequest
  43. window._ActiveXObject = window.ActiveXObject
  44. /*
  45. PhantomJS
  46. TypeError: '[object EventConstructor]' is not a constructor (evaluating 'new Event("readystatechange")')
  47. https://github.com/bluerail/twitter-bootstrap-rails-confirm/issues/18
  48. https://github.com/ariya/phantomjs/issues/11289
  49. */
  50. try {
  51. new window.Event('custom')
  52. } catch (exception) {
  53. window.Event = function(type, bubbles, cancelable, detail) {
  54. var event = document.createEvent('CustomEvent') // MUST be 'CustomEvent'
  55. event.initCustomEvent(type, bubbles, cancelable, detail)
  56. return event
  57. }
  58. }
  59. var XHR_STATES = {
  60. // The object has been constructed.
  61. UNSENT: 0,
  62. // The open() method has been successfully invoked.
  63. OPENED: 1,
  64. // All redirects (if any) have been followed and all HTTP headers of the response have been received.
  65. HEADERS_RECEIVED: 2,
  66. // The response's body is being received.
  67. LOADING: 3,
  68. // The data transfer has been completed or something went wrong during the transfer (e.g. infinite redirects).
  69. DONE: 4
  70. }
  71. var XHR_EVENTS = 'readystatechange loadstart progress abort error load timeout loadend'.split(' ')
  72. var XHR_REQUEST_PROPERTIES = 'timeout withCredentials'.split(' ')
  73. var XHR_RESPONSE_PROPERTIES = 'readyState responseURL status statusText responseType response responseText responseXML'.split(' ')
  74. // https://github.com/trek/FakeXMLHttpRequest/blob/master/fake_xml_http_request.js#L32
  75. var HTTP_STATUS_CODES = {
  76. 100: "Continue",
  77. 101: "Switching Protocols",
  78. 200: "OK",
  79. 201: "Created",
  80. 202: "Accepted",
  81. 203: "Non-Authoritative Information",
  82. 204: "No Content",
  83. 205: "Reset Content",
  84. 206: "Partial Content",
  85. 300: "Multiple Choice",
  86. 301: "Moved Permanently",
  87. 302: "Found",
  88. 303: "See Other",
  89. 304: "Not Modified",
  90. 305: "Use Proxy",
  91. 307: "Temporary Redirect",
  92. 400: "Bad Request",
  93. 401: "Unauthorized",
  94. 402: "Payment Required",
  95. 403: "Forbidden",
  96. 404: "Not Found",
  97. 405: "Method Not Allowed",
  98. 406: "Not Acceptable",
  99. 407: "Proxy Authentication Required",
  100. 408: "Request Timeout",
  101. 409: "Conflict",
  102. 410: "Gone",
  103. 411: "Length Required",
  104. 412: "Precondition Failed",
  105. 413: "Request Entity Too Large",
  106. 414: "Request-URI Too Long",
  107. 415: "Unsupported Media Type",
  108. 416: "Requested Range Not Satisfiable",
  109. 417: "Expectation Failed",
  110. 422: "Unprocessable Entity",
  111. 500: "Internal Server Error",
  112. 501: "Not Implemented",
  113. 502: "Bad Gateway",
  114. 503: "Service Unavailable",
  115. 504: "Gateway Timeout",
  116. 505: "HTTP Version Not Supported"
  117. }
  118. /*
  119. MockXMLHttpRequest
  120. */
  121. function MockXMLHttpRequest() {
  122. // 初始化 custom 对象,用于存储自定义属性
  123. this.custom = {
  124. events: {},
  125. requestHeaders: {},
  126. responseHeaders: {}
  127. }
  128. }
  129. MockXMLHttpRequest._settings = {
  130. timeout: '10-100',
  131. /*
  132. timeout: 50,
  133. timeout: '10-100',
  134. */
  135. }
  136. MockXMLHttpRequest.setup = function(settings) {
  137. Util.extend(MockXMLHttpRequest._settings, settings)
  138. return MockXMLHttpRequest._settings
  139. }
  140. Util.extend(MockXMLHttpRequest, XHR_STATES)
  141. Util.extend(MockXMLHttpRequest.prototype, XHR_STATES)
  142. // 标记当前对象为 MockXMLHttpRequest
  143. MockXMLHttpRequest.prototype.mock = true
  144. // 是否拦截 Ajax 请求
  145. MockXMLHttpRequest.prototype.match = false
  146. // 初始化 Request 相关的属性和方法
  147. Util.extend(MockXMLHttpRequest.prototype, {
  148. // https://xhr.spec.whatwg.org/#the-open()-method
  149. // Sets the request method, request URL, and synchronous flag.
  150. open: function(method, url, async, username, password) {
  151. var that = this
  152. Util.extend(this.custom, {
  153. method: method,
  154. url: url,
  155. async: typeof async === 'boolean' ? async : true,
  156. username: username,
  157. password: password,
  158. options: {
  159. url: url,
  160. type: method
  161. }
  162. })
  163. this.custom.timeout = function(timeout) {
  164. if (typeof timeout === 'number') return timeout
  165. if (typeof timeout === 'string' && !~timeout.indexOf('-')) return parseInt(timeout, 10)
  166. if (typeof timeout === 'string' && ~timeout.indexOf('-')) {
  167. var tmp = timeout.split('-')
  168. var min = parseInt(tmp[0], 10)
  169. var max = parseInt(tmp[1], 10)
  170. return Math.round(Math.random() * (max - min)) + min
  171. }
  172. }(MockXMLHttpRequest._settings.timeout)
  173. // 查找与请求参数匹配的数据模板
  174. var item = find(this.custom.options)
  175. function handle(event) {
  176. // 同步属性 NativeXMLHttpRequest => MockXMLHttpRequest
  177. for (var i = 0; i < XHR_RESPONSE_PROPERTIES.length; i++) {
  178. try {
  179. that[XHR_RESPONSE_PROPERTIES[i]] = xhr[XHR_RESPONSE_PROPERTIES[i]]
  180. } catch (e) {}
  181. }
  182. // 触发 MockXMLHttpRequest 上的同名事件
  183. that.dispatchEvent(new Event(event.type /*, false, false, that*/ ))
  184. }
  185. // 如果未找到匹配的数据模板,则采用原生 XHR 发送请求。
  186. if (!item) {
  187. // 创建原生 XHR 对象,调用原生 open(),监听所有原生事件
  188. var xhr = createNativeXMLHttpRequest()
  189. this.custom.xhr = xhr
  190. // 初始化所有事件,用于监听原生 XHR 对象的事件
  191. for (var i = 0; i < XHR_EVENTS.length; i++) {
  192. xhr.addEventListener(XHR_EVENTS[i], handle)
  193. }
  194. // xhr.open()
  195. if (username) xhr.open(method, url, async, username, password)
  196. else xhr.open(method, url, async)
  197. // 同步属性 MockXMLHttpRequest => NativeXMLHttpRequest
  198. for (var j = 0; j < XHR_REQUEST_PROPERTIES.length; j++) {
  199. try {
  200. xhr[XHR_REQUEST_PROPERTIES[j]] = that[XHR_REQUEST_PROPERTIES[j]]
  201. } catch (e) {}
  202. }
  203. return
  204. }
  205. // 找到了匹配的数据模板,开始拦截 XHR 请求
  206. this.match = true
  207. this.custom.template = item
  208. this.readyState = MockXMLHttpRequest.OPENED
  209. this.dispatchEvent(new Event('readystatechange' /*, false, false, this*/ ))
  210. },
  211. // https://xhr.spec.whatwg.org/#the-setrequestheader()-method
  212. // Combines a header in author request headers.
  213. setRequestHeader: function(name, value) {
  214. // 原生 XHR
  215. if (!this.match) {
  216. this.custom.xhr.setRequestHeader(name, value)
  217. return
  218. }
  219. // 拦截 XHR
  220. var requestHeaders = this.custom.requestHeaders
  221. if (requestHeaders[name]) requestHeaders[name] += ',' + value
  222. else requestHeaders[name] = value
  223. },
  224. timeout: 0,
  225. withCredentials: false,
  226. upload: {},
  227. // https://xhr.spec.whatwg.org/#the-send()-method
  228. // Initiates the request.
  229. send: function send(data) {
  230. var that = this
  231. this.custom.options.body = data
  232. // 原生 XHR
  233. if (!this.match) {
  234. this.custom.xhr.send(data)
  235. return
  236. }
  237. // 拦截 XHR
  238. // X-Requested-With header
  239. this.setRequestHeader('X-Requested-With', 'MockXMLHttpRequest')
  240. // loadstart The fetch initiates.
  241. this.dispatchEvent(new Event('loadstart' /*, false, false, this*/ ))
  242. if (this.custom.async) setTimeout(done, this.custom.timeout) // 异步
  243. else done() // 同步
  244. function done() {
  245. that.readyState = MockXMLHttpRequest.HEADERS_RECEIVED
  246. that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
  247. that.readyState = MockXMLHttpRequest.LOADING
  248. that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
  249. that.status = 200
  250. that.statusText = HTTP_STATUS_CODES[200]
  251. // fix #92 #93 by @qddegtya
  252. that.response = that.responseText = JSON.stringify(
  253. convert(that.custom.template, that.custom.options),
  254. null, 4
  255. )
  256. that.readyState = MockXMLHttpRequest.DONE
  257. that.dispatchEvent(new Event('readystatechange' /*, false, false, that*/ ))
  258. that.dispatchEvent(new Event('load' /*, false, false, that*/ ));
  259. that.dispatchEvent(new Event('loadend' /*, false, false, that*/ ));
  260. }
  261. },
  262. // https://xhr.spec.whatwg.org/#the-abort()-method
  263. // Cancels any network activity.
  264. abort: function abort() {
  265. // 原生 XHR
  266. if (!this.match) {
  267. this.custom.xhr.abort()
  268. return
  269. }
  270. // 拦截 XHR
  271. this.readyState = MockXMLHttpRequest.UNSENT
  272. this.dispatchEvent(new Event('abort', false, false, this))
  273. this.dispatchEvent(new Event('error', false, false, this))
  274. }
  275. })
  276. // 初始化 Response 相关的属性和方法
  277. Util.extend(MockXMLHttpRequest.prototype, {
  278. responseURL: '',
  279. status: MockXMLHttpRequest.UNSENT,
  280. statusText: '',
  281. // https://xhr.spec.whatwg.org/#the-getresponseheader()-method
  282. getResponseHeader: function(name) {
  283. // 原生 XHR
  284. if (!this.match) {
  285. return this.custom.xhr.getResponseHeader(name)
  286. }
  287. // 拦截 XHR
  288. return this.custom.responseHeaders[name.toLowerCase()]
  289. },
  290. // https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method
  291. // http://www.utf8-chartable.de/
  292. getAllResponseHeaders: function() {
  293. // 原生 XHR
  294. if (!this.match) {
  295. return this.custom.xhr.getAllResponseHeaders()
  296. }
  297. // 拦截 XHR
  298. var responseHeaders = this.custom.responseHeaders
  299. var headers = ''
  300. for (var h in responseHeaders) {
  301. if (!responseHeaders.hasOwnProperty(h)) continue
  302. headers += h + ': ' + responseHeaders[h] + '\r\n'
  303. }
  304. return headers
  305. },
  306. overrideMimeType: function( /*mime*/ ) {},
  307. responseType: '', // '', 'text', 'arraybuffer', 'blob', 'document', 'json'
  308. response: null,
  309. responseText: '',
  310. responseXML: null
  311. })
  312. // EventTarget
  313. Util.extend(MockXMLHttpRequest.prototype, {
  314. addEventListener: function addEventListener(type, handle) {
  315. var events = this.custom.events
  316. if (!events[type]) events[type] = []
  317. events[type].push(handle)
  318. },
  319. removeEventListener: function removeEventListener(type, handle) {
  320. var handles = this.custom.events[type] || []
  321. for (var i = 0; i < handles.length; i++) {
  322. if (handles[i] === handle) {
  323. handles.splice(i--, 1)
  324. }
  325. }
  326. },
  327. dispatchEvent: function dispatchEvent(event) {
  328. var handles = this.custom.events[event.type] || []
  329. for (var i = 0; i < handles.length; i++) {
  330. handles[i].call(this, event)
  331. }
  332. var ontype = 'on' + event.type
  333. if (this[ontype]) this[ontype](event)
  334. }
  335. })
  336. // Inspired by jQuery
  337. function createNativeXMLHttpRequest() {
  338. var isLocal = function() {
  339. var rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/
  340. var rurl = /^([\w.+-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/
  341. var ajaxLocation = location.href
  342. var ajaxLocParts = rurl.exec(ajaxLocation.toLowerCase()) || []
  343. return rlocalProtocol.test(ajaxLocParts[1])
  344. }()
  345. return window.ActiveXObject ?
  346. (!isLocal && createStandardXHR() || createActiveXHR()) : createStandardXHR()
  347. function createStandardXHR() {
  348. try {
  349. return new window._XMLHttpRequest();
  350. } catch (e) {}
  351. }
  352. function createActiveXHR() {
  353. try {
  354. return new window._ActiveXObject("Microsoft.XMLHTTP");
  355. } catch (e) {}
  356. }
  357. }
  358. // 查找与请求参数匹配的数据模板:URL,Type
  359. function find(options) {
  360. for (var sUrlType in MockXMLHttpRequest.Mock._mocked) {
  361. var item = MockXMLHttpRequest.Mock._mocked[sUrlType]
  362. if (
  363. (!item.rurl || match(item.rurl, options.url)) &&
  364. (!item.rtype || match(item.rtype, options.type.toLowerCase()))
  365. ) {
  366. // console.log('[mock]', options.url, '>', item.rurl)
  367. return item
  368. }
  369. }
  370. function match(expected, actual) {
  371. if (Util.type(expected) === 'string') {
  372. return expected === actual
  373. }
  374. if (Util.type(expected) === 'regexp') {
  375. return expected.test(actual)
  376. }
  377. }
  378. }
  379. // 数据模板 => 响应数据
  380. function convert(item, options) {
  381. return Util.isFunction(item.template) ?
  382. item.template(options) : MockXMLHttpRequest.Mock.mock(item.template)
  383. }
  384. module.exports = MockXMLHttpRequest