electron-anyproxy源码解析(四):recorder基于nedb的请求录制与缓存架构设计
2026/8/21 15:11:43 网站建设 项目流程

electron-anyproxy源码解析(四):recorder基于nedb的请求录制与缓存架构设计

【免费下载链接】electron-anyproxy📢 A http/https proxy client, using to analyze and mock.项目地址: https://gitcode.com/gh_mirrors/el/electron-anyproxy

electron-anyproxy 是一款基于 Electron 的 http/https 代理客户端,核心能力是抓包分析与接口 Mock。本篇 electron-anyproxy源码解析继续深入其内部实现,聚焦请求录制模块 recorder:它如何在内存、nedb 数据库与文件系统三者之间分配职责,如何做到「元数据入库、响应体落盘」的缓存架构设计,以及如何通过事件机制把录制结果实时推送到前端界面。理解了这套架构,你就掌握了整个代理工具数据链路的地基。

一、recorder 模块的定位:整个代理工具的「数据中枢」

在 electron-anyproxy 中,recorder 是记录一切的模块:它记录每一次请求的 URL、Host、Method、请求头、响应头、状态码、耗时,以及完整的请求/响应体。它并不直接处理网络 IO,而是作为被动接收者,由请求处理链路主动喂数据。

recorder 实例在 proxy.js 的 ProxyServer 构造器中创建:

this.recorder = new Recorder(); global.recorder = this.recorder;

创建后,它被注入 RequestHandler(lib/requestHandler.js)和 webInterface(lib/webInterface.js),成为请求处理与前端展示之间的桥梁。可以说,recorder 是代理工具的「数据中枢」。

二、缓存架构设计:内存自增 ID + nedb 数据库 + 文件系统三层结构

这是整个模块最值得学习的设计。recorder 没有把所有数据一股脑塞进数据库,而是做了清晰的分层:

数据存储位置设计原因
自增 ID内存变量globalId生成唯一主键,避免依赖数据库自增
请求/响应元数据nedb 数据库文件结构化查询、排序、分页
响应体内容独立文件res_body_{id}大体积二进制不污染数据库,按需读取

1. 随机缓存目录:每次启动都是全新的快照

recorder 初始化时会生成一个随机缓存目录(lib/recorder.js):

const CACHE_DIR_PREFIX = 'cache_r'; const DB_FILE_NAME = 'anyproxy_db'; function getCacheDir() { const rand = Math.floor(Math.random() * 1000000); const cachePath = path.join(proxyUtil.getAnyProxyPath('cache'), './' + CACHE_DIR_PREFIX + rand); fs.mkdirSync(cachePath); return cachePath; }

目录位于~/.anyproxy/cache/cache_r{随机数}下。随机后缀意味着每次启动代理都是全新的录制快照,互不干扰,也简化了清理逻辑。

2. nedb 数据库:零配置的嵌入式文档数据库

recorder 使用 nedb 作为存储引擎。nedb 是纯 JavaScript 实现的嵌入式 NoSQL 数据库,API 与 MongoDB 高度相似,非常适合 Electron 这类需要随应用分发、又不想引入重量级数据库服务的场景:

db = new Datastore({ filename: dbFilePath, autoload: true }); db.persistence.setAutocompactionInterval(5001);

两个细节值得注意:

  • autoload: true让数据库打开即用;
  • setAutocompactionInterval(5001)每 5 秒自动压缩一次数据文件。nedb 采用 append-only 写入,频繁更新会产生大量冗余记录,自动压缩能显著减小文件体积。

如果磁盘文件加载失败(比如权限问题),代码会优雅降级为纯内存数据库,保证代理功能不中断,这种容错思路值得借鉴。

3. 响应体落盘:body-map 设计

响应体(可能包含大图片、大文件)没有存入数据库,而是单独写成文件:

const BODY_FILE_PRFIX = 'res_body_'; self.updateRecordBody = function (id, info) { if (!id || !info.resBody) return; const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id); fs.writeFile(bodyFile, info.resBody); };

数据库里只保存_id,响应体通过res_body_{id}文件按需读取。这种「元数据与内容分离」的缓存架构设计,避免了数据库膨胀,也让大响应体的读取不会阻塞录制流程。

