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

相关推荐
拾光拾趣录5 分钟前
从“祖传”构造函数到 `class`
前端·javascript
yvvvy12 分钟前
从“按钮都不会点”到“能撸大厂 UI”:我用 react-vant 踢开组件库的大门!
前端·javascript
安然dn13 分钟前
Cropper.js:JS图像裁剪库
前端·javascript
Danny_FD1 小时前
Vue + Element UI 实现模糊搜索自动补全
前端·javascript
gnip1 小时前
闭包实现一个简单Vue3的状态管理
前端·javascript
Enddme1 小时前
《面试必问!JavaScript 中this 全方位避坑指南 (含高频题解析)》
前端·javascript·面试
你怎么知道我是队长1 小时前
python---eval函数
开发语言·javascript·python
用户2519162427111 小时前
Canvas之图像合成
前端·javascript·canvas
Mintopia2 小时前
🎯 光与面的命运交锋:Möller-Trumbore 线段三角形相交算法全解析
前端·javascript·计算机图形学
Ares-Wang2 小时前
Vue》》@ 用法
前端·javascript·vue.js