ByteNoteByteNote

字节笔记本

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. 完整的取消支持

通过 AbortControllerAbortSignal 实现请求取消:

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
}

应用场景

此代理机制主要用于解决浏览器扩展中的以下问题:

  1. 跨域请求:内容脚本受 CSP 限制,通过后台脚本代理可绕过限制
  2. 身份隔离:不同标签页的请求上下文隔离,避免 Cookie 混淆
  3. 流式响应:支持 SSE 和大文件下载,保持响应实时性

项目链接

分享: