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

相关推荐
黑夜照亮前行的路9 分钟前
JavaScript 性能优化实战技术指南
javascript·性能优化
Stringzhua1 小时前
Vue数据的变更操作与表单数据的收集【6】
前端·javascript·vue.js
乐~~~2 小时前
el-date-picker type=daterange 日期范围限制
javascript·vue.js·elementui
初遇你时动了情2 小时前
uniapp vue3 ts自定义底部 tabbar菜单
前端·javascript·uni-app
烛阴4 小时前
掌握 TypeScript 的边界:any, unknown, void, never 的正确用法与陷阱
前端·javascript·typescript
前端工作日常6 小时前
H5 实时摄像头 + 麦克风:完整可运行 Demo 与深度拆解
前端·javascript
韩沛晓6 小时前
uniapp跨域怎么解决
前端·javascript·uni-app
Rubin936 小时前
TS 相关
javascript
拾光拾趣录7 小时前
JavaScript 究竟怎么跑
前端·javascript
Aotman_7 小时前
el-input 重写带图标密码框(点击小眼睛显示、隐藏密码)
前端·javascript·vue.js