1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | var ajaxHooker = function() { |
8 | 'use strict'; |
9 | const version = '1.4.3'; |
10 | const hookInst = { |
11 | hookFns: [], |
12 | filters: [] |
13 | }; |
14 | const win = window.unsafeWindow || document.defaultView || window; |
15 | let winAh = win.__ajaxHooker; |
16 | const resProto = win.Response.prototype; |
17 | const xhrResponses = ['response', 'responseText', 'responseXML']; |
18 | const fetchResponses = ['arrayBuffer', 'blob', 'formData', 'json', 'text']; |
19 | const fetchInitProps = ['method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect', |
20 | 'referrer', 'referrerPolicy', 'integrity', 'keepalive', 'signal', 'priority']; |
21 | const xhrAsyncEvents = ['readystatechange', 'load', 'loadend']; |
22 | const getType = ({}).toString.call.bind(({}).toString); |
23 | const getDescriptor = Object.getOwnPropertyDescriptor.bind(Object); |
24 | const emptyFn = () => {}; |
25 | const errorFn = e => console.error(e); |
26 | function isThenable(obj) { |
27 | return obj && ['object', 'function'].includes(typeof obj) && typeof obj.then === 'function'; |
28 | } |
29 | function catchError(fn, ...args) { |
30 | try { |
31 | const result = fn(...args); |
32 | if (isThenable(result)) return result.then(null, errorFn); |
33 | return result; |
34 | } catch (err) { |
35 | console.error(err); |
36 | } |
37 | } |
38 | function defineProp(obj, prop, getter, setter) { |
39 | Object.defineProperty(obj, prop, { |
40 | configurable: true, |
41 | enumerable: true, |
42 | get: getter, |
43 | set: setter |
44 | }); |
45 | } |
46 | function readonly(obj, prop, value = obj[prop]) { |
47 | defineProp(obj, prop, () => value, emptyFn); |
48 | } |
49 | function writable(obj, prop, value = obj[prop]) { |
50 | Object.defineProperty(obj, prop, { |
51 | configurable: true, |
52 | enumerable: true, |
53 | writable: true, |
54 | value: value |
55 | }); |
56 | } |
57 | function parseHeaders(obj) { |
58 | const headers = {}; |
59 | switch (getType(obj)) { |
60 | case '[object String]': |
61 | for (const line of obj.trim().split(/[\r\n]+/)) { |
62 | const [header, value] = line.split(/\s*:\s*/); |
63 | if (!header) break; |
64 | const lheader = header.toLowerCase(); |
65 | headers[lheader] = lheader in headers ? `${headers[lheader]}, ${value}` : value; |
66 | } |
67 | break; |
68 | case '[object Headers]': |
69 | for (const [key, val] of obj) { |
70 | headers[key] = val; |
71 | } |
72 | break; |
73 | case '[object Object]': |
74 | return {...obj}; |
75 | } |
76 | return headers; |
77 | } |
78 | function stopImmediatePropagation() { |
79 | this.ajaxHooker_isStopped = true; |
80 | } |
81 | class SyncThenable { |
82 | then(fn) { |
83 | fn && fn(); |
84 | return new SyncThenable(); |
85 | } |
86 | } |
87 | class AHRequest { |
88 | constructor(request) { |
89 | this.request = request; |
90 | this.requestClone = {...this.request}; |
91 | } |
92 | shouldFilter(filters) { |
93 | const {type, url, method, async} = this.request; |
94 | return filters.length && !filters.find(obj => { |
95 | switch (true) { |
96 | case obj.type && obj.type !== type: |
97 | case getType(obj.url) === '[object String]' && !url.includes(obj.url): |
98 | case getType(obj.url) === '[object RegExp]' && !obj.url.test(url): |
99 | case obj.method && obj.method.toUpperCase() !== method.toUpperCase(): |
100 | case 'async' in obj && obj.async !== async: |
101 | return false; |
102 | } |
103 | return true; |
104 | }); |
105 | } |
106 | waitForRequestKeys() { |
107 | const requestKeys = ['url', 'method', 'abort', 'headers', 'data']; |
108 | if (!this.request.async) { |
109 | win.__ajaxHooker.hookInsts.forEach(({hookFns, filters}) => { |
110 | if (this.shouldFilter(filters)) return; |
111 | hookFns.forEach(fn => { |
112 | if (getType(fn) === '[object Function]') catchError(fn, this.request); |
113 | }); |
114 | requestKeys.forEach(key => { |
115 | if (isThenable(this.request[key])) this.request[key] = this.requestClone[key]; |
116 | }); |
117 | }); |
118 | return new SyncThenable(); |
119 | } |
120 | const promises = []; |
121 | win.__ajaxHooker.hookInsts.forEach(({hookFns, filters}) => { |
122 | if (this.shouldFilter(filters)) return; |
123 | promises.push(Promise.all(hookFns.map(fn => catchError(fn, this.request))).then(() => |
124 | Promise.all(requestKeys.map(key => Promise.resolve(this.request[key]).then( |
125 | val => this.request[key] = val, |
126 | () => this.request[key] = this.requestClone[key] |
127 | ))) |
128 | )); |
129 | }); |
130 | return Promise.all(promises); |
131 | } |
132 | waitForResponseKeys(response) { |
133 | const responseKeys = this.request.type === 'xhr' ? xhrResponses : fetchResponses; |
134 | if (!this.request.async) { |
135 | if (getType(this.request.response) === '[object Function]') { |
136 | catchError(this.request.response, response); |
137 | responseKeys.forEach(key => { |
138 | if ('get' in getDescriptor(response, key) || isThenable(response[key])) { |
139 | delete response[key]; |
140 | } |
141 | }); |
142 | } |
143 | return new SyncThenable(); |
144 | } |
145 | return Promise.resolve(catchError(this.request.response, response)).then(() => |
146 | Promise.all(responseKeys.map(key => { |
147 | const descriptor = getDescriptor(response, key); |
148 | if (descriptor && 'value' in descriptor) { |
149 | return Promise.resolve(descriptor.value).then( |
150 | val => response[key] = val, |
151 | () => delete response[key] |
152 | ); |
153 | } else { |
154 | delete response[key]; |
155 | } |
156 | })) |
157 | ); |
158 | } |
159 | } |
160 | const proxyHandler = { |
161 | get(target, prop) { |
162 | const descriptor = getDescriptor(target, prop); |
163 | if (descriptor && !descriptor.configurable && !descriptor.writable && !descriptor.get) return target[prop]; |
164 | const ah = target.__ajaxHooker; |
165 | if (ah && ah.proxyProps) { |
166 | if (prop in ah.proxyProps) { |
167 | const pDescriptor = ah.proxyProps[prop]; |
168 | if ('get' in pDescriptor) return pDescriptor.get(); |
169 | if (typeof pDescriptor.value === 'function') return pDescriptor.value.bind(ah); |
170 | return pDescriptor.value; |
171 | } |
172 | if (typeof target[prop] === 'function') return target[prop].bind(target); |
173 | } |
174 | return target[prop]; |
175 | }, |
176 | set(target, prop, value) { |
177 | const descriptor = getDescriptor(target, prop); |
178 | if (descriptor && !descriptor.configurable && !descriptor.writable && !descriptor.set) return true; |
179 | const ah = target.__ajaxHooker; |
180 | if (ah && ah.proxyProps && prop in ah.proxyProps) { |
181 | const pDescriptor = ah.proxyProps[prop]; |
182 | pDescriptor.set ? pDescriptor.set(value) : (pDescriptor.value = value); |
183 | } else { |
184 | target[prop] = value; |
185 | } |
186 | return true; |
187 | } |
188 | }; |
189 | class XhrHooker { |
190 | constructor(xhr) { |
191 | const ah = this; |
192 | Object.assign(ah, { |
193 | originalXhr: xhr, |
194 | proxyXhr: new Proxy(xhr, proxyHandler), |
195 | resThenable: new SyncThenable(), |
196 | proxyProps: {}, |
197 | proxyEvents: {} |
198 | }); |
199 | xhr.addEventListener('readystatechange', e => { |
200 | if (ah.proxyXhr.readyState === 4 && ah.request && typeof ah.request.response === 'function') { |
201 | const response = { |
202 | finalUrl: ah.proxyXhr.responseURL, |
203 | status: ah.proxyXhr.status, |
204 | responseHeaders: parseHeaders(ah.proxyXhr.getAllResponseHeaders()) |
205 | }; |
206 | const tempValues = {}; |
207 | for (const key of xhrResponses) { |
208 | try { |
209 | tempValues[key] = ah.originalXhr[key]; |
210 | } catch (err) {} |
211 | defineProp(response, key, () => { |
212 | return response[key] = tempValues[key]; |
213 | }, val => { |
214 | delete response[key]; |
215 | response[key] = val; |
216 | }); |
217 | } |
218 | ah.resThenable = new AHRequest(ah.request).waitForResponseKeys(response).then(() => { |
219 | for (const key of xhrResponses) { |
220 | ah.proxyProps[key] = {get: () => { |
221 | if (!(key in response)) response[key] = tempValues[key]; |
222 | return response[key]; |
223 | }}; |
224 | } |
225 | }); |
226 | } |
227 | ah.dispatchEvent(e); |
228 | }); |
229 | xhr.addEventListener('load', e => ah.dispatchEvent(e)); |
230 | xhr.addEventListener('loadend', e => ah.dispatchEvent(e)); |
231 | for (const evt of xhrAsyncEvents) { |
232 | const onEvt = 'on' + evt; |
233 | ah.proxyProps[onEvt] = { |
234 | get: () => ah.proxyEvents[onEvt] || null, |
235 | set: val => ah.addEvent(onEvt, val) |
236 | }; |
237 | } |
238 | for (const method of ['setRequestHeader', 'addEventListener', 'removeEventListener', 'open', 'send']) { |
239 | ah.proxyProps[method] = {value: ah[method]}; |
240 | } |
241 | } |
242 | toJSON() {} |
243 | addEvent(type, event) { |
244 | if (type.startsWith('on')) { |
245 | this.proxyEvents[type] = typeof event === 'function' ? event : null; |
246 | } else { |
247 | if (typeof event === 'object' && event !== null) event = event.handleEvent; |
248 | if (typeof event !== 'function') return; |
249 | this.proxyEvents[type] = this.proxyEvents[type] || new Set(); |
250 | this.proxyEvents[type].add(event); |
251 | } |
252 | } |
253 | removeEvent(type, event) { |
254 | if (type.startsWith('on')) { |
255 | this.proxyEvents[type] = null; |
256 | } else { |
257 | if (typeof event === 'object' && event !== null) event = event.handleEvent; |
258 | this.proxyEvents[type] && this.proxyEvents[type].delete(event); |
259 | } |
260 | } |
261 | dispatchEvent(e) { |
262 | e.stopImmediatePropagation = stopImmediatePropagation; |
263 | defineProp(e, 'target', () => this.proxyXhr); |
264 | defineProp(e, 'currentTarget', () => this.proxyXhr); |
265 | this.proxyEvents[e.type] && this.proxyEvents[e.type].forEach(fn => { |
266 | this.resThenable.then(() => !e.ajaxHooker_isStopped && fn.call(this.proxyXhr, e)); |
267 | }); |
268 | if (e.ajaxHooker_isStopped) return; |
269 | const onEvent = this.proxyEvents['on' + e.type]; |
270 | onEvent && this.resThenable.then(onEvent.bind(this.proxyXhr, e)); |
271 | } |
272 | setRequestHeader(header, value) { |
273 | this.originalXhr.setRequestHeader(header, value); |
274 | if (!this.request) return; |
275 | const headers = this.request.headers; |
276 | headers[header] = header in headers ? `${headers[header]}, ${value}` : value; |
277 | } |
278 | addEventListener(...args) { |
279 | if (xhrAsyncEvents.includes(args[0])) { |
280 | this.addEvent(args[0], args[1]); |
281 | } else { |
282 | this.originalXhr.addEventListener(...args); |
283 | } |
284 | } |
285 | removeEventListener(...args) { |
286 | if (xhrAsyncEvents.includes(args[0])) { |
287 | this.removeEvent(args[0], args[1]); |
288 | } else { |
289 | this.originalXhr.removeEventListener(...args); |
290 | } |
291 | } |
292 | open(method, url, async = true, ...args) { |
293 | this.request = { |
294 | type: 'xhr', |
295 | url: url.toString(), |
296 | method: method.toUpperCase(), |
297 | abort: false, |
298 | headers: {}, |
299 | data: null, |
300 | response: null, |
301 | async: !!async |
302 | }; |
303 | this.openArgs = args; |
304 | this.resThenable = new SyncThenable(); |
305 | ['responseURL', 'readyState', 'status', 'statusText', ...xhrResponses].forEach(key => { |
306 | delete this.proxyProps[key]; |
307 | }); |
308 | return this.originalXhr.open(method, url, async, ...args); |
309 | } |
310 | send(data) { |
311 | const ah = this; |
312 | const xhr = ah.originalXhr; |
313 | const request = ah.request; |
314 | if (!request) return xhr.send(data); |
315 | request.data = data; |
316 | new AHRequest(request).waitForRequestKeys().then(() => { |
317 | if (request.abort) { |
318 | if (typeof request.response === 'function') { |
319 | Object.assign(ah.proxyProps, { |
320 | responseURL: {value: request.url}, |
321 | readyState: {value: 4}, |
322 | status: {value: 200}, |
323 | statusText: {value: 'OK'} |
324 | }); |
325 | xhrAsyncEvents.forEach(evt => xhr.dispatchEvent(new Event(evt))); |
326 | } |
327 | } else { |
328 | xhr.open(request.method, request.url, request.async, ...ah.openArgs); |
329 | for (const header in request.headers) { |
330 | xhr.setRequestHeader(header, request.headers[header]); |
331 | } |
332 | xhr.send(request.data); |
333 | } |
334 | }); |
335 | } |
336 | } |
337 | function fakeXHR() { |
338 | const xhr = new winAh.realXHR(); |
339 | if ('__ajaxHooker' in xhr) console.warn('检测到不同版本的ajaxHooker,可能发生冲突!'); |
340 | xhr.__ajaxHooker = new XhrHooker(xhr); |
341 | return xhr.__ajaxHooker.proxyXhr; |
342 | } |
343 | fakeXHR.prototype = win.XMLHttpRequest.prototype; |
344 | Object.keys(win.XMLHttpRequest).forEach(key => fakeXHR[key] = win.XMLHttpRequest[key]); |
345 | function fakeFetch(url, options = {}) { |
346 | if (!url) return winAh.realFetch.call(win, url, options); |
347 | return new Promise(async (resolve, reject) => { |
348 | const init = {}; |
349 | if (getType(url) === '[object Request]') { |
350 | for (const prop of fetchInitProps) init[prop] = url[prop]; |
351 | if (url.body) init.body = await url.arrayBuffer(); |
352 | url = url.url; |
353 | } |
354 | url = url.toString(); |
355 | Object.assign(init, options); |
356 | init.method = init.method || 'GET'; |
357 | init.headers = init.headers || {}; |
358 | const request = { |
359 | type: 'fetch', |
360 | url: url, |
361 | method: init.method.toUpperCase(), |
362 | abort: false, |
363 | headers: parseHeaders(init.headers), |
364 | data: init.body, |
365 | response: null, |
366 | async: true |
367 | }; |
368 | const req = new AHRequest(request); |
369 | await req.waitForRequestKeys(); |
370 | if (request.abort) { |
371 | if (typeof request.response === 'function') { |
372 | const response = { |
373 | finalUrl: request.url, |
374 | status: 200, |
375 | responseHeaders: {} |
376 | }; |
377 | await req.waitForResponseKeys(response); |
378 | const key = fetchResponses.find(k => k in response); |
379 | let val = response[key]; |
380 | if (key === 'json' && typeof val === 'object') { |
381 | val = catchError(JSON.stringify.bind(JSON), val); |
382 | } |
383 | const res = new Response(val, { |
384 | status: 200, |
385 | statusText: 'OK' |
386 | }); |
387 | defineProp(res, 'type', () => 'basic'); |
388 | defineProp(res, 'url', () => request.url); |
389 | resolve(res); |
390 | } else { |
391 | reject(new DOMException('aborted', 'AbortError')); |
392 | } |
393 | return; |
394 | } |
395 | init.method = request.method; |
396 | init.headers = request.headers; |
397 | init.body = request.data; |
398 | winAh.realFetch.call(win, request.url, init).then(res => { |
399 | if (typeof request.response === 'function') { |
400 | const response = { |
401 | finalUrl: res.url, |
402 | status: res.status, |
403 | responseHeaders: parseHeaders(res.headers) |
404 | }; |
405 | fetchResponses.forEach(key => res[key] = function() { |
406 | if (key in response) return Promise.resolve(response[key]); |
407 | return resProto[key].call(this).then(val => { |
408 | response[key] = val; |
409 | return req.waitForResponseKeys(response).then(() => key in response ? response[key] : val); |
410 | }); |
411 | }); |
412 | } |
413 | resolve(res); |
414 | }, reject); |
415 | }); |
416 | } |
417 | function fakeFetchClone() { |
418 | const descriptors = Object.getOwnPropertyDescriptors(this); |
419 | const res = winAh.realFetchClone.call(this); |
420 | Object.defineProperties(res, descriptors); |
421 | return res; |
422 | } |
423 | winAh = win.__ajaxHooker = winAh || { |
424 | version, fakeXHR, fakeFetch, fakeFetchClone, |
425 | realXHR: win.XMLHttpRequest, |
426 | realFetch: win.fetch, |
427 | realFetchClone: resProto.clone, |
428 | hookInsts: new Set() |
429 | }; |
430 | if (winAh.version !== version) console.warn('A different version of AjaxHooker was detected, which may cause conflicts!'); |
431 | win.XMLHttpRequest = winAh.fakeXHR; |
432 | win.fetch = winAh.fakeFetch; |
433 | resProto.clone = winAh.fakeFetchClone; |
434 | winAh.hookInsts.add(hookInst); |
435 | return { |
436 | hook: fn => hookInst.hookFns.push(fn), |
437 | filter: arr => { |
438 | if (Array.isArray(arr)) hookInst.filters = arr; |
439 | }, |
440 | protect: () => { |
441 | readonly(win, 'XMLHttpRequest', winAh.fakeXHR); |
442 | readonly(win, 'fetch', winAh.fakeFetch); |
443 | readonly(resProto, 'clone', winAh.fakeFetchClone); |
444 | }, |
445 | unhook: () => { |
446 | winAh.hookInsts.delete(hookInst); |
447 | if (!winAh.hookInsts.size) { |
448 | writable(win, 'XMLHttpRequest', winAh.realXHR); |
449 | writable(win, 'fetch', winAh.realFetch); |
450 | writable(resProto, 'clone', winAh.realFetchClone); |
451 | delete win.__ajaxHooker; |
452 | } |
453 | } |
454 | }; |
455 | }(); |