你写过这种代码吗?前端传过来?status=active&age_gte=18,后端用正则拆字符串,再堆三层if判断。需求一变,加了个"或"条件,整个解析逻辑重写一遍。这不是技术债,这是技术高利贷——利滚利,越还越多。
GitHub上有个叫nestjs-filter-grammar的项目,用一套40年前就有的理论,把这个问题彻底终结。作者说:"你不是在处理边界情况,你是在定义一个让非法输入无法被表达的结构。"这话听着像数学系黑话,但看完实现你会发现,它比任何手写解析器都狠。
从"字符串地狱"到递归下降
问题的根源很朴素:布尔逻辑是递归的。AND和OR可以无限嵌套,但HTTP查询参数是扁平的键值对。status=active OR status=pending这种需求,用传统方式要么发明一套私有编码规范,要么直接告诉客户端"不支持"。
作者举了个算术表达式的例子。小学数学告诉我们先乘除后加减,但这不是硬编码在代码里的规则——它来自语法结构的层级。term嵌套在expression里,所以乘法自然比加法绑得更紧。括号能无限嵌套,因为factor可以展开成( expression ),而expression里又有factor。
这套东西叫上下文无关文法(Context-Free Grammar,CFG)。名字吓人,核心就一句话:用几条产生式规则,描述整个语言的合法结构。解析器不是"处理各种情况",而是"识别规则允许的结构"——非法输入根本进不来。
手写解析器的噩梦在于,你总在补漏洞。用户输入了个你没预料到的嵌套?崩溃。多了个空格?崩溃。大小写混用?再写一层trim。CFG的思路是:漏洞本来就不该存在。
Chevrotain:把理论变成TypeScript
理论归理论,作者选了Chevrotain这个TypeScript工具包落地。第一步是词法分析(lexer):把原始字符串切成token流。这里有个反直觉的细节——token定义的顺序直接影响匹配结果。
Chevrotain是贪婪匹配。>=和>同时存在时,如果先定义>,那>=永远匹配不到,会被拆成>和=两个token。所以多字符运算符必须排在单字符前面:
作者列了11种比较操作符。从>=、<=这种常规的,到*~(不区分大小写包含)、^=(前缀匹配)、$~(后缀匹配)这种业务常用的。每个都是先多字符、后单字符,顺序错一点,整个语法就崩。
这套token定义本身就是文档。新来的开发者不用读解析逻辑,看一眼token列表就知道系统支持哪些查询方式。比翻几百行正则表达式高效47倍——我编的,但你觉得呢?
为什么这比你手写的好
作者没明说的是,这个项目解决了一个NestJS生态的结构性痛点。官方文档教你用@Query()装饰器取参数,但复杂过滤怎么办?要么上GraphQL,要么自己造轮子。大多数团队选了第三条路:凑合。
凑合的结果是什么?我见过一个生产系统,过滤参数解析用了600行正则+条件判断,注释里写着"TODO: 重构"。三年后那个TODO还在,原作者离职了。
CFG方案的优势不是性能——虽然递归下降解析器通常够快——而是正确性。你用文法规则描述的语法,解析器"由构造保证"能处理所有合法输入。这不是营销话术,是形式语言理论的结论。换句话说,bug的搜索空间被规则本身压缩了。
项目里没提的一个细节:Chevrotain能自动生成语法错误提示。用户输了个错括号,你返回"position 23: unexpected token",而不是500 Internal Server Error。这种体验差距,换算成客服工单,可能值半个程序员。
谁该用,谁不该用
如果你只是做简单的等值查询,typeorm的where选项够用,别折腾。但如果你需要把过滤逻辑暴露给前端,让用户自己组合条件——比如后台管理系统、数据报表、低代码平台——这套方案能省掉你未来两年的维护成本。
作者的项目目前star数不多,但代码质量极高。测试覆盖、类型定义、错误处理都到位,不是那种"概念验证"级别的玩具。NestJS社区有个怪现象:官方不做的功能,往往有高质量第三方实现,但发现的人少。
这个项目就是典型。它解决的是个老问题,用的是个老理论,但组合出来的体验是新的。不是那种"令人兴奋"的新,是那种"早该如此"的新——像第一次用自动扶梯,之前爬楼梯的日子突然显得有点傻。
你现在的项目里,查询参数解析写了多少行代码?如果超过200行,可能该算算技术高利贷的利息了。
热门跟贴