油猴脚本也能用跨域 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/[email protected]/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 进行翻译这个需求以外暂时没有想到别的场景,先这样吧。

相关推荐
独立开阀者_FwtCoder1 分钟前
# 白嫖千刀亲测可行——200刀拿下 Cursor、V0、Bolt和Perplexity 等等 1 年会员
前端·javascript·面试
那小孩儿8 分钟前
?? 、 || 、&&=、||=、??=这些运算符你用对了吗?
前端·javascript
菜鸟码农_Shi16 分钟前
Node.js 如何实现 GitHub 登录(OAuth 2.0)
javascript·node.js
没资格抱怨21 分钟前
如何在vue3项目中使用 AbortController取消axios请求
前端·javascript·vue.js
总之就是非常可爱1 小时前
🚀 使用 ReadableStream 优雅地处理 SSE(Server-Sent Events)
前端·javascript·后端
ᖰ・◡・ᖳ1 小时前
Web APIs阶段
开发语言·前端·javascript·学习
stoneSkySpace1 小时前
算法——BFS
前端·javascript·算法
H5开发新纪元1 小时前
基于 Vue3 + TypeScript + Vite 的现代化移动端应用架构实践
前端·javascript
快乐的小前端2 小时前
class 类基础知识
前端·javascript
kovli2 小时前
红宝书第十讲:「构造函数与原型链」入门及深入解读:用举例子+图画理解“套娃继承”
前端·javascript