// ==UserScript== // @name ajax // @author cxxjackie // @version 1.0.0 // ==/UserScript== var ajax = function() { let defaultOptions = {}; const requests = new WeakMap(); function isString(str) { return typeof str === 'string' || str instanceof String; } function isPlainObject(obj) { if (typeof obj !== 'object' || obj === null) return false; let proto = obj; while (Object.getPrototypeOf(proto) !== null) { proto = Object.getPrototypeOf(proto); } return Object.getPrototypeOf(obj) === proto; } function isSimpleObject(obj) { if (!isPlainObject(obj)) return false; for (const key in obj) { if (typeof obj[key] === 'object') { return false; } } return true; } function parseUrlString(obj) { if (typeof obj !== 'object' || obj === null) return ''; let str = ''; for (const key in obj) { str += `&${key}=${encodeURIComponent(obj[key])}`; } return str.substr(1); } function lowerHeaders(headers) { if (typeof headers !== 'object' || headers === null) return {}; const lheaders = {}; for (const key in headers) { lheaders[key.toLowerCase()] = headers[key]; } return lheaders; } function parseHeaders(str) { if (!str) return {}; const headers = {}; const arr = str.trim().split(/[\r\n]+/); for (const line of arr) { const parts = line.split(/:\s*/); headers[parts[0].toLowerCase()] = parts[1]; } return headers; } function parseJson(str) { let result = str; try { while (typeof result !== 'object') { result = JSON.parse(result); } } catch (e) { result = null; } return result; } function parseDom(str, contentType) { let mimeType = contentType ? contentType.split(';').shift() : 'text/html'; switch (mimeType) { case 'text/html': case 'text/xml': case 'application/xml': case 'application/xhtml+xml': case 'image/svg+xml': break; default: mimeType = 'text/html'; } return new DOMParser().parseFromString(str, mimeType); } function adaptData(options) { if (!('data' in options)) return; if (options.method === 'get') { let search; if (isString(options.data)) { search = options.data; } else if (isSimpleObject(options.data)) { search = parseUrlString(options.data); } if (search) options.url += (options.url.includes('?') ? '&' : '?') + search; } if (options.method === 'post') { if (!('content-type' in options.headers)) { switch (true) { case isString(options.data): options.headers['content-type'] = 'application/x-www-form-urlencoded'; break; case isSimpleObject(options.data): options.headers['content-type'] = 'application/x-www-form-urlencoded'; options.data = parseUrlString(options.data); break; case isPlainObject(options.data): options.headers['content-type'] = 'application/json'; options.data = JSON.stringify(options.data); break; } } else if (!isString(options.data)) { const mimeType = options.headers['content-type'].split(';').shift(); switch (mimeType) { case 'application/x-www-form-urlencoded': options.data = parseUrlString(options.data); break; case 'application/json': options.data = JSON.stringify(options.data); break; } } } } function ajaxGM(GM_xhr, options) { const {_status, _detail, _nocatch, ..._options} = options; let abort = () => {}; const promise = new Promise((resolve, reject) => { const _resolve = data => { requests.delete(promise); resolve(_detail ? data : data.response); }; const _reject = (reason, status = 0) => { requests.delete(promise); const err = { url: _options.url, status: status, error: reason }; _nocatch ? resolve(_detail ? err : null) : reject(err); }; const responseType = _options.responseType; if (['text', 'json', 'document'].includes(responseType)) delete _options.responseType; const xhr = GM_xhr({ ..._options, onload: res => { if (_status.includes(res.status)) { let result; const headers = parseHeaders(res.responseHeaders); switch (responseType) { case 'text': result = res.responseText; break; case 'json': result = parseJson(res.responseText); if (!result) return _reject('parse json failed', res.status); break; case 'document': const contentType = _options.overrideMimeType || headers['content-type']; result = parseDom(res.responseText, contentType); break; default: result = res.response; } _resolve({ finalUrl: res.finalUrl, status: res.status, response: result, responseHeaders: headers }); } else { _reject('bad status', res.status); } }, onerror: () => _reject('request failed'), onabort: () => _reject('aborted'), // Don't work in Tampermonkey < 4.9.5927 ontimeout: () => _reject('timeout') }); if (xhr && typeof xhr.abort === 'function') { abort = () => { xhr.abort(); _reject('aborted'); }; } }); requests.set(promise, {abort}); return promise; } function ajaxXHR(options) { const {_status, _detail, _nocatch, ..._options} = options; const xhr = new XMLHttpRequest(); const abort = xhr.abort.bind(xhr); const promise = new Promise((resolve, reject) => { const _resolve = data => { requests.delete(promise); resolve(_detail ? data : data.response); }; const _reject = reason => { requests.delete(promise); const err = { url: _options.url, status: xhr.status, error: reason }; _nocatch ? resolve(_detail ? err : null) : reject(err); }; xhr.open(_options.method, _options.url, true, _options.user, _options.password); for (const header in _options.headers) { xhr.setRequestHeader(header, _options.headers[header]); } if ('overrideMimeType' in _options) { xhr.overrideMimeType(_options.overrideMimeType); delete _options.overrideMimeType; } const responseType = _options.responseType; if (responseType === 'json') _options.responseType = 'text'; for (const key in _options) { switch (typeof xhr[key]) { case 'object': if (key.startsWith('on')) { xhr[key] = e => _options[key].call(xhr, e.target); } else { xhr[key] = _options[key]; } break; case 'function': case 'undefined': break; default: xhr[key] = _options[key]; } } xhr.onload = () => { if (_status.includes(xhr.status)) { let result; if (responseType === 'json') { result = parseJson(xhr.responseText); if (!result) _reject('parse json failed'); } else { result = xhr.response; } _resolve({ finalUrl: xhr.responseURL, status: xhr.status, response: result, responseHeaders: parseHeaders(xhr.getAllResponseHeaders()) }); } else { _reject('bad status'); } }; xhr.onerror = () => _reject('request failed'); xhr.onabort = () => _reject('aborted'); xhr.ontimeout = () => _reject('timeout'); xhr.send(_options.data); }); requests.set(promise, {abort}); return promise; } function ajax(desc, obj = {}) { switch (desc) { case 'default': defaultOptions = obj; defaultOptions.headers = lowerHeaders(defaultOptions.headers); return; case 'abort': if (requests.has(obj)) { requests.get(obj).abort(); } return; } const {onload, onerror, onabort, ontimeout, _mode, ...options} = { method: 'get', responseType: 'text', _status: [200], _detail: false, _nocatch: false, ...defaultOptions, ...obj, url: desc }; options.method = options.method.toLowerCase(); options.headers = lowerHeaders(options.headers); if ('headers' in defaultOptions) { options.headers = {...defaultOptions.headers, ...options.headers}; } if ('cookie' in options) { options.headers.cookie = options.cookie; delete options.cookie; } adaptData(options); let GM_xhr; switch (_mode) { case 'GM_xhr': case 'GM_xmlhttpRequest': if (!window.GM_xmlhttpRequest) { return Promise.reject('缺少GM_xmlhttpRequest,请检查你的脚本声明。'); } GM_xhr = window.GM_xmlhttpRequest; break; case 'GM.xhr': case 'GM.xmlHttpRequest': if (!window.GM || !window.GM.xmlHttpRequest) { return Promise.reject('缺少GM.xmlHttpRequest,请检查你的脚本声明。'); } GM_xhr = window.GM.xmlHttpRequest; break; case 'xhr': case 'XMLHttpRequest': break; default: GM_xhr = window.GM_xmlhttpRequest || (window.GM && window.GM.xmlHttpRequest); } return GM_xhr ? ajaxGM(GM_xhr.bind(window), options) : ajaxXHR(options); } return ajax; }();