在平时的开发工作中,接口对接是一件无可避免的事情。虽然在“前后端分离”的大趋势下,后端的角色逐渐转换为数据接口的提供者,然而在实际的应用场景中,我们面对的往往是各种不同的“数据”,譬如企业应用中普遍使用的企业服务总线(ESB),这类服务要求服务接入者必须使用WebService来作为数据交换格式;再譬如电子数据交换(EDI)这种特定行业中使用的数据交换格式,从可读性上甚至还不如基于XML的WebService……而更为普遍的则可能是需要使用Word、Excel、CSV来作为数据交换的媒介。顺着这个思路继续发散下去,进入我们失业的或许还有各种数据库,譬如MySQL和MongoDB;各种大数据平台,譬如Hadoop和Spark;各种消息队列,譬如RabbitMQ和Kafka等等。

注意到,这里反复提到的一个概念是数据交换(Data Switching),它是指在多个数据终端设备间,为任意两个终端设备建立数据通信临时互联通路的过程。自从阿里提出“中台”的概念以来,越来越多的公司开始跟风“中台”概念,并随之衍生出譬如组织中台、数据中台、业务中台、内容中台等等的概念。今天这篇博客,我并不打算故弄玄虚地扯这些概念,我的落脚点是接口级别的数据交换,主要通过Liquid这款模板引擎来实现。它对应我在这篇博客开头提到的场景:一个对外提供RESful风格API的系统,如何快速地和一个WebService实现对接。总而言之,希望能对这篇博客对大家有所启发吧!

关于Liquid

首先,我们来介绍Liquid,通过它的官方网站,我们应该它是一门模板语言。对于模板语言,我们应该是非常熟悉啦,JavaScript里的HandlebarsEjs就是非常著名的模板语言。如大家所见,这个博客就是用Ejs模板渲染出来的。而到了三大前端框架并驾齐驱的时代,模版语法依然被保留了下来,比如Vue{{model.userName}}标记常常用来做文本插值。所以,如果要认真追溯起来的话,也许这些框架都或多或少的收到了Liquid的影响,因为它的基本语法如下:

1
2
//使用page实例的title属性插值
{{ page.title}}

假设page是一个对象,它的title属性值为:Introduction,此时,渲染后的结果即为:Introduction。是不是感觉非常简单呢? 我们继续往下看。除了基本的“插值”语法以外,我们可以用{% tag %}这种结构(Liquid称之为Tag):

1
2
3
4
5
6
7
8
9
10
//声称变量author并赋值
{% sssign author = '猫先森' %}
//条件语句
{% if author == '猫先森' %}
帅哥,你好
{% endif %}
//循环语句
{% for post in posts %}
{{post.date}}-{{post.title}}
{% endfor %}

这里仅仅展示了一部分Liquid的特性,但对于我们了解一门“语言”已经足够了,因为对于一门编程语言来说,只要学会顺序、条件和循环三种结构足矣。言下之意呢,像常规elseelseifbreakcontinueLiquid都是支持的,这样子是不是更有编程语言的感觉了呢?除此之外,它还支持像tablerow这样的Tag,主要用来渲染HTML里的表格。

也许有人想说,这玩意儿有什么用呢?抱歉啊,这玩意儿还真有用。像发送邮件、发送短信这种一般都需要写个字符串模板的,简单的大家可以用String.Format()或者$来搞定,可一旦遇上循环的场景,这种基于字符串替换的方式就有点力不从心了。不开玩笑地说,在代码里用StringBuilder拼接HTML的方式,实在是太傻逼了。如果用Liquid写可能就是:

1
2
3
4
5
6
7
亲爱的{{ model.UserID }}:
您好!您有以下设备即将超过校验有效期,请及时采取有效行动。
{% for equipment in model.Equipments %}
{{ equipment.EquipmentID }}
{% endfor %}

{{ model.SendBy }}

显然,这个代码比拼接字符串要优雅很多。博主曾经在一个前端页面看到过大量的HTML拼接操作,果然是jQuery操作DOM一时爽,jQuery操作DOM一直爽,可明明前端就有HandlebarsEjs这样的模板语言。最近一位同事写前端页面的经历不由得让我感慨,眼睛觉得简单的事情,为什么总是要求手去做呢?直接操作DOM带来的弊端就是,业务逻辑永远和DOM纠缠在一起,那些没有人敢改的JavaScript代码,那些未经模块化全局引入的JavaScript代码,虽然马上就要2020年了,写下这些句子的时候还是感到魔幻,可能这就是所谓的魔幻现实主义吧。

