在平时的开发工作中,接口对接是一件无可避免的事情。虽然在“前后端分离”的大趋势下,后端的角色逐渐转换为数据接口的提供者,然而在实际的应用场景中,我们面对的往往是各种不同的“数据”,譬如企业应用中普遍使用的企业服务总线(ESB),这类服务要求服务接入者必须使用 WebService 来作为数据交换格式;再譬如电子数据交换(EDI)这种特定行业中使用的数据交换格式,从可读性上甚至还不如基于 XML 的 WebService……而更为普遍的则可能是需要使用 Word、Excel、CSV 来作为数据交换的媒介。顺着这个思路继续发散下去,进入我们失业的或许还有各种数据库,譬如 MySQL 和 MongoDB;各种大数据平台,譬如 Hadoop 和 Spark;各种消息队列,譬如 RabbitMQ 和 Kafka 等等。
注意到,这里反复提到的一个概念是数据交换(Data Switching),它是指在多个数据终端设备间,为任意两个终端设备建立数据通信临时互联通路的过程。自从阿里提出“中台”的概念以来,越来越多的公司开始跟风“中台”概念,并随之衍生出譬如组织中台、数据中台、业务中台、内容中台等等的概念。今天这篇博客,我并不打算故弄玄虚地扯这些概念,我的落脚点是接口级别的数据交换,主要通过 Liquid 这款模板引擎来实现。它对应我在这篇博客开头提到的场景:一个对外提供 RESful 风格 API 的系统,如何快速地和一个 WebService 实现对接。总而言之,希望能对这篇博客对大家有所启发吧!
关于 Liquid
首先,我们来介绍Liquid,通过它的官方网站,我们应该它是一门模板语言。对于模板语言,我们应该是非常熟悉啦,JavaScript 里的Handlebars和Ejs就是非常著名的模板语言。如大家所见,这个博客就是用 Ejs
模板渲染出来的。而到了三大前端框架并驾齐驱的时代,模版语法依然被保留了下来,比如 Vue
中 {% raw %}{{model.userName}}{% endraw %}
标记常常用来做文本插值。所以,如果要认真追溯起来的话,也许这些框架都或多或少的收到了 Liquid
的影响,因为它的基本语法如下:
// 使用page实例的title属性插值
{{ page.title}}
假设 page 是一个对象,它的 title 属性值为:Introduction,此时,渲染后的结果即为:Introduction。是不是感觉非常简单呢? 我们继续往下看。除了基本的“插值”语法以外,我们可以用 {% raw %}{% tag %}{% endraw %}
这种结构(Liquid 称之为 Tag):
// 声称变量author并赋值
{% sssign author = '猫先森' %}
// 条件语句
{% if author == '猫先森' %}
帅哥,你好
{% endif %}
// 循环语句
{% for post in posts %}
{{post.date}}-{{post.title}}
{% endfor %}
这里仅仅展示了一部分Liquid
的特性,但对于我们了解一门“语言”已经足够了,因为对于一门编程语言来说,只要学会顺序、条件和循环三种结构足矣。言下之意呢,像常规else
、elseif
、break
和continue
,Liquid
都是支持的,这样子是不是更有编程语言的感觉了呢?除此之外,它还支持像 tablerow
这样的 Tag,主要用来渲染 HTML 里的表格。
也许有人想说,这玩意儿有什么用呢?抱歉啊,这玩意儿还真有用。像发送邮件、发送短信这种一般都需要写个字符串模板的,简单的大家可以用 String.Format()
或者 $
来搞定,可一旦遇上循环的场景,这种基于字符串替换的方式就有点力不从心了。不开玩笑地说,在代码里用 StringBuilder
拼接 HTML 的方式,实在是太傻逼了。如果用 Liquid
写可能就是:
亲爱的{{ model.UserID }}:
您好!您有以下设备即将超过校验有效期,请及时采取有效行动。
{% for equipment in model.Equipments %}
{{ equipment.EquipmentID }}
{% endfor %}
{{ model.SendBy }}
显然,这个代码比拼接字符串要优雅很多。博主曾经在一个前端页面看到过大量的 HTML 拼接操作,果然是 jQuery 操作 DOM 一时爽,jQuery 操作 DOM 一直爽,可明明前端就有 Handlebars 和 Ejs 这样的模板语言。最近一位同事写前端页面的经历不由得让我感慨,眼睛觉得简单的事情,为什么总是要求手去做呢?直接操作 DOM 带来的弊端就是,业务逻辑永远和 DOM 纠缠在一起,那些没有人敢改的 JavaScript 代码,那些未经模块化全局引入的 JavaScript 代码,虽然马上就要 2020 年了,写下这些句子的时候还是感到魔幻,可能这就是所谓的魔幻现实主义吧。
OK, 我们把思绪拉回到 Liquid
。除了使用各种 Tag 实现流程控制以外, Liquid
中还提供了过滤器(Filter)的概念,过滤器主要是配合 {% raw %}{{ variable | filter }}{% endraw %}
语法来使用的。比如说,数据层返回了一个负数,而展示层希望展示正数,在不确定这个数值是否被别人使用的情况下,贸然去修改数据层的返回值是件危险的事情。此时,我们可以:
//对绑定的变量或者值取绝对值
{{ -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
的 .NET 版本 DotLiquid 来负责模板的解析和渲染,这个库可以直接通过 Nuget
安装,可以注意到这个代码非常的简单:
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 中的数组结构时,需要在各元素间添加,
,同时最后一个元素不需要,
,此时,可以使用以下语法:
"FEntity": [
{% for Detail in Details %}
{
"FCOSTID": {
"FNumber": "{{Detail.FCOSTID}}"
},
"FCOSTDEPARTMENTID": {
"FNumber": "BM000005"
},
"FINVOICETYPE": "0",
"FTOTALAMOUNTFOR": {{Detail.FEE_AMOUNT}},
}
{% if forloop.last == false %},{% endif %}
{% endfor %}
]