返回

使用 Liquid 实现简单的数据交换

在平时的开发工作中,接口对接是一件无可避免的事情。虽然在“前后端分离”的大趋势下,后端的角色逐渐转换为数据接口的提供者,然而在实际的应用场景中,我们面对的往往是各种不同的“数据”,譬如企业应用中普遍使用的企业服务总线(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{% 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的特性,但对于我们了解一门“语言”已经足够了,因为对于一门编程语言来说,只要学会顺序、条件和循环三种结构足矣。言下之意呢,像常规elseelseifbreakcontinueLiquid都是支持的,这样子是不是更有编程语言的感觉了呢?除此之外,它还支持像 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 一直爽,可明明前端就有 HandlebarsEjs 这样的模板语言。最近一位同事写前端页面的经历不由得让我感慨,眼睛觉得简单的事情,为什么总是要求手去做呢?直接操作 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的JSON报文模板
基于Liquid的JSON报文模板

好了,现在我们使用 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 %}
]
Built with Hugo v0.104.1
Theme Stack designed by Jimmy
已创作 242 篇文章,共计 914893 字