前后端数据交互那点事儿

一、背景

在还没有前后端分离概念的那个年代,前后端的工作基本上都是一个人做,早期的互联网公司体量小,业务简单,社会普通大众也还没有太多途径接触互联网,对前端视觉和交互效果的诉求也没有那么高,所以这种方式还是可行的。但是随着互联网行业的迅猛发展,一些互联网公司的体量和用户数量爆炸式的增长,公司的业务不管是在复杂度还是在数量上都呈现出一种井喷的状态,与此同时用户在满足自身功能需求的前提下,也越来越表现出对更好的体验和交互上的追求,通俗点说就是在“物质需求得到满足之后,精神需求日益凸显”。在这种大背景的变化之下,一个人前后端通吃似乎显得有些心有余而力不足,前后端分离也就自然而然的发生了。分离之后的前后端实现了工作上的拆分,为了尽可能做到并行开发,减少彼此之间的依赖,面向接口开发成为了主流,前后端开发前约定接口,形成接口文档,大家都按照文档进行开发,实现了开发过程中的并行。

二、分离后的阵痛

前后端分离符合软件行业发展的趋势和潮流,毋庸置疑是正确的。但是我们也不能忽视这种过程中产生的一些小问题,最显而易见的一点就是原来一个人做的事情,变成了两个人,甚至是两个部门之间的事,这就不可避免的带来沟通上的成本,人的增多也带来了更多的不稳定性,这种不稳定性最直接的体现在了接口的不稳定上。我大致将这种不稳定性出现的原因总结为如下两点:

  • 提前约定的接口文档无法做到强约束

  • 在将业务转化为数据这件事上不同的人之间存在差异

这两点原因导致的问题表现在开发中的方方面面:

  • 联调阶段的调试成本较大,更多的精力用在了兼容各种数据结构,而不是去检查业务逻辑是否能跑通并且正常

  • 前后端可能都需要花费较大精力做兼容

  • 在测试阶段这种数据的动态性导致的突发性问题一定程度上拉低了测试效率

  • 要求前端或者要求后端去改变逻辑来迎合彼此的实现逻辑是一件困难的事

这些影响可能会因为业务线和部门的不同有不同的表现,但或多或少都会存在问题。

三、关于问题的解决思路的一些探讨

问题是普遍存在的,那么有没有什么解决办法呢?

3.1 GraphQL

早在 2015 React 欧洲大会上,Lee Byron就提出了GraphQL。在GraphQL中,通过预先定义的SchemaType来实现调用者来声明接口应该返回的数据类型及结构这种实现的思路非常巧妙的将接口数据定义的工作从后端转移到了前端,更大程度上实现了前后端的解耦。但是,凡事都具有两面性,当我们沉浸在GraphQL理念上的巨大进步的同时,也要看到GraphQL在现有开发模式时上进行升级的成本。距离GraphQL推出已经很多年了,在火热程度上有点出道即巅峰的感觉(但是不可否认GraphQL相较于REST确实是一种进步,我个人还是非常看好GraphQL未来的前景的),看了网络上一些大佬的分析,指出了一些问题所在,有兴趣了解详情的同学可以跳转这里,因为我在GraphQL上没有什么使用经验,就不再赘述了。

3.2 防火墙模式

目前来看,似乎对这一问题还没有什么公认的,被广泛采用的解决方案。但是问题就摆在那里,在等待出现一种公认的,能够被广泛接受的解决方案的同时,不如先自己实现一种轻量简单的保障机制来实现对问题的规避。

假如我们将前后端看作两个黑盒子,现有的前后端交互是非常直接的:这种方式导致的问题就是当接口出现问题时前端和服务端都缺少自我保护的机制。前端经常会因为接口数据类型和数据结构上的问题导致需要在数据使用的地方添加各种类型判断和兼容措施,类似于如下所示的兼容代码随处可见:

if (result && result.data && result.data.name) {
   ... ...
}

这些代码一是不够灵活,改起来比较麻烦,二是会有大量冗余,漏改一个地方就可能导致出问题。所以既然前端和服务端对彼此而言都是个黑盒,那么对彼此而言对方都是不可信的,就有必要增加一种校验机制,对不可信的数据源做校验并且在校验不通过时有相应的处理机制,就好比是在内网入口架设了一道防火墙,校验机制就是防火墙的安全策略。

对于前端而言,“防火墙”应该具有哪些能力呢?在我看来,最核心的能力就是:

  • 数据类型校验

  • 数据结构校验

  • 校验未通过时的处理机制

除了上述这三种能力,是否还需要别的能力,例如错误上报,错误日志等可以结合自身业务进行扩展。

四、一种前端安全校验方式的简易实现

基于上述思路,我做了一些简单的尝试来校验可行性,因为时间和精力关系仅实现了数据类型和数据结构的校验。实现方式是使用json对象预先定义安全校验模版,模版包含期望得到的数据类型和数据结构,使用该模版对服务端数据进行校验。

4.1 代码结构图

4.2 调用方式

具体的调用方法如下所示:

4.2.1 声明模版

const tpl = {
 head: { // attrname: head: object
   module: "", // attrname: module: string
   function: "",
   error_code: 0, // attrname: module: number
},
 body: {
   data: [],
   status: {
     name: "", // attrname: name: string
     age: 1, // attrname: age: number
     flags: [] // attrname: flags: array
  }
}
};

模版声明的数据类型及数据结构。

4.2.2 服务端数据

const res = {
 head: {
   module: "module",
   function: "function",
   error_code: "0"
},
 body: {
   data: [
    {
       tag: 1,
       a: {}
    }
  ],
   status: {
     name: "lipeng",
     age: 18,
     flags: ["1", "2"]
  }
}
};

4.2.3 安全校验

compare(tpl, res);    // => return false;

校验结果不通过,因为head.error_code声明为number类型,实际返回为head.error_codestring类型。从数据的类型校验和结构校验来看,代码量很少,实现思路就是对json数据做了一下递归遍历关于compare函数的具体实现代码我这里就不放了,我的初衷是希望能让大家重视这个问题并给大家提供一个解决问题的思路,起到抛砖引玉的作用。

五、最后

这种“防火墙理念”的核心能力的实现可以是非常简单轻量的,实现成本低,使用成本低就使得业务线根据自身情况进行定制成为可能。所以不论是依赖第三方的类型校验库,还是自己实现,最终尽可能减少学习和实现成本,基于自身业务扩展,最高效的解决问题,提高研发和测试效率才是核心目标