Electron ClientRequest 类详解:基于 Chromium 网络栈发起 HTTP/HTTPS 请求的实现与实战
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
ClientRequest是 Electron 中用于发起 HTTP/HTTPS 请求的核心类,它是net模块(以及 Utility 进程中的网络 API)的底层返回类型。读完本文,你将完整掌握ClientRequest的全部构造参数(credentials、redirect、priority 等)及其与 Node.jshttp模块的行为差异,并通过 lib/common/api/net-client-request.ts 的源码实现理解请求体缓冲/分块编码、重定向策略与上传进度上报的底层机制,从而在主进程或 Utility 进程中编写健壮的网络请求代码。
定位:ClientRequest在哪里、如何获得
ClientRequest用于发起 HTTP/HTTPS 请求,可运行在主进程(Main)和 Utility 进程(Utility)中。需要特别注意它的使用限制:
该类不从
'electron'模块直接导出,只能作为 Electron API 中其他方法的返回值获得——最主要的入口就是 net.request()。
ClientRequest实现了 Node.js 的 Writable Stream 接口,因此本质上也是一个EventEmitter。从源码结构看,它的 TypeScript 实现直接继承自Writable:
// lib/common/api/net-client-request.ts export class ClientRequest extends Writable implements Electron.ClientRequest { ... }主进程与 Utility 进程的net.request都复用同一个ClientRequest实现,区别仅在主进程多了app.isReady()前置校验:
// lib/browser/api/net.ts export function request( options: ClientRequestConstructorOptions | string, callback?: (message: IncomingMessage) => void ) { if (!app.isReady()) { throw new Error('net module can only be used after app is ready'); } return new ClientRequest(options, callback); }也就是说:在ready事件之前调用net.request会直接抛错,Utility 进程则没有这个限制(见 lib/utility/api/net.ts)。
构造函数new ClientRequest(options)
options可以是字符串(按请求 URL 解释),也可以是对象(完整描述一次 HTTP 请求)。若以对象形式给出,支持以下属性:
| 属性 | 类型 | 说明 |
|---|---|---|
method | string(可选) | HTTP 请求方法,默认GET |
url | string(可选) | 请求 URL,必须为绝对形式,协议须为http或https |
headers | Record<string, string | string[]>(可选) | 随请求发送的头部 |
session | Session(可选) | 请求关联的Session实例 |
partition | string(可选) | 请求关联的 partition 名称,默认空字符串;session显式指定时partition被忽略 |
bypassCustomProtocolHandlers | boolean(可选) | 为true时不触发该 URL scheme 注册的自定义协议处理器,可用于将被拦截的请求转发给内置处理器;webRequest处理器仍会被触发。默认false |
credentials | string(可选) | 可为include、omit、same-origin,决定是否随请求发送凭据,见下文详述 |
useSessionCookies | boolean(可选) | 是否随请求发送来自所提供 session 的 cookies;指定了credentials时此项无效。默认false |
protocol | string(可选) | 可为http:或https:,默认http: |
host | string(可选) | 以hostname:port拼接形式提供的服务器主机 |
hostname | string(可选) | 服务器主机名 |
port | Integer(可选) | 服务器监听端口号 |
path | string(可选) | 请求 URL 的路径部分 |
redirect | string(可选) | 重定向模式:follow/error/manual,默认follow |
origin | string(可选) | 请求的 Origin URL |
referrerPolicy | string(可选) | 可为""、no-referrer、no-referrer-when-downgrade、origin、origin-when-cross-origin、unsafe-url、same-origin、strict-origin、strict-origin-when-cross-origin,默认strict-origin-when-cross-origin |
cache | string(可选) | 可为default、no-store、reload、no-cache、force-cache、only-if-cached |
priority | string(可选) | 可为throttled、idle、lowest、low、medium、highest,默认idle |
priorityIncremental | boolean(可选) | HTTP 可扩展优先级(RFC 9218)中的增量加载标志,默认true |
credentials与useSessionCookies的行为细节
这是最容易被用错的一组参数:
- 设为
include时,将使用请求关联 session 中的凭据; - 设为
omit时不发送凭据,遇到 401 时不会触发'login'事件; - 设为
same-origin时必须同时指定origin,否则构造函数直接抛错。源码中的对应校验为:
// lib/common/api/net-client-request.ts if (urlLoaderOptions.credentials === 'same-origin' && !urlLoaderOptions.origin) { throw new Error('credentials: same-origin requires origin to be set'); }- 若未指定
credentials,则发送 session 中的认证数据,但不发送 cookies(除非设置了useSessionCookies)。
测试用例 spec/api-net-spec.ts 中也验证了这一优先级关系:{ useSessionCookies: false, credentials: 'include' }与{ credentials: 'include' }被列为等价场景,印证了“指定credentials后useSessionCookies不再生效”的语义。
协议限制与 URL 组装
ClientRequest只支持http:和https:两种协议,源码中通过白名单校验:
// lib/common/api/net-client-request.ts const kHttpProtocols = new Set(['http:', 'https:']); ... if (!urlLoaderOptions.allowNonHttpProtocols && !kHttpProtocols.has(urlObj.protocol)) { throw new Error('ClientRequest only supports http: and https: protocols'); }protocol、host、hostname、port、path等属性严格遵循 Node.js URL 模块的模型。等价地,向github.com发起的同一请求可以用两种方式构造:
// 方式一:URL 字符串 const request = net.request('https://github.com/') // 方式二:显式字段(与 Node.js URL 模块模型一致) const request = net.request({ method: 'GET', protocol: 'https:', hostname: 'github.com', port: 443, path: '/' })解析逻辑位于 lib/common/api/net-client-request.ts 的parseOptions中:字符串参数会被new URL(optionsIn)解析;没有url时则从protocol/host/hostname/port/path拼装,且path中含空格会抛出Request path contains unescaped characters错误。此外redirect取值若不是follow/error/manual之一,同样在构造阶段就抛错。
实例事件(Instance Events)
Event: 'response'
responseIncomingMessage — 表示 HTTP 响应消息的对象。
Event: 'login'
authInfoObjectisProxybooleanschemestringhoststringportIntegerrealmstring
callbackFunctionusernamestring(可选)passwordstring(可选)
当需要认证的代理请求用户凭据时触发。callback应携带用户凭据调用:
request.on('login', (authInfo, callback) => { callback('username', 'password') })提供空凭据将取消该请求,并在响应对象上报告认证错误:
request.on('response', (response) => { console.log(`STATUS: ${response.statusCode}`) response.on('error', (error) => { console.log(`ERROR: ${JSON.stringify(error)}`) }) }) request.on('login', (authInfo, callback) => { callback() })从源码看,'login'事件由底层URLLoader的login事件转发而来;若没有任何监听者,Electron 会主动调用callback()取消认证——这与“空凭据取消请求”的行为一致:
// lib/common/api/net-client-request.ts this._urlLoader.on('login', (event, authInfo, callback) => { const handled = this.emit('login', authInfo, callback); if (!handled) { // If there were no listeners, cancel the authentication request. callback(); } });Event: 'finish'
在request数据的最后一个 chunk 写入request对象之后触发。
Event: 'abort'
当request被中止时触发;若request已经关闭(已发出close),则不会触发abort。
Event: 'error'
errorError — 提供有关失败信息的错误对象。
当net模块无法发出网络请求时触发。通常当request对象发出error事件后,会随后跟随一个close事件,并且不会再提供 response 对象。源码中URLLoader的错误会先销毁已存在的响应流,再让request走_die(error)(最终destroy(err))路径:
// lib/common/api/net-client-request.ts this._urlLoader.on('error', (event, netErrorString) => { const error = new Error(netErrorString); if (this._response) this._response.destroy(error); this._die(error); });Event: 'close'
作为 HTTP 请求-响应事务中的最后一个事件触发,表示request或response对象上不会再发出任何事件。
Event: 'redirect'
statusCodeIntegermethodstringredirectUrlstringresponseHeadersRecord<string, string[]>
当服务器返回重定向响应(如 301 Moved Permanently)时触发。调用request.followRedirect()会继续重定向。如果处理了这个事件,必须同步调用request.followRedirect,否则请求将被取消。
源码中manual策略的实现精确对应了“同步”这一要求:emit('redirect', ...)前后用try/finally包裹,事件返回后立即检查标志位,未同步跟进就报错销毁请求:
// lib/common/api/net-client-request.ts(redirect 处理节选) } else if (this._redirectPolicy === 'manual') { let _followRedirect = false; this._followRedirectCb = () => { _followRedirect = true; }; try { this.emit('redirect', statusCode, newMethod, newUrl, headers); } finally { this._followRedirectCb = undefined; if (!_followRedirect && !this._aborted) { this._die(new Error('Redirect was cancelled')); } } }三种redirect策略的行为汇总:
| 策略 | 遇到重定向时的行为 |
|---|---|
follow(默认) | 自动跟随;仍会 emit'redirect'事件供观察,此时调用followRedirect()无副作用 |
error | 请求立即失败,错误信息为Attempted to redirect, but redirect policy was 'error' |
manual | 取消重定向,除非在'redirect'事件中同步调用request.followRedirect() |
实例属性(Instance Properties)
request.chunkedEncoding
一个boolean,指定请求是否使用 HTTP chunked transfer encoding。默认false。该属性可读可写,但只能在第一次 write 之前设置(此时 HTTP 头部尚未发到网络),第一次 write 之后再设置会抛错。源码中还有一条更严格的约束——该属性只能被设置一次:
// lib/common/api/net-client-request.ts set chunkedEncoding(value: boolean) { if (this._started) { throw new Error('chunkedEncoding can only be set before the request is started'); } if (typeof this._chunkedEncoding !== 'undefined') { throw new Error('chunkedEncoding can only be set once'); } ... }需要发送大请求体时强烈建议使用 chunked 编码:数据会以小块形式流式传输,而不是在 Electron 进程内存中整体缓冲。从源码结构看这一建议的依据非常直接——未开启 chunked 时,请求体写入内部SlurpStream被整体拼接缓存,end之后以完整 Buffer 交给网络层;开启后才走ChunkedBodyStream管道,边写边发:
/** Writable stream that buffers up everything written to it. */ class SlurpStream extends Writable { _write(chunk: Buffer, encoding: string, callback: () => void) { this._data = Buffer.concat([this._data, chunk]); callback(); } ... }实例方法(Instance Methods)
request.setHeader(name, value)
namestring — 额外的 HTTP 头部名valuestring — 额外的 HTTP 头部值
添加一个额外 HTTP 头部。头部名会按原样发出、不做小写化。只能在第一次 write 之前调用,之后调用会抛错(源码报错文案为Can't set headers after they are sent)。若传入的 value 不是string,会调用其toString()取得最终值。setHeader前还会经过validateHeader校验:name/value 非法时分别抛出Invalid header name/Invalid value for header错误。
以下头部不允许应用设置(受限头部名单与 Chromium 的 header utils 一致):
Content-LengthHostTrailer或TeUpgradeCookie2Keep-AliveTransfer-Encoding
另外,将Connection头部设置为upgrade也不被允许。
request.getHeader(name)
namestring — 要查询的头部名
返回string— 之前设置的头部值(内部以头部名小写为键存储)。
request.removeHeader(name)
namestring — 要移除的头部名
移除之前设置的额外头部。同样只能在第一次 write 之前调用,之后调用会抛错。
request.write(chunk[, encoding][, callback])
chunk(string | Buffer) — 请求体的一个数据块;若是字符串,会用指定 encoding 转换为 Bufferencodingstring(可选)— 用于将字符串块转换为 Buffer,默认'utf-8'callbackFunction(可选)— 写入操作结束后调用
callback本质上是一个为保持与 Node.js API 相似性而引入的占位函数,它在 chunk 内容交付给 Chromium 网络层之后的下一个 tick异步调用。与 Node.js 实现不同,不保证callback调用时 chunk 内容已经刷到网络上。
向请求体添加一个数据块。第一次 write 可能就会把请求头发到网络上;第一次 write 之后不允许再添加或移除自定义头部。从源码看,非 chunked 模式下第一次write会创建SlurpStream并在finish时统一调用_startRequest()创建底层URLLoader;chunked 模式下则由ChunkedBodyStream在首次写数据时调用_startRequest()开始请求。
request.end([chunk][, encoding][, callback])
chunk(string | Buffer)(可选)encodingstring(可选)callbackFunction(可选)
返回this。
发送请求数据的最后一个 chunk。之后不允许再执行 write 或 end 操作。finish事件在 end 操作之后触发。
request.abort()
取消进行中的 HTTP 事务。如果请求已经发出过close事件,abort 操作不产生任何影响;否则正在进行的请求会发出abort和close事件。此外,若此时存在进行中的 response 对象,它将发出aborted事件。实现上abort()会先process.nextTick触发abort事件,再标记中止并通过_die()取消底层URLLoader。
request.followRedirect()
继续等待中的重定向。只能在'redirect'事件期间调用,否则源码会抛出followRedirect() called, but was not waiting for a redirect。
request.getUploadProgress()
返回Object:
activeboolean — 请求当前是否处于活动状态。若为false,其他属性均不会设置startedboolean — 上传是否已开始。若为false,current与total均为 0currentInteger — 目前已上传的字节数totalInteger — 本次请求将上传的总字节数
可以与POST请求配合,用于获取文件上传或其他数据传输的进度。实现上,URLLoader的upload-progress事件会同时更新内部状态并对外 emit 一个(目前未写入官方文档的)'upload-progress'事件,两者返回值一致:
// lib/common/api/net-client-request.ts this._urlLoader.on('upload-progress', (event, position, total) => { this._uploadProgress = { active: true, started: true, current: position, total }; this.emit('upload-progress', position, total); // Undocumented, for now }); getUploadProgress(): UploadProgress { return this._uploadProgress ? { ...this._uploadProgress } : { active: false, started: false, current: 0, total: 0 }; }spec/api-net-spec.ts 中的“should report upload progress”测试印证了这一契约:end之前getUploadProgress().active为false,upload-progress事件触发后返回值与事件参数(position, total)完全一致。
完整实战示例:流式上传与进度上报
结合前文要点,下面是一个在主进程中使用net.request的完整示例:大请求体使用 chunked 编码流式发送,并轮询上传进度:
const { app, net } = require('electron') app.whenReady().then(() => { const request = net.request({ method: 'POST', protocol: 'https:', hostname: 'example.com', port: 443, path: '/upload', redirect: 'follow', priority: 'low' }) // 大请求体:先开 chunked,再流式 write request.chunkedEncoding = true request.on('response', (response) => { console.log(`STATUS: ${response.statusCode}`) response.on('data', (chunk) => { console.log(`BODY: ${chunk}`) }) response.on('end', () => { console.log('No more data in response.') }) }) request.on('login', (authInfo, callback) => { callback('username', 'password') // 空凭据则取消请求 }) request.on('error', (err) => console.error(err)) request.on('close', () => console.log('transaction closed')) const timer = setInterval(() => { const p = request.getUploadProgress() console.log(p.active ? `uploaded ${p.current}/${p.total}` : 'inactive') }, 500) request.write(Buffer.from('first-chunk')) request.end(Buffer.from('last-chunk')) request.on('finish', () => clearInterval(timer)) })需要认证的代理场景中,也可参考 docs/api/net.md 列出的能力:net模块使用 Chromium 原生网络库,自动管理系统代理配置(含 wpad、PAC)、自动隧道化 HTTPS 请求、支持 basic / digest / NTLM / Kerberos / negotiate 等认证方案。
底层调用链速览
综合源码,ClientRequest的一次请求生命周期可以概括为:
net.request(options)(lib/browser/api/net.ts / lib/utility/api/net.ts)→new ClientRequest;- 构造函数中
parseOptions完成 URL 拼装、redirect/headers校验; - 首次写入请求体(或无 body 的
end())触发_startRequest(),调用 C++ 绑定createURLLoader(来自process._linkedBinding('electron_common_net'))创建底层URLLoader; URLLoader的response-started/data/complete/error/login/redirect/upload-progress事件逐一映射为response事件、IncomingMessage流数据、error/login/redirect/upload-progress事件;- 请求结束时通过
_die()销毁流并cancel()底层 loader,保证close作为最后事件。
C++ 侧的ElectronURLLoaderFactory(shell/browser/net/electron_url_loader_factory.h)进一步处理重定向时的 receiver 绑定等待,配合 JS 层的manual策略实现“等followRedirect()才继续”的语义。
参考
- 官方文档:docs/api/client-request.md、docs/api/net.md、docs/api/incoming-message.md
- JS 层实现:lib/common/api/net-client-request.ts
- 模块入口:lib/browser/api/net.ts、lib/utility/api/net.ts
- C++ 网络层:shell/browser/net/electron_url_loader_factory.h
- 行为验证测试:spec/api-net-spec.ts
【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考