您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Initiate an XHR request on the page
当前为
- // ==UserScript==
- // @name http-on-pages
- // @namespace https://github.com/pansong291/
- // @version 0.1.7
- // @description Initiate an XHR request on the page
- // @description:zh 在页面上发起 XHR 请求
- // @author paso
- // @license Apache-2.0
- // @match *://*/*
- // @grant none
- // @noframes
- // @run-at context-menu
- // @require https://update.greasyfork.org/scripts/473443/1374764/popup-inject.js
- // ==/UserScript==
- /**
- * @typedef {object} Req
- * @property {string} method
- * @property {string} url
- * @property {string} code
- * @property {number} timestamp
- */
- /**
- * @typedef {object} ReqsProxy
- * @property {(i: number, v: Req) => void} insert
- * @property {(i: number) => Req} remove
- * @property {Req} selected
- */
- ;(function () {
- 'use strict'
- const namespace = 'paso-http-on-pages'
- const hint = 'const data = { headers: {}, params: {}, body: void 0, withCredentials: true }'
- window.paso.injectPopup({
- namespace,
- actionName: 'Http Request',
- collapse: '70%',
- content: `
- <div class="tip-box info">${hint}</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>`,
- style: `
- <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 {
- flex: 1 0 300px;
- }
- #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>`
- }).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 {Req[] & ReqsProxy}
- */
- 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 {Req}
- */
- 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 xhr = new XMLHttpRequest()
- xhr.open(selReq.method, selReq.url + serializeQueryParam(data.params))
- xhr.withCredentials = !!data.withCredentials
- Object.entries(data.headers).forEach(([n, v]) => xhr.setRequestHeader(n, v))
- xhr.send(isGet ? void 0 : typeof data.body === 'string' ? data.body : JSON.stringify(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(param).map(([k, v]) => k + '=' + encodeURIComponent(String(v))).join('&')
- if (str) return prefix + str
- return str
- }
- /**
- * @param {?Req} [base]
- * @returns {Req}
- */
- 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) {
- let str = String(num)
- while (str.length < count) {
- str = '0' + str
- }
- return str
- }
- /**
- * @param {*} obj
- */
- function saveCache(obj) {
- localStorage.setItem(namespace, JSON.stringify(obj))
- }
- /**
- * @returns {{requests: Req[], selected: string} | undefined}
- */
- function getCache() {
- const str = localStorage.getItem(namespace)
- try {
- if (str) return JSON.parse(str)
- } catch (e) {
- console.error(e)
- }
- }
- })()