字
字节笔记本
2026年2月21日
ChatHub proxy-fetch.ts 源码解析:浏览器扩展的代理请求机制
API中转
¥120
本文深入解析 ChatHub 浏览器扩展中的 proxy-fetch.ts 模块,该模块实现了通过浏览器标签页代理网络请求的核心机制,解决了浏览器扩展中跨域请求和内容脚本通信的技术难题。
代码概述
proxy-fetch.ts 是 ChatHub 浏览器扩展的核心服务模块,主要实现以下功能:
- 代理请求执行器 (
setupProxyExecutor): 在后台脚本中监听连接,处理来自内容脚本的代理请求 - 代理请求函数 (
proxyFetch): 允许内容脚本通过指定标签页发起网络请求
核心实现解析
1. 类型导入
typescript
import Browser from 'webextension-polyfill'
import {
ProxyFetchRequestMessage,
ProxyFetchResponseBodyChunkMessage,
ProxyFetchResponseMetadataMessage,
RequestInitSubset,
} from '~types/messaging'
import { uuid } from '~utils'
import { string2Uint8Array, uint8Array2String } from '~utils/encoding'
import { streamAsyncIterable } from '~utils/stream-async-iterable'模块依赖说明:
webextension-polyfill: 跨浏览器扩展 API 兼容层- 自定义消息类型定义:用于类型安全的进程间通信
- 工具函数:UUID 生成、编码转换、流式迭代
2. 代理执行器 setupProxyExecutor
typescript
export function setupProxyExecutor() {
Browser.runtime.onConnect.addListener((port) => {
const abortController = new AbortController()
port.onDisconnect.addListener(() => {
abortController.abort()
})
port.onMessage.addListener(async (message: ProxyFetchRequestMessage) => {
console.debug('proxy fetch', message.url, message.options)
const resp = await fetch(message.url, {
...message.options,
signal: abortController.signal,
})
// 发送响应元数据
port.postMessage({
type: 'PROXY_RESPONSE_METADATA',
metadata: {
status: resp.status,
statusText: resp.statusText,
headers: Object.fromEntries(resp.headers.entries()),
},
} as ProxyFetchResponseMetadataMessage)
// 流式传输响应体
for await (const chunk of streamAsyncIterable(resp.body!)) {
port.postMessage({
type: 'PROXY_RESPONSE_BODY_CHUNK',
value: uint8Array2String(chunk),
done: false,
} as ProxyFetchBodyChunkMessage)
}
port.postMessage({
type: 'PROXY_RESPONSE_BODY_CHUNK',
done: true
} as ProxyFetchResponseBodyChunkMessage)
})
})
}技术要点:
- 使用
Browser.runtime.onConnect建立长连接,每个请求对应一个独立端口 AbortController实现请求取消机制,端口断开时自动中止- 响应分两个阶段传输:元数据(状态码、响应头)和响应体(流式分块)
- 使用
Object.fromEntries()将 Headers 对象转换为普通对象以便序列化
3. 代理请求函数 proxyFetch
typescript
export async function proxyFetch(
tabId: number,
url: string,
options?: RequestInitSubset
): Promise<Response> {
console.debug('proxyFetch', tabId, url, options)
return new Promise((resolve) => {
const port = Browser.tabs.connect(tabId, { name: uuid() })
port.onDisconnect.addListener(() => {
throw new DOMException('proxy fetch aborted', 'AbortError')
})
options?.signal?.addEventListener('abort', () => port.disconnect())
const body = new ReadableStream({
start(controller) {
port.onMessage.addListener(function onMessage(
message: ProxyFetchResponseMetadataMessage | ProxyFetchResponseBodyChunkMessage,
) {
if (message.type === 'PROXY_RESPONSE_METADATA') {
const response = new Response(body, message.metadata)
resolve(response)
} else if (message.type === 'PROXY_RESPONSE_BODY_CHUNK') {
if (message.done) {
controller.close()
port.onMessage.removeListener(onMessage)
port.disconnect()
} else {
const chunk = string2Uint8Array(message.value)
controller.enqueue(chunk)
}
}
})
port.postMessage({ url, options } as ProxyFetchRequestMessage)
},
cancel(_reason: string) {
port.disconnect()
},
})
})
}技术要点:
- 通过
Browser.tabs.connect()与指定标签页建立连接 - 使用 UUID 作为端口名称,确保每个请求通道唯一
- 返回标准的
Response对象,与原生 fetch API 保持兼容 ReadableStream实现响应体的流式接收,支持大文件下载- 双向取消机制:外部信号取消会断开端口,端口断开会抛出 AbortError
设计亮点
1. 流式数据传输
使用 ReadableStream 和异步迭代器处理响应体,避免大响应占用过多内存:
typescript
for await (const chunk of streamAsyncIterable(resp.body!)) {
// 分块处理,每块即时发送
}2. 完整的取消支持
通过 AbortController 和 AbortSignal 实现请求取消:
typescript
const abortController = new AbortController()
port.onDisconnect.addListener(() => abortController.abort())
options?.signal?.addEventListener('abort', () => port.disconnect())3. 类型安全的消息传递
所有消息都使用 TypeScript 类型定义,确保编译时检查:
typescript
type ProxyFetchRequestMessage = {
url: string
options?: RequestInitSubset
}
type ProxyFetchResponseMetadataMessage = {
type: 'PROXY_RESPONSE_METADATA'
metadata: ResponseMetadata
}应用场景
此代理机制主要用于解决浏览器扩展中的以下问题:
- 跨域请求:内容脚本受 CSP 限制,通过后台脚本代理可绕过限制
- 身份隔离:不同标签页的请求上下文隔离,避免 Cookie 混淆
- 流式响应:支持 SSE 和大文件下载,保持响应实时性
项目链接
- GitHub 仓库:https://github.com/chathub-dev/chathub
- 文件路径:
src/services/proxy-fetch.ts
分享: