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

相关推荐
拉不动的猪4 小时前
移动端调试工具VConsole初始化时的加载阻塞问题
前端·javascript·微信小程序
大金乄6 小时前
封装一个vue2的elementUI 表格组件(包含表格编辑以及多级表头)
前端·javascript
Lee川8 小时前
解锁 JavaScript 的灵魂:深入浅出原型与原型链
javascript·面试
swipe9 小时前
从原理到手写:彻底吃透 call / apply / bind 与 arguments 的底层逻辑
前端·javascript·面试
Lee川11 小时前
探索JavaScript的秘密令牌:独一无二的`Symbol`数据类型
javascript·面试
Lee川11 小时前
深入浅出JavaScript事件机制:从捕获冒泡到事件委托
前端·javascript
光影少年11 小时前
async/await和Promise的区别?
前端·javascript·掘金·金石计划
codingWhat11 小时前
如何实现一个「万能」的通用打印组件?
前端·javascript·vue.js
前端Hardy13 小时前
别再无脑用 `JSON.parse()` 了!这个安全漏洞你可能每天都在触发
前端·javascript·vue.js
前端Hardy14 小时前
别再让 `console.log` 上线了!它正在悄悄拖垮你的生产系统
前端·javascript·vue.js