news 2026/9/10 10:33:35

bruno-query 深度解析:为 Bruno 响应体打造带深层导航、过滤与映射的 get 查询器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
bruno-query 深度解析:为 Bruno 响应体打造带深层导航、过滤与映射的 get 查询器

bruno-query 深度解析:为 Bruno 响应体打造带深层导航、过滤与映射的 get 查询器

【免费下载链接】brunoOpensource IDE For Exploring and Testing API's (lightweight alternative to Postman/Insomnia)项目地址: https://gitcode.com/GitHub_Trending/br/bruno

本文档对应仓库内 packages/bruno-query/readme.md 这一独立 npm 子包。它围绕一个核心函数get(data, path, ...fns)展开,用字符串路径表达从嵌套 JSON/数组/对象中取值、过滤、映射的全部需求,并作为 Bruno 脚本运行时res()取值能力的基础设施被@usebruno/query所引用。读完本文,你将掌握其全部路径语法(...[0][?])、谓词/映射器的三种写法及其底层实现原理,并能在 Bruno 断言脚本中熟练使用这些表达式提取响应数据。

模块定位:一个只导出get的轻量查询器

在 Bruno 的多包仓库结构中,packages/bruno-query/package.json 显示这个包以@usebruno/query命名、版本0.1.0、MIT 协议发布,同时提供 CJS(dist/cjs/index.js)、ESM(dist/esm/index.js)与类型声明(dist/index.d.ts)三份产物。它的全部公开 API 只有一个:

export function get(source: any, path: string, ...fns: PredicateOrMapper[])

其余类型定义均为内部实现细节。source是待查询的数据(通常来自 API 响应体),path是描述导航规则的字符串,fns是与[?]占位符一一对应的"谓词函数 / 对象谓词 / 映射函数"集合。

其作用范围并不局限于 Bruno 应用自身:从 packages/bruno-js/package.json 的依赖声明看,脚本运行时包bruno-js直接依赖"@usebruno/query": "0.1.0",packages/bruno-js/src/bruno-response.js 与 packages/bruno-js/src/utils.js 均以require('@usebruno/query')引入。因此它既是可独立发布到 npm 的通用数据查询小工具,也是 Bruno 脚本生态中解析响应体的基础组件。

核心语法速览:从路径字符串到取值结果

以下语法均由 readme 原文档定义,并在 packages/bruno-query/tests/index.spec.ts 中得到逐条验证。

1. 普通数组导航:自动遍历所有层级的同名属性

get(data, 'customer.orders.items.amount')

当路径上某个字段对应的是数组时(例如orders是订单数组),get会自动对该数组逐项递归取值,最终把[10, 20, 30, 40]这样的扁平数组返回给你。测试用例customer.orders.items.amount → [10, 20, 30, 40]即验证了这一点——四个订单项的amount被聚合进了一个数组。

2. 深层导航:..双点号跳过中间层级

get(data, '..items.amount')

..表示"从当前位置出发,在后续嵌套对象中进行深度优先搜索",因此无需写全customer.orders.items.amount的完整链条。在测试数据中,..items.amount..amount的结果同为[10, 20, 30, 40]..id甚至能把两层订单里的id'order-1'12'order-2'34)全部捞出来,可见其搜索是多层级递归的。

3. 数组索引:[N]按下标取元素

get(data, '..items[0].amount') // → 10 get(data, '..items[5].amount') // → undefined(下标越界,安全返回)

索引符支持绝对索引。对于..items..amount[0]这类用法,会对深度搜索得到的扁平数组取首个元素;测试中也覆盖了越界场景..items[5].amount → undefined,越界不会抛异常而是返回undefined

4. 数组过滤:[?]+ 函数谓词

get(data, '..items[?].amount', i => i.amount > 20)

[?]是一个"惰性操作符",真正做什么取决于fns参数中传入的函数:如果函数返回布尔值,则当作谓词使用——true保留该项,继续沿路径取值。上例可筛出amount > 20的条目并取其amount,得到[30, 40]之类的子集。

5. 对象谓词:用字面量对象做等值匹配过滤

get(data, '..items[?]', { id: 2, amount: 20 })

[?]对应的参数是普通对象而非函数时,它等价于写i => i.id === 2 && i.amount === 20的函数谓词——对象的每个键值对按===逐一比对、全部命中才算通过。这正是 readme 中"same as (i => i.id === 2 && i.amount === 20)"的含义,测试用例..items[?].amount → [40](配合{ id: 4, amount: 40 })即为佐证。

6. 数组映射:[?]+ 映射函数

get(data, '..items..amount[?]', amt => amt + 10)

如果传入的函数返回的是非布尔值(且非null),则视为映射函数:返回值被收集进结果数组,替代原值。例如..items..amount[?]+amt => amt + 1会把所有金额映射为[11, 21, 31, 41]

