中台背景下如何用Node做BFF层
中台的概念在最近几年很火,并且也是大势所趋。服务器端改造成中台,避免不了使用微服务。服务器端微服务化之后,和我们传统意义上的前后端协同开发是有区别的,我们有幸在公司中台改造中,也对前端开发模式及框架进行了改造。
服务器端微服务化
常见的服务器端语言有java、php、c#、python、golang、nodejs。每种语言实现微服务可能需要选择不同的微服务RPC框架,常用的RPC框架有:
-
Dubbo,仅支持Java
-
Motan,仅支持C++
-
Tars,仅支持Java
-
Spring Cloud,仅支持Java
-
gRPC,支持多种语言
-
Thrift,支持多种语言
当时公司的服务端语言主要使用Java,但是不同的部门语言的使用情况不同,可能考虑对于多种跨平台语言的支持,公司的RPC框架使用的是Thrift。
微服务化前开发模式
服务器端微服务化之前,前后端开发模式很简单,服务器端提供接口,前端直接调用。Java API这层就相当于BFF(Back-end For Front-end),严格来说任何语言都能做BFF层。
这种模式,前端只要调用服务器端API就可以,服务器端的逻辑处理及相互调用关系,前端不用关心,前端需要什么样的接口就让服务器端提供,这也是很多公司的开发模式。
微服务化后开发模式
服务器端微服务化后,会选择不同的RPC框架,就我们公司而言,我们选择了Thrift框架,此时微服务给外界提供的是RPC接口,而前端不能直接调用RPC接口,只能让Java开发人员提供HTTP接口,这样服务器端既要写微服务层,又要实现BFF层,工作量不言而喻,并且对于前后端人员的协同开发复杂化,我们看看微服务化后的开发模式:
Node作为BFF层
如果用Java实现微服务和BFF,Java开发人员要根据前端的需求来不断更改API逻辑,我们有没有一种方式,让Java开发人员只关注微服务层呢,那就是前端实现BFF层。不言而喻,前端能很好的实现BFF,那就是Nodejs的特长,如果前端实现BFF层,后端人员只需要关注微服务的逻辑,之后前端需要怎么组装这些接口,由Nodejs来实现,更改后的开发模式:
可能没使用过Node开发的同学会问,Nodejs可以调用RPC么?答案是肯定的,gRPC和Thrift都
提供了多语言支持。
Node如何调用RPC
传统开发模式中,即使有的公司使用了Node作为中间层,但是Node只是起到了代理的作用,前端请求Node提供的接口,Node甚至都没做任何逻辑处理,直接通过HTTP client或者CURL方式透传给Java层:
这种方式,Node只是一个Proxy,在项目开发中毫无用处,甚至还增加了系统维护成本。我们看看Node如何调用RPC呢,以Thrift为例,看看Thrift定义:
Thrift 是一种轻量级的跨语言 RPC 通信方案,支持多达 25 种编程语言。为了支持多种语言,跟 gRPC 一样,Thrift 也有一套自己的接口定义语言 IDL,可以通过代码生成器,生成各种编程语言的 Client 端和 Server 端的 SDK 代码,这样就保证了不同语言之间可以相互通信
所谓IDL,就是接口定义语言Interface Definition Language,Thrift官网提供了针对各种语言的IDL生产方式,都是命令行的,Nodejs也不例外。
Node如何调用Thrift RPC
- 服务器端通过工具把接口定义生产**.thrift文件
- 前端拿到**.thrift文件,通过工具生产Node识别的IDL文件
通过命令行工具生产Node使用的IDL文件
thrift -r --gen js:node tutorial.thrift
复制代码
PS:Node直接调用RPC确实很麻烦,不同的微服务器接口要生成一份,导致Node会引入很多Thrift IDL文件。我们看看通过命令行工具生成IDL之后,Node怎么调用一个RPC接口:
const Calculator = require('./gen-nodejs/Calculator');
const ttypes = require('./gen-nodejs/tutorial_types');
const assert = require('assert');
const transport = thrift.TBufferedTransport;
const protocol = thrift.TBinaryProtocol;
const connection = thrift.createConnection("localhost", 9090, {
transport : transport,
protocol : protocol
});
connection.on('error', function(err) {
assert(false, err);
});
// Create a Calculator client with the connection
const client = thrift.createClient(Calculator, connection);
client.ping(function(err, response) {
console.log('ping()');
});
client.add(1,1, function(err, response) {
console.log("1+1=" + response);
});
work = new ttypes.Work();
work.op = ttypes.Operation.DIVIDE;
work.num1 = 1;
work.num2 = 0;
client.calculate(1, work, function(err, message) {
if (err) {
console.log("InvalidOperation " + err);
} else {
console.log('Whoa? You know how to divide by zero?');
}
});
复制代码
可以看出,通过thrift调用一个rpc接口,不管是接口定义还是传参方式,都比直接调用http借口复杂,这也是使Node作为BFF层的一个难点。
Node BFF不止提供API
上面我们只是以提供api为例,讲解了Node如何调用微服务RPC接口,但是实际项目中Node BFF可以提供更多的服务,包括路由、网关、渲染、SSR等,而我们在真实的项目中也是这样改造的。
我们在项目中使用的BFF模型:
BFF给前端提供的能力:
- 插件
- 中间件
- 路由
- 渲染
其中Node框架可以任意选择,包括Express、Koa、Eggjs、Nestjs等,我们选择了Koa,然后基于Koa做了上层封装。
插件
提供了多种内置插件,包括logger,http client,rpc client等,也可以通过配置加载第三方插件
中间件
Gatway实现方式之一,我们通过中间件实现了sso,限流,熔断等
路由
通过路由,给不同Appcation提供了不同形式的API,不管是H5,PC,小程序还是Open Api
渲染
这层可以直接渲染Client端构建生成的index,也可以实现SSR
总结
在我们的实际项目中,可能没有直接读写DB,但是在某些内部项目中,可以通过Node读写DB,而不需要服务器端人员介入。而且Node接入缓存中间件Redis、Memcache等也是可以的,甚至也能使用消息中间件MQ。如果想要BFF层变得更灵活,易于维护,我感觉使用GraphQL作为网关层更优。