您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Initiate an XHR request on the page
// ==UserScript== // @name http-on-pages // @namespace https://github.com/pansong291/ // @version 0.1.10 // @description Initiate an XHR request on the page // @description:zh 在页面上发起 XHR 请求 // @author paso // @license Apache-2.0 // @match *://*/* // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201024%201024%22%20fill%3D%22black%22%3E%3Cpath%20d%3D%22M474.937%20387.054c0-20.275%2016.638-36.403%2036.913-36.403h53.035L362.632%20148.402%20160.377%20350.65h53.297c20.275%200%2036.652%2016.128%2036.652%2036.403v259.959c0%2020.276-16.377%2036.914-36.652%2036.914-20.021%200-36.919-16.638-36.919-36.914V423.967H71.732c-20.015%200-36.137-17.159-36.137-36.914%200-9.883%203.644-19.244%2010.138-25.999l291.42-291.16c13.781-13.521%2037.435-13.521%2051.217%200l291.421%20291.161c13.776%2014.552%2013.776%2037.956%200%2051.987-7.282%206.766-16.898%2010.925-25.999%2010.925H548.247v223.045c0%2020.276-16.377%2036.914-36.398%2036.914h-53.291l202.509%20202.26%20201.994-202.26H809.77c-20.276%200-36.398-16.638-36.398-36.914V387.054c0-20.275%2016.122-36.403%2036.398-36.403s36.914%2016.128%2036.914%2036.403v223.567h104.768c9.617%200%2018.717%203.116%2026.254%2010.393a37.359%2037.359%200%200%201%200%2051.999L687.328%20962.609l-0.261%201.043c-14.558%2014.563-37.957%2014.563-51.993%200l-291.16-290.639c-6.239-6.755-10.659-16.117-10.659-26%200-20.275%2016.898-36.392%2036.397-36.392h105.285V387.054z%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E // @grant none // @noframes // @run-at context-menu // @require https://update.greasyfork.org/scripts/473443/1374764/popup-inject.js // ==/UserScript== /** * @typedef {object} ReqObj * @property {string} method * @property {string} url * @property {string} code * @property {number} timestamp */ /** * @typedef {object} ProxiedReqExtension * @property {(i: number, v: ReqObj) => void} insert * @property {(i: number) => ReqObj} remove * @property {ReqObj} selected */ /** * @typedef {ReqObj[] & ProxiedReqExtension} ProxiedReqArray */ ;(function () { 'use strict' const namespace = 'paso-http-on-pages' const injectHint = 'const data = { headers: {}, params: {}, body: void 0, withCredentials: true }' const injectHtml = ` <div class="tip-box info">${injectHint}</div> <div class="flex gap-4"> <select id="ipt-req-sel" class="input"></select> <button id="btn-req-rem" type="button" class="button square"> <svg width="16" height="16" fill="currentcolor"> <path d="M2 7h12v2H2Z"></path> </svg> </button> <button id="btn-req-add" type="button" class="button square"> <svg width="16" height="16" fill="currentcolor"> <path d="M2 7H7V2H9V7H14V9H9V14H7V9H2Z"></path> </svg> </button> </div> <div class="flex gap-4"> <select id="ipt-method" class="input"></select> <input type="text" id="ipt-url" class="input" autocomplete="off"> <button type="button" id="btn-submit" class="button">Submit</button> </div> <textarea id="ipt-code" class="input" spellcheck="false"></textarea> <div id="error-tip-box"></div>` const injectStyle = ` <style> .popup { gap: 4px; } .gap-4 { gap: 4px; } .tip-box.info { background: #d3dff7; border-left: 6px solid #3d7fff; border-radius: 4px; padding: 16px; } .button.square { width: 32px; padding: 0; } #ipt-method { width: 90px; } #ipt-url { width: 300px; flex-grow: 1; } #btn-submit { width: 100px; } #ipt-code { height: 400px; } #error-tip-box { background: #fdd; border-left: 6px solid #f66; border-radius: 4px; padding: 16px; } #error-tip-box:empty { display: none; } </style>` window.paso.injectPopup({ namespace, actionName: 'Http Request', collapse: '70%', content: injectHtml, style: injectStyle }).then((result) => { const { popup } = result.elem const { createElement } = result.func popup.classList.add('monospace') const element = { ipt_req_sel: popup.querySelector('#ipt-req-sel'), btn_req_rem: popup.querySelector('#btn-req-rem'), btn_req_add: popup.querySelector('#btn-req-add'), ipt_method: popup.querySelector('#ipt-method'), ipt_url: popup.querySelector('#ipt-url'), ipt_code: popup.querySelector('#ipt-code'), btn_submit: popup.querySelector('#btn-submit'), error_tip: popup.querySelector('#error-tip-box') } const method_options = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'] element.ipt_method.innerHTML = method_options.map(op => `<option value="${op}">${op}</option>`).join('') /** * @type {ProxiedReqArray} */ const reactiveRequests = new Proxy([], { get(target, prop, receiver) { if (prop === 'insert') { return (index, value) => { checkIndex(index, target.length + 1) const opt = createElement('option', { value: value.timestamp }, [formatDate(value.timestamp)]) if (target.length === 0 || index === target.length) element.ipt_req_sel.append(opt) else element.ipt_req_sel.children[index].before(opt) target.splice(index, 0, value) } } else if (prop === 'remove') { return (index) => { checkIndex(index, target.length) if (receiver.selected === target[index]) receiver.selected = target[index > 0 ? index - 1 : 1] element.ipt_req_sel.children[index].remove() return target.splice(index, 1)[0] } } else if (prop === 'push') { return (value) => receiver.insert(target.length, value) } else if (prop === 'selected') { if (!target.selected) { const v = String(element.ipt_req_sel.value) target.selected = target.find((r) => String(r.timestamp) === v) } } return target[prop] }, set(target, prop, newValue, receiver) { if (prop === 'selected') { target.selected = newValue element.ipt_req_sel.value = newValue?.timestamp || '' element.ipt_method.value = newValue?.method || '' element.ipt_url.value = newValue?.url || '' element.ipt_code.value = newValue?.code || '' } return true } }) /** * @param {string|number} ts * @returns {ReqObj} */ const getReqByTimestamp = (ts) => { ts = String(ts) return reactiveRequests.find((r) => String(r.timestamp) === ts) } const cache = getCache() if (cache?.requests && Array.isArray(cache.requests)) { for (const req of cache.requests) { reactiveRequests.push(createRequestObj(req)) } } if (!reactiveRequests.length) reactiveRequests.push(createRequestObj()) reactiveRequests.selected = getReqByTimestamp(cache?.selected) || reactiveRequests[0] element.ipt_req_sel.addEventListener('change', (e) => reactiveRequests.selected = getReqByTimestamp(e.currentTarget.value)) element.btn_req_rem.addEventListener('click', () => { if (reactiveRequests.length <= 1) return reactiveRequests.remove(reactiveRequests.indexOf(reactiveRequests.selected)) }) element.btn_req_add.addEventListener('click', () => { const obj = createRequestObj() reactiveRequests.push(obj) reactiveRequests.selected = obj }) element.ipt_method.addEventListener('change', (e) => reactiveRequests.selected.method = e.currentTarget.value) element.ipt_url.addEventListener('change', (e) => reactiveRequests.selected.url = e.currentTarget.value) element.ipt_code.addEventListener('change', (e) => reactiveRequests.selected.code = e.currentTarget.value) element.btn_submit.addEventListener('click', tryTo(() => { const selReq = reactiveRequests.selected if (!selReq.url) throw 'Url is required' const isGet = selReq.method === 'GET' // 预备数据 const data = { headers: { 'Content-Type': isGet ? 'application/x-www-form-urlencoded' : 'application/json' }, params: {}, body: void 0, withCredentials: true } // 处理数据 const handleData = new Function('data', selReq.code) handleData.call(data, data) const isForm = data.body instanceof FormData if (isForm) { // 使用表单时不填充 Content-Type delete data.headers['Content-Type'] } const xhr = new XMLHttpRequest() // 链接 xhr.open(selReq.method, selReq.url + serializeQueryParam(data.params)) // 使用凭证 xhr.withCredentials = !!data.withCredentials // 请求头 Object.entries(data.headers).forEach(([n, v]) => { if (v !== null && v !== undefined) xhr.setRequestHeader(n, v) }) // 请求体 xhr.send(isGet ? void 0 : isForm ? data.body : serializeHttpBody(data.body)) saveCache({ requests: reactiveRequests, selected: selReq.timestamp }) element.error_tip.innerText = '' }, e => element.error_tip.innerText = String(e))) }) /** * @param {function} fn * @param {function} [errorCallback] * @returns {function} */ function tryTo(fn, errorCallback) { return function (...args) { try { fn.apply(this, args) } catch (e) { console.error(e) errorCallback?.(e) } } } /** * @param {string | Record<string, string>} [param] * @param {string} [prefix='?'] * @returns {string} */ function serializeQueryParam(param, prefix = '?') { if (!param) return '' if (typeof param === 'string') return prefix + param const str = Object.entries(flatten(param)).flatMap(([k, v]) => { if (v === null || v === void 0) return [] return [k + '=' + encodeURIComponent(String(v))] }).join('&') if (str) return prefix + str return str } /** * @param {*} obj * @param {string} [name=''] * @returns {Record<string, *>} */ function flatten(obj, name = '') { const result = {} if (!obj || typeof obj !== 'object') { if (!name) return [obj] result[name] = obj } else { const isArr = Array.isArray(obj) Object.entries(obj).forEach(([k, v]) => { Object.entries(flatten(v, !name ? k : isArr ? `${name}[${k}]` : `${name}.${k}`)).forEach(([k2, v2]) => { result[k2] = v2 }) }) } return result } /** * @param {?ReqObj} [base] * @returns {ReqObj} */ function createRequestObj(base) { return { method: base?.method || 'GET', url: base?.url || '', code: base?.code || '', timestamp: base?.timestamp || Date.now() } } /** * @param {number} index * @param {number} length */ function checkIndex(index, length) { if (index < 0 || index >= length) throw new RangeError(`Index out of bounds error.\nindex: ${index}\nlength: ${length}`) } /** * @param {*} [date] * @returns {string} */ function formatDate(date) { date = new Date(date || null) const year = formatNumber(date.getFullYear(), 4) const month = formatNumber(date.getMonth()) const day = formatNumber(date.getDate()) const hour = formatNumber(date.getHours()) const minute = formatNumber(date.getMinutes()) const second = formatNumber(date.getSeconds()) const mill = formatNumber(date.getMilliseconds(), 3) return `${year}-${month}-${day} ${hour}:${minute}:${second}.${mill}` } /** * @param {number} num * @param {number} [count=2] * @returns {string} */ function formatNumber(num, count = 2) { return String(num).padStart(count, '0') } /** * @param {*} obj * @returns {string} */ function serializeHttpBody(obj) { if (typeof obj === 'string') return obj return JSON.stringify(obj) } /** * @param {*} obj */ function saveCache(obj) { localStorage.setItem(namespace, JSON.stringify(obj)) } /** * @returns {{requests: ReqObj[], selected: string} | undefined} */ function getCache() { const str = localStorage.getItem(namespace) try { if (str) return JSON.parse(str) } catch (e) { console.error(e) } } })()