源码级原理:路径编译与逐 token 求值

packages/bruno-query/src/index.ts 中get的实现分为两步。

第一步:字符串路径编译为 token 序列

const paths = path .replace(/\s+/g, '') // 去空白 .split(/(\.{1,2}|\[\?\]|\[\d+\])/g) // 按分隔符拆分 .filter((s) => s.length > 0) .map((str) => { str = str.replace(/\[|\]/g, ''); const index = parseInt(str); return isNaN(index) ? str : index; // 纯数字转 number });

正则(\.{1,2}|\[\?\]|\[\d+\])把路径切成四类 token:

  • ...—— 导航分隔符,...的唯一区别记录在lookbehind变量中;
  • [?]—— 处理时去掉方括号后得到字符串?,作为"消费一个 fns 参数"的标记;
  • [N]—— 去掉方括号后用parseInt转为数字,成为数组下标;
  • 普通属性名 —— 保持为字符串。

值得一提:去空白意味着路径中不能依赖空格语义,customer . orderscustomer.orders等价;同时parseInt('2')得到数字2,而parseInt('id')NaN于是保留字符串'id',据此区分索引与属性。

第二步:主循环按 token 类型分派

while (source != null && index < paths.length) { const token = paths[index++]; switch (true) { case token === '..': case token === '.': break; // 分隔符本身不做事 case token === '?': const fun = fns[funIndex++]; // 顺序消费谓词/映射器 if (fun == null) throw new Error(`missing function for ${lookbehind}`); source = filterOrMap(source, fun); break; case typeof token === 'number': source = normalize(source[token]); // 数组下标 break; default: source = getValue(source, token as string, lookbehind === '..'); // 属性读取 } lookbehind = token; } return source;

三个关键设计从源码可以明确读出:

  1. ..触发深度搜索lookbehind === '..'被作为第三个参数传给getValue,只有当上一个 token 是..时才执行深层递归取值,普通的.只做单层属性读取(源码第 144-145 行)。
  2. [?]fns按位置一一对应:每遇到一个?就通过fns[funIndex++]顺序取用下一个函数;若用户写了[?]却没有传对应参数,会直接抛出missing function for <上一个token>的运行时错误。
  3. 索引 token 走数组读取:数字 token 直接执行normalize(source[token])

normalize:递归扁平化

function normalize(value: any) { if (!Array.isArray(value)) return value; const values = [] as any[]; value.forEach((item) => { const value = normalize(item); if (value != null) { values.push(...(Array.isArray(value) ? value : [value])); } }); return values.length ? values : undefined; }

数组在导航过程中可能层层嵌套(对数组的每个元素再次取值又产生数组)。normalize递归把所有层级扁平化并跳过null/undefined;若最终一个值都没有则返回undefined。这也是为什么..address返回的是[{ city: 'bangalore' }]而非{ city: 'bangalore' }——多对象深度搜索的结果统一被包成扁平数组,测试用例'..address'一行的注释 "// .. will return array" 明确说明了这一约定。

getValue:单层读取与深层递归

getValue 负责属性取值:

  • source是数组时,对其每个元素递归getValue(item, prop, deep)后整体normalize,实现"自动遍历数组逐项取值";
  • source是对象时,先直接取source[prop];若处于deep(即上一个 token 是..)模式,则遍历对象所有其他键,对每个值为对象/数组的键递归深层搜索,且"一旦在某个分支直接命中了该属性就不会再往该值内部递归"(这正是..amount能跨层收集所有金额而不会重复挖掘amount内部结构的原因);
  • source既非对象又非数组时直接返回undefined,保证安全兜底。

filterOrMap:谓词与映射的统一分发

function filterOrMap(source: any, funOrObj: PredicateOrMapper) { const fun = typeof funOrObj === 'object' ? objectPredicate(funOrObj) : funOrObj; ... for (const item of list) { if (item == null) continue; const value = fun(item); if (value === true) result.push(item); // 谓词:通过则保留原值 else if (value != null && value !== false) result.push(value); // 映射:收集返回值 } return normalize(isArray ? result : result[0]); }

这段实现揭示了一个有趣的事实:谓词与映射没有独立语法,全靠返回值的类型区分。返回严格true时保留原 item;返回falsenullundefined时丢弃;返回其它值(如数字、字符串、对象)则当作映射结果收集。当source不是数组时,代码先把它包成[source]处理、结束后再取result[0],因此[?]也能作用在单个值上——测试中的..items..amount[0][?]+amt => amt + 1结果11即此场景。对象谓词由 objectPredicate 生成:逐键用!==比较,任一不符即返回false

边界行为与注意事项(源自测试用例)

packages/bruno-query/tests/index.spec.ts 用it.each参数化表驱动测试覆盖了大量边界,整理如下:

表达式预期结果说明
customer.address.city'bangalore'常规嵌套取值
customer.orders.items.amount[10,20,30,40]自动遍历数组
..items.amount[0]10深度搜索后取下标
..items[5].amountundefined越界安全返回
customer.orders.foo/..customer.fooundefined不存在的属性安全返回
..address[{ city: 'bangalore' }]深度搜索返回数组
..items[?].amount+{ id: 5, amount: 40 }undefined对象谓词无匹配项
..items..amount[0][?]+amt => amt > 30undefined单值过滤不命中时无输出

实践要点可归纳为四条:

  1. 路径中不要依赖空白——编译阶段replace(/\s+/g, '')会移除所有空格;
  2. [?]...fns必须数量匹配,否则抛missing function for <token>
  3. 深度搜索..会跨层级汇总所有同名属性,结果通常是扁平数组;若想取其中第一个,用[0]收尾(如..address[0]);
  4. 返回undefined是常态而非异常,取值前最好先判断,这正好契合 Bruno 脚本中"先判断再断言"的写法。

在 Bruno 脚本生态中的实际落地

bruno-query的价值最终体现在 Bruno 的断言与测试脚本中。它在 packages/bruno-js/src/bruno-response.js 中被用得非常巧妙:BrunoResponse构造函数把实例包装成可调用对象callable = (...args) => get(this.body, ...args),于是脚本里拿到的res本身就能当函数调用,等价于对响应体执行@usebruno/queryget

同样在 packages/bruno-js/src/utils.js 的createResponseParser中,res = (expr, ...fns) => get(response.data, expr, ...fns)把 query 直接桥接到响应解析器上。也就是说,你写在断言脚本里的res('..items[?].amount', i => i.amount > 20)这类调用,底层就是get的这套路径引擎在跑。

另一个值得注意的实现约束来自 packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js:注释明确写着"Safe because @usebruno/query's get() invokes filters synchronously"。这意味着在 QuickJS 沙箱环境里传给[?]的谓词/映射函数必须是同步的,get自身也是同步求值的——设计上是刻意与异步过滤划清界限的。

构建、测试与发布

包内 packages/bruno-query/rollup.config.js 定义了双构建管线:第一段把 src/index.ts 同时打包为 CJS 与 ESM(经terser压缩、sourcemap开启),第二段用rollup-plugin-dtsdist/esm/index.d.ts收敛为统一的dist/index.d.ts类型入口。

发布前默认走npm test && npm run build(见 package.json 的prepack钩子),测试执行jest。readme 也给出了面向 npm Registry 的标准发布命令:

npm publish --access=public

由于该包同时服务 Electron 主应用脚本与 CLI 运行时的bruno-js依赖链,发布@usebruno/query新版本时,需要同步在 packages/bruno-js/package.json 中升级对应依赖版本并回归jest测试,以确保响应取值行为的一致性。

【免费下载链接】brunoOpensource IDE For Exploring and Testing API's (lightweight alternative to Postman/Insomnia)项目地址: https://gitcode.com/GitHub_Trending/br/bruno

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 10:33:28

GE动态输入端口索引获取

GetDynamicInputIndexesByName 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTor…

作者头像 李华
网站建设 2026/9/10 10:31:38

WorkBuddy:面向学术认知过程的AI协作者

1. 这不是又一个“教授用AI写论文”的故事“一位教授与WorkBuddy的几个月&#xff0c;看看擦出了什么样的火花”——这个标题刚出现在我邮箱里时&#xff0c;我下意识点开又关掉三次。不是因为不感兴趣&#xff0c;而是太熟悉了&#xff1a;高校教师AI工具自动批改作业、生成PP…

作者头像 李华
网站建设 2026/9/10 10:28:53

YOLOv3-ROS机械臂抓取闭环系统:实时生成三维抓取位姿

简介&#xff1a;本资源是一个基于YOLOv3与PyTorch实现的ROS实时物体抓取检测功能包&#xff0c;面向机器人视觉方向的ROS开发者及高校机器人课程实践者&#xff0c;重点解决机械臂在Gazebo仿真环境中对螺丝等小目标的旋转角度感知与抓握定位问题。包内共110个文件&#xff0c;…

作者头像 李华
网站建设 2026/9/10 10:27:43

CANN/ge图引擎AttrValue.GetValue接口

GetValue 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow 前端的…

作者头像 李华
网站建设 2026/9/10 10:26:28

电视盒子播放全格式视频:TVBoxOSC 开源播放器免费使用指南

电视盒子播放全格式视频&#xff1a;TVBoxOSC 开源播放器免费使用指南 【免费下载链接】TVBoxOSC TVBoxOSC - 一个基于第三方项目的代码库&#xff0c;用于电视盒子的控制和管理。 项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC 把 NAS 里的 4K 纪录片投到…

作者头像 李华