ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

undici MockClient 完全指南:用单连接 MockClient 拦截 HTTP 请求

undici MockClient 完全指南:用单连接 MockClient 拦截 HTTP 请求 后端网络通信【免费下载链接】undiciAn HTTP/1.1 client, written from scratch for Node.js项目地址https://gitcode.com/gh_mirrors/un/undici点击查看免费下载MockClient是 undici 中基于真实Client实现的 Mock 调度器它以单连接模式挂载在MockAgent下拦截与注册路由匹配的请求直接返回预编程的模拟响应完全不走网络。本文以 MockClient.md 为骨架结合 mock-client.js、mock-utils.js 等源码与 test/mock-client.js 测试用例系统讲解它的获取方式、拦截匹配、响应定义、生命周期管理与底层实现原理帮助你在单连接场景下写出可复现、可维护的 HTTP 测试。MockClient 是什么MockClient是Client的子类用于拦截与已注册路由匹配的请求并以模拟响应应答而不是访问真实网络。它由MockAgent在配置为单连接connections: 1时创建并暴露与MockPool相同的拦截 APIInterceptable接口。在绝大多数场景中你不直接构造MockClient而是通过mockAgent.get(origin)获取——只要 agent 的connections选项设为1返回的就是MockClient实例否则返回MockPool。这一点在 mock-agent.js 的kFactory中有直接证据[kFactory] (origin) { const mockOptions Object.assign({ agent: this }, this[kOptions]) return this[kOptions] this[kOptions].connections 1 ? new MockClient(origin, mockOptions) : new MockPool(origin, mockOptions) }获取 MockClientmockAgent.get(origin)先创建一个connections: 1的MockAgent再调用get(origin)即可拿到与origin绑定的MockClient。后续对同一 origin 的调用返回同一个实例内部缓存在MockAgent的 clients Map 中。ESM 写法import { MockAgent } from undici // connections: 1 会让 agent 分发 MockClient 实例 const mockAgent new MockAgent({ connections: 1 }) const mockClient mockAgent.get(http://localhost:3000)CommonJS 写法const { MockAgent } require(undici) const mockAgent new MockAgent({ connections: 1 }) const mockClient mockAgent.get(http://localhost:3000)origin只应包含协议、主机名和端口如http://localhost:3000不需要路径。它支持string、RegExp、Function三种匹配形式匹配器类型通过条件string与值精确相等RegExp正则表达式匹配Function函数返回true从源码看get()内部会先通过normalizeOrigin归一化 origin支持 URL 对象并做小写化见 mock-utils.js若传入非字符串匹配器RegExp/Function还会按匹配器命中动态创建对应的 MockClient 并把已注册的拦截共享给它见 mock-agent.js。直接构造 MockClient 与MockClientOptionsnew MockClient(origin[, options])origin{string}与该 mock client 关联的 origin只含协议、主机名和端口。options{MockClientOptions}继承Client的 options。返回{MockClient}构造函数中agent选项是必填的且必须实现Agent接口即暴露dispatch方法否则抛出InvalidArgumentError。源码实现在 mock-client.jsconstructor (origin, opts) { if (!opts || !opts.agent || typeof opts.agent.dispatch ! function) { throw new InvalidArgumentError(Argument opts.agent must implement Agent) } super(origin, opts) this[kMockAgent] opts.agent this[kOrigin] origin this[kIgnoreTrailingSlash] opts.ignoreTrailingSlash ?? false this[kDispatches] [] this[kConnected] 1 this[kOriginalDispatch] this.dispatch this[kOriginalClose] this.close.bind(this) this.dispatch buildMockDispatch.call(this) this.close this[kClose] }对应的测试也验证了这一点test/mock-client.js 中断言new MockClient(http://localhost:9999, { agent: { get: not a function } })会抛InvalidArgumentError而传入合法MockAgent则不会。MockClientOptions继承自ClientOptions新增两个成员选项类型默认值说明agentMockAgent—与该 mock client 关联的 agent必填ignoreTrailingSlashbooleanfalse匹配拦截请求的 path 时是否忽略尾部斜杠ignoreTrailingSlash在构造时被写入内部符号kIgnoreTrailingSlash并作为拦截器的基础配置向下传递——intercept()创建MockInterceptor时会把该值作为默认值合并进去见 mock-client.jsintercept (opts) { return new MockInterceptor( opts { ignoreTrailingSlash: this[kIgnoreTrailingSlash], ...opts }, this[kDispatches] ) }此外MockAgent也支持全局ignoreTrailingSlash选项构造时同样做了类型校验必须是 boolean见 mock-utils.js。注册拦截路由mockClient.intercept(options)intercept()在 mock client 上注册一条路由并返回MockInterceptor随后用reply()或replyWithError()定义该路由的应答。只有与 mock client 的 origin 相同的请求才会参与匹配。import { MockAgent } from undici const mockAgent new MockAgent({ connections: 1 }) const mockClient mockAgent.get(http://localhost:3000) mockClient.intercept({ path: /foo, method: GET }).reply(200, foo)匹配选项详解options即MockInterceptor.Options定义请求的匹配条件选项类型默认值说明pathstring\|RegExp\|Function—要匹配的路径函数形式接收请求路径字符串并返回 booleanmethodstring\|RegExp\|FunctionGET要匹配的方法函数形式接收请求方法字符串并返回 booleanbodystring\|RegExp\|Function—要匹配的请求体函数形式接收请求体字符串并返回 booleanheadersObject\|Function—要匹配的请求头可以是「header 名 → string/RegExp/匹配函数」的映射也可以是接收全部 headers 并返回 boolean 的单一函数queryObject—要匹配的查询参数ignoreTrailingSlashboolean继承自 mock client匹配 path 时是否忽略尾部斜杠请求只有全部匹配条件都通过才会被拦截与 MockPool 语义一致。各种匹配器统一经由matchValue处理见 mock-utils.jsfunction matchValue (match, value) { if (typeof match string) { return match value } if (match instanceof RegExp) { return match.test(value) } if (typeof match function) { return match(value) true } return false }测试中还验证了两个细节test/mock-client.js不传path会抛InvalidArgumentError(opts.path must be defined)小写方法名会被自动转成大写如patch→PATCH这是MockInterceptor构造器中对字符串方法做toUpperCase()的结果见 mock-interceptor.js。定义模拟响应MockInterceptor 与 MockScope拦截器返回的MockInterceptor提供以下应答方法方法说明reply(statusCode[, data[, responseOptions]])定义匹配请求的响应data支持string/Buffer/对象自动 JSON 序列化/函数以入站请求为参数动态计算响应体responseOptions可附带headers与trailersreply(callback)通过回调动态计算全部响应选项statusCode、data、responseOptions支持异步返回 Promise 会被 awaitreplyWithError(error)让匹配到的请求抛出指定错误defaultReplyHeaders(headers)为该拦截器后续所有响应设置默认响应头defaultReplyTrailers(trailers)为该拦截器后续所有响应设置默认响应尾replyContentLength()自动为后续响应计算并写入content-length头返回的MockScope用于控制响应被消费的次数方法说明delay(waitInMs)延迟waitInMs毫秒后再返回响应参数必须是大于 0 的整数persist()让该响应无限匹配每次匹配请求都收到同样的应答times(repeatTimes)只让该响应匹配固定次数persist()优先级更高默认情况下每次intercept()只能被一个匹配请求消费需要匹配多次时要么为每个预期请求各调用一次intercept()要么使用persist()/times()。在 mock-interceptor.js 中可以看到reply()会把statusCode、data、responseOptions组装成 dispatch 数据并调用addMockDispatch压入kDispatches队列同时返回MockScope而 dispatchMockReply 负责在匹配发生时把响应数据交给 handler调用onResponseStart、onResponseData、onResponseEnd并在delay存在时用setTimeout延迟发送。若定义的是错误响应response.error ! null则走handler.onResponseError并删除该 dispatch。匹配与回退逻辑底层实现原理当通过mockClient.dispatch()发起请求时会进入buildMockDispatch构造的包装函数mock-utils.jsmock 激活时调用mockDispatch匹配请求。匹配过程在getMockDispatchmock-utils.js中按「路径 → 方法 → 请求体 → 请求头」的顺序逐步过滤未消费的拦截器路径匹配时若拦截器设置了ignoreTrailingSlash则先通过removeTrailingSlash去掉两侧尾斜杠再比较路径会先经safeUrl归一化确保查询参数顺序一致。任一环节失败都会抛出MockNotMatchedError。匹配失败时的回退若MockAgent启用了网络连接enableNetConnect全部放行或按 host 匹配器放行则回退到原始Client的dispatch发起真实请求若disableNetConnect()已调用kNetConnect false则直接抛出MockNotMatchedError错误信息会附带origin、剩余拦截器数量等调试信息。mock 未激活时透传原始dispatch行为等同普通Client。测试 test/mock-client.js 直接以手工填充kDispatches的方式验证了单拦截器 dispatch 与错误透传逻辑。生命周期管理cleanMocks()与close()mockClient.cleanMocks()v7.11.0 加入移除 mock client 上注册的全部拦截器调用前定义但尚未被消费的 mock 将不再匹配后续请求。import { MockAgent } from undici const mockAgent new MockAgent({ connections: 1 }) const mockClient mockAgent.get(http://localhost:3000) mockClient.intercept({ path: /foo }).reply(200, foo) mockClient.cleanMocks()源码实现非常直接——清空kDispatches数组mock-client.js。mockClient.close()返回{Promise}mock client 关闭后 fulfill 为undefined。优雅关闭 mock client等待所有已入队请求完成然后从关联的MockAgent中移除。实现上它会先调用原始的Client.close()再把kConnected置 0并从 agent 的 clients Map 中删除该 originmock-client.jsasync [kClose] () { await promisify(this[kOriginalClose])() this[kConnected] 0 this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) }import { MockAgent } from undici const mockAgent new MockAgent({ connections: 1 }) const mockClient mockAgent.get(http://localhost:3000) await mockClient.close()发起请求dispatch()与request()mockClient.dispatch(options, handlers)options{DispatchOptions}请求选项。handlers{DispatchHandler}请求生命周期中触发的处理器。返回{boolean}——若 dispatcher 繁忙返回false调用方应等待后再 dispatch否则true。dispatch()是对dispatcher.dispatch(options, handlers)的覆写它将请求与已注册的 mock 进行匹配——所有更高级别的方法如request的 mocking 行为都由它驱动。mockClient.request(options[, callback])options{DispatchOptions}callback{Function}可选不要求 Promise 时用回调接收响应。返回{Promise}——未提供callback时以模拟响应 fulfill。该方法继承自Client完整参数与返回值文档见 dispatcher.request(options[, callback])。import { MockAgent } from undici const mockAgent new MockAgent({ connections: 1 }) const mockClient mockAgent.get(http://localhost:3000) mockClient.intercept({ path: /foo }).reply(200, foo) const { statusCode, body } await mockClient.request({ origin: http://localhost:3000, path: /foo, method: GET }) console.log(response received, statusCode) // response received 200 for await (const data of body) { console.log(data, data.toString(utf8)) // data foo }注意这里request直接挂在mockClient上因此可以只传path/method而省略origin而把mockClient作为dispatcher传给全局request()时请求本身仍需携带完整的http://localhost:3000/fooURL见 MockAgent.md 中的「Returning a MockClient」示例。完整实战单连接 MockClient 测试示例结合上文一个覆盖「注册、匹配、消费、断言」的完整流程如下import { MockAgent, setGlobalDispatcher, request } from undici const mockAgent new MockAgent({ connections: 1 }) setGlobalDispatcher(mockAgent) const mockClient mockAgent.get(http://localhost:3000) // 注册两条路由/foo 可无限匹配/bar 只匹配一次 mockClient .intercept({ path: /foo, method: GET }) .reply(200, foo) .persist() mockClient .intercept({ path: /bar, method: POST, body: data }) .reply(200, { ok: true }, { headers: { content-type: application/json } }) // 第一次请求命中 /foo const foo await request(http://localhost:3000/foo) console.log(foo.statusCode) // 200 // 再次请求仍命中persist 生效 await request(http://localhost:3000/foo) // 精确匹配 body 的 POST 请求命中 /bar const bar await request(http://localhost:3000/bar, { method: POST, body: data }) console.log(bar.statusCode) // 200 // 断言没有遗留未消费的拦截器 mockAgent.assertNoPendingInterceptors() await mockAgent.close()assertNoPendingInterceptors()会扫描MockAgent下所有 MockClient/MockPool 中仍处于 pending 状态的拦截器mock-agent.js存在即抛出带表格化明细的UndiciError非常适合在测试收尾处验证「预期中的 mock 都按预期被消费」。相关资源类的完整 API 参考MockClient.md、MockAgent.md、MockPool.md底层实现lib/mock/mock-client.js、lib/mock/mock-agent.js、lib/mock/mock-utils.js、lib/mock/mock-interceptor.js测试证据test/mock-client.js、test/mock-agent.js类型定义types/mock-client.d.ts、types/mock-agent.d.ts需要留意的是MockClient与MockPool只在「单连接 vs 多连接」这一形态上有别拦截 API 完全一致单连接场景connections: 1下优先使用MockClient语义上更贴近真实Client的使用方式。赞分享后端网络通信【免费下载链接】undiciAn HTTP/1.1 client, written from scratch for Node.js项目地址https://gitcode.com/gh_mirrors/un/undici点击查看免费下载相关推荐Hutool请求拦截HTTP请求响应拦截Hutool请求拦截HTTP请求响应拦截 还在为HTTP请求的全局处理而烦恼每次都要手动添加相同的header、记录日志、处理异常Hutool的HTTP拦后端开发工具Plate 编辑器基准测试的中性协议注册表与 Fixture 层构建 editor-benchmarks 可信证据体系Plate 编辑器基准测试的中性协议注册表与 Fixture 层构建 editor benchmarks 可信证据体系 本文以 Plate 仓库中的 2026后端网络通信Requestly HTTP拦截器完全教程拦截、修改和监控网络请求 终极指南掌握Requestly HTTP拦截器的完整使用方法 作为前端开发者和QA工程师最受欢迎的调试工具Requestly能帮助你轻松拦截、修改和文档开发工具接口测试上一篇AI ShortChatGPT Shortcut完整部署指南Vercel、Docker 与 Cloudflare 的标准化落地下一篇如何突破图像缩放限制专业矢量转换工具全攻略创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表