静态网页配合油猴脚本实现跨域请求

如果想做一个纯静态网页,而又想跨域请求,可以通过油猴脚本来实现

油猴脚本

// ==UserScript==
// @name         注入 GM_xmlhttpRequest 和 GM.xmlHttpRequest
// @namespace    https://docs.scriptcat.org/
// @version      0.1.0
// @description  try to take over the world!
// @author       You
// @match        http://localhost:5173/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=localhost
// @connect      *
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';
    // 向 window 注入 GM_xmlhttpRequest
    // 将 GM_xmlhttpRequest 函数赋值给 unsafeWindow 的一个新属性
    // 这样页面的 <script> 标签就能通过 window.GM_xmlhttpRequest 访问到它
    if (typeof unsafeWindow.GM_xmlhttpRequest === 'undefined') {
        unsafeWindow.GM_xmlhttpRequest = GM_xmlhttpRequest;
    }

    // 向 window 注入 GM.xmlHttpRequest
    // 1. 检查页面上是否存在 GM 对象,如果不存在,则创建一个
    if (typeof unsafeWindow.GM === 'undefined') {
        // 2. 将油猴沙箱中的 GM.xmlHttpRequest 函数赋值给页面 window 上的 GM.xmlHttpRequest
        //    由于 GM.xmlHttpRequest 本身就返回 Promise,所以页面可以直接 await 它
        unsafeWindow.GM = { xmlHttpRequest: GM.xmlHttpRequest };
    }
})();

注意将 // @match http://localhost:5173/* 替换成你的静态网页地址

这样,在你的静态网页中,你可以通过 GM_xmlhttpRequest 或者 GM.xmlHttpRequest 来发起跨域请求。例如:

GM_xmlhttpRequest({
  method: "GET",
  url: "https://example.com/",
  headers: {
    "Content-Type": "application/json"
  },
  onload: function(response) {
    console.log(response.responseText);
  }
});

const r = await GM.xmlHttpRequest({ url: "https://example.com/" }).catch(e => console.error(e));
console.log(r.responseText);

try {
  // 如果在GM.xmlHttpRequest 中指定 responseType: 'json',
  // 那么会自动将响应数据解析为 JSON 对象
  const gmResponse = await GM.xmlHttpRequest({
    url: 'https://jsonplaceholder.typicode.com/todos/1',
    responseType: 'json'
  })

  // object
  console.log(typeof gmResponse.response)
} catch (error) {
  console.error('请求失败:', error)
}

try {
  const gmResponse = await GM.xmlHttpRequest({
    url: 'https://jsonplaceholder.typicode.com/todos/1'
  })

  // string
  console.log(typeof gmResponse.response)
} catch (error) {
  console.error('请求失败:', error)
}

添加类型声明文件

如果你在使用 TypeScript,你需要为 GM_xmlhttpRequestGM.xmlHttpRequest 添加类型声明。

在项目的 src 目录下创建一个文件,例如 GM_xmlhttpRequest.d.ts 或者 globals.d.ts 或者 tampermonkey.d.ts,并添加以下内容:

// To ensure this file is treated as a module.
export {};

/**
 * Represents the response object received from a GM_xmlhttpRequest.
 * @template TContext The type of the context object passed in the request.
 * @template TResponse The expected type of the `response` property if `responseType` is 'json'.
 */
interface GM_Response<TResponse = any, TContext = any> {
  finalUrl: string;
  readyState: 0 | 1 | 2 | 3 | 4;
  status: number;
  statusText: string;
  responseHeaders: string;
  /** The response data. If `responseType` was 'json', this will be of type `TResponse`. */
  response: TResponse;
  responseXML: Document | null;
  responseText: string;
  context?: TContext;
}

/**
 * The base details for an XML HTTP request.
 * @template TContext The type of the context object to be passed.
 */
interface GM_Request<TContext = any> {
  method?: 'GET' | 'POST' | 'HEAD' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';
  url: string | URL | File | Blob;
  headers?: Record<string, string>;
  data?: string | Blob | File | object | any[] | FormData | URLSearchParams;
  redirect?: 'follow' | 'error' | 'manual';
  cookie?: string;
  cookiePartition?: object;
  topLevelSite?: string;
  binary?: boolean;
  nocache?: boolean;
  revalidate?: boolean;
  timeout?: number;
  context?: TContext;
  /** 
   * The expected response type. Set to 'json' to have Tampermonkey automatically parse the response.
   */
  responseType?: 'arraybuffer' | 'blob' | 'json' | 'stream';
  overrideMimeType?: string;
  anonymous?: boolean;
  fetch?: boolean;
  user?: string;
  password?: string;
}

/**
 * Details for a callback-based GM_xmlhttpRequest.
 * @template TResponse The expected type of the `response` property if `responseType` is 'json'.
 * @template TContext The type of the context object to be passed.
 */
interface GM_CallbackRequest<TResponse = any, TContext = any> extends GM_Request<TContext> {
  onabort?: (response: GM_Response<TResponse, TContext>) => void;
  onerror?: (response: GM_Response<TResponse, TContext>) => void;
  onloadstart?: (response: GM_Response<TResponse, TContext>) => void;
  onprogress?: (response: GM_Response<TResponse, TContext>) => void;
  onreadystatechange?: (response: GM_Response<TResponse, TContext>) => void;
  ontimeout?: (response: GM_Response<TResponse, TContext>) => void;
  onload: (response: GM_Response<TResponse, TContext>) => void;
}

interface GM_AbortHandle {
  abort: () => void;
}

declare global {
  function GM_xmlhttpRequest<TResponse = any, TContext = any>(
    details: GM_CallbackRequest<TResponse, TContext>
  ): GM_AbortHandle;

  const GM: {
    xmlHttpRequest<TResponse = any, TContext = any>(
      details: GM_Request<TContext>
    ): Promise<GM_Response<TResponse, TContext>> & GM_AbortHandle;
  };
}

这样项目中使用GM_xmlhttpRequestGM.xmlHttpRequest 就会得到类型提示。

这样会导致 .d.ts 文件中 ESLint 报错: ESLint: Unexpected any. Specify a different type. (@typescript-eslint/no-explicit-any)

可以把 any 改成 never ,这样 .d.ts 文件中 ESLint 不报错,但这样在调用处会报错 Property [property name] does not exist on type never

还可以把 any 改成 unknown ,这样在调用处会要求显式指定类型。