ARTICLE DETAIL

资讯详情

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

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

bruno-query 深度解析:为 Bruno 响应体打造带深层导航、过滤与映射的 get 查询器 bruno-query 深度解析为 Bruno 响应体打造带深层导航、过滤与映射的 get 查询器【免费下载链接】brunoOpensource IDE For Exploring and Testing APIs (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 协议发布同时提供 CJSdist/cjs/index.js、ESMdist/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.0packages/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甚至能把两层订单里的idorder-1、1、2、order-2、3、4全部捞出来可见其搜索是多层级递归的。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 . orders与customer.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;三个关键设计从源码可以明确读出..触发深度搜索lookbehind ..被作为第三个参数传给getValue只有当上一个 token 是..时才执行深层递归取值普通的.只做单层属性读取源码第 144-145 行。[?]与fns按位置一一对应每遇到一个?就通过fns[funIndex]顺序取用下一个函数若用户写了[?]却没有传对应参数会直接抛出missing function for 上一个token的运行时错误。索引 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返回false、null、undefined时丢弃返回其它值如数字、字符串、对象则当作映射结果收集。当source不是数组时代码先把它包成[source]处理、结束后再取result[0]因此[?]也能作用在单个值上——测试中的..items..amount[0][?]amt amt 1结果11即此场景。对象谓词由 objectPredicate 生成逐键用!比较任一不符即返回false。边界行为与注意事项源自测试用例packages/bruno-query/tests/index.spec.ts 用it.each参数化表驱动测试覆盖了大量边界整理如下表达式预期结果说明customer.address.citybangalore常规嵌套取值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单值过滤不命中时无输出实践要点可归纳为四条路径中不要依赖空白——编译阶段replace(/\s/g, )会移除所有空格[?]与...fns必须数量匹配否则抛missing function for token深度搜索..会跨层级汇总所有同名属性结果通常是扁平数组若想取其中第一个用[0]收尾如..address[0]返回undefined是常态而非异常取值前最好先判断这正好契合 Bruno 脚本中先判断再断言的写法。在 Bruno 脚本生态中的实际落地bruno-query的价值最终体现在 Bruno 的断言与测试脚本中。它在 packages/bruno-js/src/bruno-response.js 中被用得非常巧妙BrunoResponse构造函数把实例包装成可调用对象callable (...args) get(this.body, ...args)于是脚本里拿到的res本身就能当函数调用等价于对响应体执行usebruno/query的get。同样在 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/querys get() invokes filters synchronously。这意味着在 QuickJS 沙箱环境里传给[?]的谓词/映射函数必须是同步的get自身也是同步求值的——设计上是刻意与异步过滤划清界限的。构建、测试与发布包内 packages/bruno-query/rollup.config.js 定义了双构建管线第一段把 src/index.ts 同时打包为 CJS 与 ESM经terser压缩、sourcemap开启第二段用rollup-plugin-dts把dist/esm/index.d.ts收敛为统一的dist/index.d.ts类型入口。发布前默认走npm test npm run build见 package.json 的prepack钩子测试执行jest。readme 也给出了面向 npm Registry 的标准发布命令npm publish --accesspublic由于该包同时服务 Electron 主应用脚本与 CLI 运行时的bruno-js依赖链发布usebruno/query新版本时需要同步在 packages/bruno-js/package.json 中升级对应依赖版本并回归jest测试以确保响应取值行为的一致性。【免费下载链接】brunoOpensource IDE For Exploring and Testing APIs (lightweight alternative to Postman/Insomnia)项目地址: https://gitcode.com/GitHub_Trending/br/bruno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表