引子
前些日子在写 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 进行翻译这个需求以外暂时没有想到别的场景,先这样吧。