OK, 我们把思绪拉回到Liquid。除了使用各种Tag实现流程控制以外,Liquid中还提供了过滤器(Filter)的概念,过滤器主要是配合{{ variable | filter }}语法来使用的。比如说,数据层返回了一个负数,而展示层希望展示正数,在不确定这个数值是否被别人使用的情况下,贸然去修改数据层的返回值是件危险的事情。此时,我们可以:

1
2
3
4
5
6
//对绑定的变量或者值取绝对值
{{ -17 | abs}}
//保留小数位
{{ 183.357 | round: 2 }}
//日期/时间格式
{{ article.created_date | data: %b %d, %Y}}

类似小数点位数、日期/时间格式等问题,均可以在Liquid中找到相应的过滤器。需要说明的是,Liquid使用Ruby进行开发的。也许在读到这篇博客前,大家都没有听说过Liquid,那么至少听说过Jekyll这个著名的静态博客生成器吧。实际上,在我写这篇博客的时候,我刚刚了解到一件事情,Jekyll就是基于Liquid而开发的,想到当初搭建这个博客时被Ruby劝退的回忆,我大概想不到有一天会再次接触它吧,不得不说,人生还真是奇妙啊!

一个简单的想法

好了,关于Liquid的介绍我们先了解到这里。写到这里,再回头去看我们一开始的问题,即:怎么把上游的数据(Model)转化为下游的数据(Template)。这里暂且抛开它到底是XML、JSON还是EDI这种细节性的问题,我想我们大概会有一个简单的想法,如果把需要传输给对方的接口报文做成模板,然后通过Liquid语法完成数据的绑定,那么数据映射这一层的工作就可以减轻不少,毕竟写A.XXX=B.XXX这种赋值语句是没什么前途的啦,而AutoMapper则需要提前写好Map并注册,经过一番权衡,我们来验证一下我们的想法吧!

这段时间一直在和金蝶K3Cloud接口做对接,坦白说我觉得金蝶的接口设计得非常糟糕,从它那个奇葩的FNumber字段就能看出来,而且它试图用一个接口做完所有事情的做法恕我不敢苟同,在我看来它违反了单一职责原则。因为要对接的接口数量多、字段多,我首先根据字段对应关系制作了一份Liquid模板,并根据业务上的需要,用主表(Main) + 明细表(Details)的方式来定义数据,这意味着我接下来只需要根据业务实现不同的数据源即可:

基于Liquid的JSON报文模板
基于Liquid的JSON报文模板

好了,现在我们使用Liquid的.NET版本DotLiquid来负责模板的解析和渲染,这个库可以直接通过Nuget安装,可以注意到这个代码非常的简单:

1
2
3
4
5
6
7
string RenderTpl(string filePath, dynamic model)
{
var content = File.ReadAllText(filePath);
var template = Template.Parse(content);
var output = template.Render(Hash.FromAnonymousObject(model));
return output;
}

实际上渲染后的文本就是对方需要的接口报文了,此时,该怎么样就怎么样处理,只需要把这个报文发送给对方就可以了。唯一需要花时间的就是对字段、写绑定,相比写实体类的方式效率要高更多。这种方式的话,我个人觉得更适合分工合作,如果需要数据加字段,那在数据层(Model)里增加就好了,而像改字段映射关系、字段默认值都可以由别人来完成。我一直相信,开发并不是帮别人做越多事情越好,而是可以提供一种能力让别人去做更多的事情,这就是我们常常听到的“赋能”。继续延伸下去的话,传统的MVC其实和Liquid是一个道理,都是根据数据去生成视图,无非是我们这里的”视图”变成了数据报文。

本文小结

通过日常工作中的接口对接这一典型场景,我们引出了“数据交换”的概念,而最低层级的数据交换实际上是接口报文的交换。为此,我们介绍了Liquid模板引擎,它提供的语法可以让我们完成一系列的绑定,顺着这个思路,博主为大家展示了这种想法的可行性。Liquid是一个非常成熟的模板引擎,无论是编写邮件、短信的文本模板,还是轻量级的文本表达式实现,都是一个非常不错的选择。即使是做一个ApiCaller,一定要做一个有头脑的ApiCaller。好了,以上就是这篇博客的全部内容啦,欢迎大家留言,谢谢大家。

2020-01-09 更新

在组织JSON中的数组结构时,需要在各元素间添加,,同时最后一个元素不需要,,此时,可以使用以下语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"FEntity": [
{% for Detail in Details %}
{
"FCOSTID": {
"FNumber": "{{Detail.FCOSTID}}"
},
"FCOSTDEPARTMENTID": {
"FNumber": "BM000005"
},
"FINVOICETYPE": "0",
"FTOTALAMOUNTFOR": {{Detail.FEE_AMOUNT}},
}
{% if forloop.last == false %},{% endif %}
{% endfor %}
]