油猴脚本也能用跨域 SSE 么?

引子

前些日子在写 AI 智能翻译脚本时,偶然发现 Duckduckgo 的 AI 可以免费无限次调用且不违反 EULA,然而请求地址的 CORS 头不允许我直接访问 API。

理论上来说在油猴脚本中设置 // @grant unsafeWindow 后就可以使用无视 CORS 头的 XMLHttpRequest,然而实操了一下发现即使隔离运行,XMLHttpRequest 仍然会受到限制。

阅读文档发现想要无视 CORS 头,需要使用油猴特有的 GM_xmlHttpRequest,于是我就遇到了两个问题:

  • GM_xmlHttpRequest 怎么流式传输?
  • SSE.js 不会使用 GM_xmlHttpRequest,如何在其间牵线搭桥?

经过一些探究,便有了这篇文章(其实更类似于代码块的分享)。

观察

阅读文档发现,GM_xmlHttpRequest 存在和一般 XMLHttpRequest 相同的事件监听器,不难写出以下 XMLHttpRequest polyfill:

js 复制代码
const _XMLHttpRequest = window.XMLHttpRequest
// Tampermonkey XMLHttpRequest polyfill
class XMLHttpRequest extends EventTarget {
  static get UNSENT() {
    return _XMLHttpRequest.UNSENT
  }
  static get OPENED() {
    return _XMLHttpRequest.OPENED
  }
  static get HEADERS_RECEIVED() {
    return _XMLHttpRequest.HEADERS_RECEIVED
  }
  static get LOADING() {
    return _XMLHttpRequest.LOADING
  }
  static get DONE() {
    return _XMLHttpRequest.DONE
  }
  abort() {
    if (this._abortToken) this._abortToken()
  }
  get status() {
    return this._status
  }
  get statusText() {
    return this._statusText
  }
  get responseText() {
    return this._text
  }
  get responseXML() {
    throw new Error('Not implemented')
  }
  get responseURL() {
    return this._finalUrl
  }
  get readyState() {
    return this._readyState
  }
  open(method, url) {
    this._method = method
    this._requestUrl = url
    this._readyState = XMLHttpRequest.OPENED
  }
  getAllResponseHeaders() {
    return this._responseHeaders
  }
  setRequestHeader(header, value) {
    this._requestHeaders[header] = value
  }
  send(payload) {
    this._abortToken = GM_xmlhttpRequest({
      method: this._method,
      url: this._requestUrl,
      headers: this._requestHeaders,
      ...(this.timeout !== 0 ? { timeout: this.timeout } : {}),
      data: payload,
      responseType: 'text',
      onabort: e => {
        this._readyState = e.readyState
        this.dispatchEvent(new ProgressEvent('abort'))
      },
      onerror: e => {
        this._readyState = e.readyState
        this.dispatchEvent(new ProgressEvent('error'))
      },
      onloadstart: e => {
        this._readyState = e.readyState
        this.dispatchEvent(new ProgressEvent('loadstart'))
      },
      onreadystatechange: e => {
        if (e.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
          this._responseHeaders = e.responseHeaders
          this._status = e.status
          this._statusText = e.statusText
        }
        if (this.lastResponse !== e.response)
          this._text += this.lastResponse = e.response
        this._readyState = e.readyState
        this.dispatchEvent(new Event('readystatechange'))
        if (e.readyState === XMLHttpRequest.DONE) {
          this.dispatchEvent(new ProgressEvent('load'))
        } else this.dispatchEvent(new ProgressEvent('progress'))
      },
      ontimeout: e => {
        this._readyState = e.readyState
        this.dispatchEvent(new ProgressEvent('timeout'))
      }
    }).abort
  }
  constructor() {
    super()
    this.timeout = 0
    this._readyState = 0
    this._abortToken = null
    this._requestUrl = ''
    this._finalUrl = ''
    this._text = ''
    this._lastResponse = ''
    this._requestHeaders = {}
    this._responseHeaders = ''
    this._status = 0
    this._statusText = ''
  }
}

经测试,可以正常在 SSE.js 中运行。

欢迎使用到自己的项目中,注上我的 github profile 链接即可 (MIT 开源协议)。

让 SSE 真正用上 polyfill

然而,由于我们只能通过 dynamic import 导入 SSE.js,如何让它用上我们 polyfill 过的 XMLHttpRequest 呢?

我用了一个比较拙劣的方法,暂时也没想到别的更巧妙的方式,各位可以借鉴一下。其原理是将 SSE.js 官方代码中的 export 语句删除,然后使用 new Function 进行类似于 CommonJS 的 IIFE 导入。

js 复制代码
const code = (await fetch('https://cdn.jsdelivr.net/npm/sse.js@2.6.0/lib/sse.min.js').then(v => v.text())).replaceAll("export{SSE};", "")
const fn = new Function("XMLHttpRequest", "exports", code)
const exports = {}
fn(XMLHttpRequest, exports)
const SSE = exports.SSE

这样的缺点就是 fetch 自身在一些网站会违反 CSP 策略。希望有人能提供更好的处理方式。

结语

这次探究了一下油猴脚本处理跨域 SSE 的方式,也算积累了一点 code segments。

除了调用 Duckduckgo AI 进行翻译这个需求以外暂时没有想到别的场景,先这样吧。

相关推荐
要加油哦~3 分钟前
AI | 实践教程 - ScreenCoder | 多agents前端代码生成
前端·javascript·人工智能
一个public的class13 分钟前
你在浏览器输入一个网址,到底发生了什么?
java·开发语言·javascript
青茶36022 分钟前
php怎么实现订单接口状态轮询请求
前端·javascript·php
火车叼位1 小时前
脚本伪装:让 Python 与 Node.js 像原生 Shell 命令一样运行
运维·javascript·python
VT.馒头1 小时前
【力扣】2727. 判断对象是否为空
javascript·数据结构·算法·leetcode·职场和发展
鱼毓屿御1 小时前
如何给用户添加权限
前端·javascript·vue.js
JustHappy1 小时前
「web extensions🛠️」有关浏览器扩展,开发前你需要知道一些......
前端·javascript·开源
xixixin_2 小时前
【JavaScript 】从 || 到??:JavaScript 空值处理的最佳实践升级
开发语言·javascript·ecmascript
belldeep2 小时前
python:用 Flask 3 , mistune 2 和 mermaid.min.js 10.9 来实现 Markdown 中 mermaid 图表的渲染
javascript·python·flask
凉辰2 小时前
使用uni.createInnerAudioContext()播放指定音频(踩坑分享功能)
开发语言·javascript·音视频