前端人员应该知道些简单的语法分析技巧

现在工作中的项目是一个数据展示类的项目,主要负责将后端分析过的数据,通过 charts 显示到页面中。项目是前后端分离的,它构架是:

前端页面 -> nodejs 中间层 -> java 服务

这里会有一个 nodejs 中间层,是因为 java 服务提供的数据是尽可能和业务无关的,因此可能不能直接满足前端页面的需求,nodejs 中间层在这里会对数据进行一些和业务相关的处理和包装。

当然了,前端的我面临的实际情况则相当的残酷。

首页,java 服务是和业务耦合度非常高的,几乎不需要 nodejs 中间层的存在,下面就具体的业务来简单的描述下 java 接口的请求和相应内容。

比如页面中需要显示三个指标:访客数(UV),支付订单数(PAY_ORDER_CNT),退货率

对于前两个指标,要得到它们的数值非常简单,就是把括号中的字母内容作为 key 提交给接口服务,接口服务就会返回 key 所对应的数值,显示到页面即可。我们可以暂且把这种可以直接由 key 取到内容的指标成为 "简单指标",并且由于接口不支持多个 key 同时查询,所以每个简单指标必须独立为一个接口请求。

对于退货率这个指标,它是一个 "复合指标",它是由几个简单指标计算得来的,比如这个退货率,它在文档中的计算表达式是:

RETURN_ORDER_CNT/(RETURN_ORDER_CNT+SUB_PAY_ORDER_CNT)

这个表达式中涉及的两个简单指标是:退货订单数(RETURN_ORDER_CNT) 和 正向支付订单数(SUB_PAY_ORDER_CNT)

说一句题外话,严格来说,前端因为不是数据的提供方,所以并不清楚数据内部的细节,所以按接口文档来就行了。但是本着对工作负责的态度,有了这样一个疑问:

"直观上退货率的计算方式不是应该是: 退货订单数/支付订单数 吗?支付订单数直接对应了一个简单指标,上面已经提到了,就是 PAY_ORDER_CNT,所以我不清楚为什么这里的表达式为什么不能直接是 RETURN_ORDER_CNT/PAY_ORDER_CNT 问了 java 方面也是支支吾吾的说不出个所以然。"

言归正传。简单介绍了 java 接口的形式,那么很容易发现,如果页面中只需要展示简单指标的话,那完全没有 nodejs 中间层什么事情,但是还有复合指标,上面提到了,每个简单指标必须发起独立的接口请求,那么很明显,为了得到复合指标的值,必须对它所需的多个简单指标发起请求,所以这部分的工作应该放在 nodejs 层来做,这是无可厚非的。

但是负责 nodejs 工作的是这样说的,这种计算的步骤,应该放在客户端去做,从而减轻服务器的压力。不知道从哪里听来的说法,在一些特定的情况下,这句话可能会是对的,但是对于现在的情况,服务端在响应和处理多个简单指标请求所花的 CPU 和内存绝对不会比计算一个率来的少。所以 nodejs 中间层目前的工作就是原封不同的在前端和 java 接口之间做转发,这种代码在 nodejs 中实现不用超过 50 行并且没有任何技术难点。

到了最后,计算复合指标的工作就到了我的手上,当然了,我知道这样的工作放到浏览器里面做既不会减轻服务端的压力,还会增加页面的响应时间,拉低用户体验。所以在和相关人员以及领导密切沟通之后,得到了我自作多情的结果。

在和相关人员的沟通过程中,我发现他们 "不能做" ,而需要放在浏览器中来做的正真原因是,他们以为要计算复合指标是一件机械繁琐的事情,以为对每一个复合指标都需要写一套计算的代码,所以能推出去的就推出去,到了我这里就是个终点站了。

其实,如果知道像我一样知道一点语法解析的知识,只要一套代码就可以解决了,真是非常的简单。

首先复合指标都是一些简单的数学表达式,把这些复合指标的计算表达式形容成 DSL 也不为过。就像是写一个计算器一样,如果只是 eval 的话就会很尴尬,我们需要做一点 eval 内部的事情。

要计算复合指标,只需要分两步:

  1. 分别请求计算复合指标所需的每个简单指标的值
  2. 对表达式进行求值

如果我们把表达式当成一个编程语言来处理的话,那么按照规范就必须写出:词法分析,语法分析,语义分析,解释执行这几个步骤。有点杀鸡用牛刀的感觉,毕竟这里我们首要的问题是表达式中的运算符优先级。

所以,将处理方式简单化,只保留语法分析和解释执行两个阶段,因为我们的语法规则里面只有标识符和运算符,所以词法分析阶段就和语法分析交叉到一起就可以了,于是分为两步:

  1. 利用 Shunting Yard Algorithm,将表达式由 中缀(infix) 转换成 后缀(postfix) 形式,保存在一个栈中,
RETURN_ORDER_CNT/(RETURN_ORDER_CNT+SUB_PAY_ORDER_CNT)

就被处理成了

RETURN_ORDER_CNT RETURN_ORDER_CNT SUB_PAY_ORDER_CNT + /
  1. 然后逐个弹出栈中的内容,如果是运算符,就分别弹出两个操作数,如果操作数又是运算符,那么就递归调用,如果操作数是标识符,那么就带入之前的简单指标请求的结果,最后根据不同的运算符来对两个操作数进行运算。

这是一个在线的预览 复合指标计算,点击了 run 之后,打开 console 看下是否有没有通过的 assert 就可以了。

之所以记录这个问题,是因为它比较有代表性 - 既有技术上的细节,也有工作态度的一些描述。