electron-renderer-fetch
v0.0.1
Published
在 Electron 主进程里复用某个 BrowserWindow 渲染进程的 window.fetch,便于在该窗口的 DevTools Network 面板里抓包/调试主进程发起的网络请求。可选 onlyWhenDevToolsOpen,未启用时透明 fallback 到 net.fetch。
Readme
@nlai/electron-renderer-fetch
在 Electron 主进程里复用某个 BrowserWindow 渲染进程的
window.fetch,便于在该窗口的 Chrome DevTools → Network 面板里抓主进程发起的网络请求。
主进程默认走的是 electron.net.fetch,这条网络请求是看不到的——既不会出现在任何窗口的 DevTools 里,也不会被 Chromium 抓到。本库通过 IPC 把主进程的 fetch 调用「转发」到一个已注册的渲染进程里执行真正的网络 IO,请求自然就出现在该窗口的 Network 面板了。
特性
- 提供一个完全标准的
fetch函数(签名兼容globalThis.fetch)。 onlyWhenDevToolsOpen: true:只有当目标窗口打开了 DevTools 时才走代理,平时零开销 fallback 到net.fetch,生产环境无感知。- 流式响应支持(SSE / OpenAI streaming),
Response.body是ReadableStream<Uint8Array>。 AbortSignal双向取消(主→渲染、渲染→主、超时、stream.cancel())。- 单次请求超时 (
requestTimeoutMs)。 - body 不可序列化(如
FormData/ReadableStream)时自动 fallback 而不是静默丢 body。 RequestInit透传:method/headers/body/signal/credentials/mode/redirect/cache/referrer/referrerPolicy/integrity/keepalive/priority。- 渲染进程崩溃 / 关闭时,已在飞的请求会立刻收到
AbortError,不会悬挂。
行为差异需要心里有数
主进程 net.fetch 与渲染进程 window.fetch 走的是两套网络栈:
| 维度 | net.fetch (main) | window.fetch (renderer) |
| --- | --- | --- |
| Cookie jar | 主进程 session | 该窗口的 partition |
| Proxy 设置 | 主进程 session | 渲染进程 session |
| User-Agent | Electron UA | Chromium UA(含窗口注入) |
| TLS / 证书豁免 | net 层 | webSecurity 设置 |
切换到 renderer fetch 的请求行为会有微小差异,这是为了能在 DevTools 看到请求所付的代价。只在调试场景下使用代理(默认配合 onlyWhenDevToolsOpen: true)就能避免影响生产。
安装
作为 monorepo workspace 包使用时,在使用方的 package.json 里加:
{
"dependencies": {
"@nlai/electron-renderer-fetch": "workspace:*"
}
}然后用你项目当前的包管理器安装(pnpm / yarn / npm 任一)即可。本仓库使用 pnpm。
⚠️ 注意:本包是 ESM + TS 源码 + 子入口导出,必须让 electron-vite 把它一起 bundle,不能走 externalize。否则 Node 直接 import
.ts会失败。在
electron.vite.config.ts的main与preload两段都把它加进externalizeDepsPlugin的exclude列表:main: { plugins: [ externalizeDepsPlugin({ exclude: ['@nlai/electron-renderer-fetch' /* ... */] }) ] }, preload: { plugins: [ externalizeDepsPlugin({ exclude: ['@nlai/electron-renderer-fetch' /* ... */] }) ] }
用法
整套链路只有 3 步:
1. preload:注册当前窗口
// src/preload/index.ts
import { setupRendererFetchProxy } from '@nlai/electron-renderer-fetch/preload'
setupRendererFetchProxy()调用一次即可,幂等。所有用了同一份 preload 的 BrowserWindow 都会自动注册成「可被代理的渲染进程」。
2. 主进程:创建代理 fetch
⚠️ 调用时机:
createRendererFetch(...)必须在第一个BrowserWindow加载 preload 之前至少执行一次(IPC 监听器要先于REGISTER消息装好),否则首个窗口的 register 消息会被丢弃。一般做法是在主进程模块的顶层就
import+ 调用,让模块加载时即装好 listener;如果你只能在窗口创建之后才能拿到配置,可以提前调用一次installRendererFetchMain()占位(见 API 章节)。
// src/main/somewhere.ts
import { createRendererFetch } from '@nlai/electron-renderer-fetch/main'
import { windowService } from '@main/services/WindowService'
const myFetch = createRendererFetch({
// 首选这个窗口;它没注册 / 已销毁时会按 fallback 处理
preferredWebContents: () => windowService.getClawAgentsWindow()?.webContents,
// 推荐生产开启:仅在 DevTools 打开时才走代理,平时直接走 net.fetch
onlyWhenDevToolsOpen: true,
// 可选:单次请求超时(含等待 meta 与流式接收的整段时间)
requestTimeoutMs: 5 * 60 * 1000,
// 可选:自定义 fallback(默认 electron.net.fetch)
// fallback: globalThis.fetch,
// 可选:调用时如果还没渲染进程注册,最多等多少 ms 再 fallback
// waitForRendererMs: 1000,
// 可选:preferred 不可用时是否拒绝回退到其他注册窗口;默认 true(严格、安全)
// strictPreferred: true
})
// 返回值是一个完全标准的 fetch
const res = await myFetch('https://api.example.com/v1/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ ... })
})
const json = await res.json()3. 把代理 fetch 注入到第三方 SDK
例如把它塞给 OpenAI / @agentx/harness:
await harness.registerModelProvider({
...modelProviderConfig,
request: {
fetch: (_models, input, init) => myFetch(input, init)
}
})真实示例(本仓库 ai-studio)
// src/main/claw-agents/index.ts
import { createRendererFetch } from '@nlai/electron-renderer-fetch/main'
import { windowService } from '@main/services/WindowService'
const clawAgentsRendererFetch = createRendererFetch({
preferredWebContents: () => windowService.getClawAgentsWindow()?.webContents,
onlyWhenDevToolsOpen: true,
requestTimeoutMs: 5 * 60 * 1000
})
await harness.registerModelProvider({
...modelProviderConfig,
request: {
fetch: async (_models, input, init) => clawAgentsRendererFetch(input, init)
}
})调试方式:打开 ClawAgents 窗口 → F12 → 切到 Network 面板 → 触发一次模型请求,就能看到对应 HTTP 请求的 headers / body / response / timing。关掉 DevTools 后下次请求自动回到 net.fetch,不再产生 IPC 开销。
API
主进程 — import ... from '@nlai/electron-renderer-fetch/main'
createRendererFetch(options?: RendererFetchOptions): typeof fetch
返回一个标准 fetch。优先把请求转发到 preferredWebContents() 返回的渲染进程;不可用时走 fallback。
type RendererFetchOptions = {
/**
* 返回首选的 webContents。
* 指定后默认严格使用——不可用时直接 fallback,避免请求被悄悄发到无关窗口。
*/
preferredWebContents?: () => WebContents | null | undefined
/**
* 当 `preferredWebContents` 返回的 webContents 不可用时是否拒绝回退到其他已注册窗口。
* 默认 true(严格模式,安全)。设为 false 才会获得「任意可用 wc 都行」的旧行为。
* 当未提供 `preferredWebContents` 时此选项被忽略。
*/
strictPreferred?: boolean
/**
* 仅当目标 webContents 已打开 DevTools 时才走代理;未打开则 fallback。
* 推荐生产环境开启,做到「打开 DevTools 才抓包」、平时零开销。
*/
onlyWhenDevToolsOpen?: boolean
/** 没有可用渲染进程时使用的兜底 fetch,默认 `electron.net.fetch`。 */
fallback?: typeof fetch
/** 调用时若没有可用渲染进程,最长等待多少毫秒;默认 0(不等待,直接 fallback)。 */
waitForRendererMs?: number
/** 单次请求超时时间(毫秒);0 表示不限制。包括等待 meta 与流式接收的整段时间。 */
requestTimeoutMs?: number
}hasRegisteredRendererFetch(): boolean
是否有任意已注册、未销毁、未崩溃的 webContents(不考虑 DevTools 是否打开)。
installRendererFetchMain(): void
显式安装主进程侧 IPC 监听器。createRendererFetch() 内部会自动调用,因此通常不需要直接用它。
只在「需要 lazy 创建 fetch、但要确保不丢失 preload register 消息」的场景下才有用:在 app 顶层尽早调用一次 installRendererFetchMain() 即可消除该竞态。多次调用幂等。
Preload — import ... from '@nlai/electron-renderer-fetch/preload'
setupRendererFetchProxy(): void
把当前 webContents 注册为主进程的 fetch 代理。多次调用是幂等的。
teardownRendererFetchProxy(): void
注销 + 取消所有未完成请求。一般用于测试 / HMR。
共享 — import ... from '@nlai/electron-renderer-fetch/shared'
导出 IPC channel 名常量与序列化类型,仅在你需要做特殊扩展(比如自定义 transport)时才会用到。
工作原理
┌─────────────── main process ───────────────┐
│ │
│ createRendererFetch({preferredWC, ...}) │
│ │ │
│ │ targetWc.send(REQUEST) │
│ ▼ │
└─────── IPC ────────────────────────────────┘
│
▼
┌─────────── renderer process (preload) ─────┐
│ │
│ ipcRenderer.on(REQUEST) │
│ → window.fetch(url, init) │
│ → 把 meta + chunks 流式 send 回 main │
│ │
└────────────────────────────────────────────┘
│
▼ Network 面板里看得见的 HTTP 请求
外网主进程拿到 Response 后构造一个 ReadableStream<Uint8Array>,IPC 收到的每个 chunk 实时 enqueue 进去,所以业务代码可以无感地用流式 API(response.body.getReader() / response.text() / SSE 解析)。
限制 / 不支持的场景
body是FormData/ReadableStream时会自动 fallback 到net.fetch(IPC 不能稳定序列化它们)。- IPC 每个 chunk 一次
send,对每秒上千 chunk 的极快流可能有 IPC 开销上的额外消耗(OpenAI streaming 远低于这个量级,无影响)。 - 渲染进程的 cookies / proxy / UA 与主进程不一致,需要严格行为一致时不要开启
onlyWhenDevToolsOpen: false让它在生产里持续生效。
License
Internal use only (workspace package).