三、请求录制核心流程:appendRecord 与 updateRecord 的前后呼应

录制不是一个动作,而是一对动作,由 lib/requestHandler.js 在请求处理的不同阶段触发:

  1. 请求到达时:调用recorder.appendRecord(resourceInfo),分配自增 ID,插入初始记录,并立即通过事件推送前端,用户能马上看到「请求已发出」;
  2. 响应返回后:填充状态码、响应头、响应体、耗时等字段,调用recorder.updateRecord(resourceInfoId, resourceInfo)更新同一条记录。

这两个方法内部都调用了normalizeInfo,把原始信息规整为统一的记录结构:

singleRecord._id = id; singleRecord.url = info.url; singleRecord.host = info.host; singleRecord.method = info.method; singleRecord.reqHeader = info.req.headers; singleRecord.startTime = info.startTime; singleRecord.statusCode = info.statusCode; singleRecord.resHeader = info.resHeader; singleRecord.duration = info.endTime - info.startTime;

其中duration(耗时)由 endTime 减去 startTime 计算得出,为前端展示接口耗时提供了直接数据。

四、响应体解码:字符集识别与类型判定的巧妙处理

响应体拿到手后不能直接展示,recorder 提供了getDecodedBody方法,完成两件事:

  • 字符集解码:遍历响应头 JSON 字符串,正则匹配charset,若服务端不是 UTF-8 编码,则用 iconv-lite 转码;
  • 内容类型判定:根据content-type区分 JSON、图片和普通文本,图片以 Buffer 形式返回,便于前端直接渲染。

对应前端接口位于 lib/webInterface.js,/fetchBody?id=xx返回解码后的内容,&raw=true则直接输出原始二进制(如图片)。

五、查询接口设计:满足列表、分页与详情的全部场景

recorder 暴露了四个查询方法,覆盖前端所有数据需求:

  • getSummaryList(cb):全量查询,用于统计;
  • getRecords(idStart, limit, cb):按_id升序排序并分页,配合 Web 界面「加载更多」的场景,例如 webInterface 中一次取 10000 条(lib/webInterface.js 的/latestLog接口);
  • getSingleRecord(id, cb):按主键查询单条详情;
  • getBody(id, cb)/getDecodedBody(id, cb):读取响应体。

排序、limit 都直接在 nedb 查询中完成,无需在 JS 层做二次处理,代码非常精简。

六、事件驱动:EventEmitter 与 WebSocket 的实时推送链路

recorder 继承了events.EventEmitter,这是它连接前端的关键设计。emitUpdate在每次 append 或 update 后触发update事件,而 lib/wsServer.js 订阅了这个事件:

recorder.on('update', (data) => { sendMultipleMessage(data); });

消息不会一条条立刻发出,而是先进入messageQueue队列,由 50ms 的定时器批量打包广播,前端通过 WebSocket 一次性收到多条更新,大幅降低推送频率、提升渲染性能。这正是客户端 network.vue 中请求列表实时滚动的数据来源。

七、缓存清理机制:关闭即清空

ProxyServer 的close()方法会调用recorder.clear(),通过递归删除缓存目录实现一键清空(lib/util.js 的deleteFolderContentsRecursive)。因为缓存目录是随机命名的独立快照,删除时无需担心误伤其他数据,这也是随机目录设计带来的另一大红利。

八、总结:recorder 架构设计的三个启示

回顾整个 lib/recorder.js,这套请求录制与缓存架构设计给我们带来三点启发:

  1. 职责分层:元数据入库、内容落盘、ID 自增放内存,各司其职,互不拖累;
  2. 容错降级:数据库加载失败自动切内存模式,保证代理主流程不受影响;
  3. 事件解耦:录制、推送、展示通过事件机制解耦,新增消费方只需监听update事件,无需改动录制核心。

下一篇 electron-anyproxy源码解析将继续剖析 rule 规则引擎与 Mock 机制,看这些被录制的数据如何反过来支撑接口模拟与篡改,敬请期待。

【免费下载链接】electron-anyproxy📢 A http/https proxy client, using to analyze and mock.项目地址: https://gitcode.com/gh_mirrors/el/electron-anyproxy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询