零散专题28 BFF调研2

BFF调研报告第二部分

为什么说Node作为中间层带来了真正的前后端分离

前后端未分离时是怎么做的?

典型的MVC框架,使用一门后端语言、框架,这里以Egg.js为例子,定义路由:

1
2
3
4
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
};

当访问/时,会到Controller层从处理业务逻辑的Service层(或者叫Modal层)拿到数据,渲染一个模板(View)并且将数据嵌入进去

1
2
3
4
5
6
7
8
9
10
11
class HomeController extends Controller {
async index() {
const { ctx } = this;
// 从Service层获取数据
const { title } = await ctx.service.news.list();
// 渲染模版
await ctx.render('index.tpl', { title });
}
}

module.exports = HomeController;

模板使用了nunjucks引擎,在index.tpl

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
</head>
<body>
<h1>Hi Egg</h1>
<a href="news">Movie</a>
</body>
</html>

这种前后端没有分离的开发方式,主要指的是前后端传递数据的方式是通过后端渲染模板时将数据传递给模板,前端开发的只是模板的样式,被动的从模板拿到数据。也就是说大部分的逻辑、数据获取、处理都是在后端完成的。

前后端开始分离

为了脱离前端强依赖后端的关系,将数据层的接口分离出来,以Ajax的形式进行交互,服务端只负责渲染逻辑、提供数据和接口,不负责数据填充。

页面的数据以JSON格式由前端通过Ajax获取,填充到页面中。具体的做法是,在路由中添加一个接口:

1
router.get('/getTitle', controller.home.getTitle);

在Controller中添加对应的处理逻辑,返回JSON数据格式

1
2
3
4
5
6
7
async getTitle() {
const { ctx } = this;
ctx.body = {
title: 'OK',
};
ctx.status = 200;
}

在模板中通过<script>脚本访问接口,获得数据后填充到DOM中

这个时候前后端的界限是按照浏览器和服务器的划分,但是也有一些问题

  1. 环境:本地开发需要起后端环境,影响开发效率(当然可以通过Mock数据解决)
  2. 联调:前后端关注点不同,联调时效率低下
  3. 接口:接口不好维护,接口变更、新加时需要沟通成本

前后端分离新阶段:SPA

为了提高效率和用户体验,前端在前端开始了MVVM的单页面开发,后端不再负责渲染模板,只单纯的提供数据接口,后端的模板成为了前端SPA的容器。

前端也有了自己的路由、Model、View。

在SPA之前,浏览器接受的是服务器渲染好的HTML字符串和少许的JS/CSS脚本,路由由后端控制;到了SPA之后,服务器渲染好的只是一个空壳子,浏览器接受这个空壳子和大量的JS/CSS脚本,由浏览器渲染为HTML字符串,页面跳转由前端路由控制

前端路由一般是通过Hash模式实现,只只要后台服务器配合一个根路由对应基础模板,其他的跳转都是通过前端路由改变#后面的内容实现,#值的改变并不会想浏览器发送请求,所以SPA的url一般是http://demo.com/#index

Vue-router也提供了History模式,利用了HTML5的history.pushState来实现URL跳转而无需重新加载页面,这样的URL就像正常的URL,例如http://demo.com/index,但是这需要后台配置支持,增加一个覆盖所有情况的候选资源,当URL匹配不到任何资源,应该返回同一个index.html页面,也就是app依赖的页面。这个时候服务端不再返回404,需要通过前端路由对不匹配的资源返回404

前面提到的Mock数据的问题,本地Mock是一种解决方案,但添加、维护、同步都非常麻烦。现在比较成熟的解决方案可以使用easy-mock,它可以很好地支持Swagger,在开发时在前端不再通过后台服务器获取数据,也不在本地Mock,而是添加一个Mock服务器,直接通同步服务端的接口,产出Mock数据。

Swagger是一款API文档工具,官网在这里

真正的前后端分离:Node.js中间层服务

现在的架构下,前后端的职能更加清晰:

后端 前端
提供数据 接收数据
处理业务逻辑 处理渲染逻辑
Server-side MVC Client-side MVVM
代码跑在服务器上 代码跑在浏览器上

但是服务端和客户端的部分职责重叠,很难统一具体要做的事。在SPA应用上也暴露了一些性能的问题

  1. 渲染、取值都在客户端执行,有性能的隐患
  2. 首屏渲染有白屏、闪烁的问题
  3. 无法SEO
  4. 对移动设备网速较低时体验较差

为了解决这个问题,可以引入Node作为中间层,从工作职能的角度上重新定义前后端的范畴

v2-54f37a6ef6b8c907b63cc2515de78ad6_hd.jpg

引入了之后前后端的职能重新划分为:

v2-d64e794fd62d774e4122ab4cd759fa09_hd.jpg

Node中间层,也就是BFF,常见的业务场景

  1. 接口数据二次处理,在中间层做接口转发
  2. 页面性能,将首屏渲染交给中间层去做,次屏渲染依然走之前的浏览器渲染
  3. 使用Bigpiper技术,合并请求,降低负担,分批输出,提高体验。

淘宝前端基于Node.js的前后端分离的思考与实践

PPT在这里

SPA式的前后端分离,是从物理层做区分(认为只要是客户端就是前端,服务器端就是后端),这种分法已经无法满足前后端分离的需求,从职责上划分更加满足使用场景:

  • 前端:负责 View 和 Controller 层。
  • 后端:只负责 Model 层,业务处理/数据等。

为什么要前后端分离?

  1. 现有开发模式的适用场景:后端为主的MVC,做一些同步展现的业务效率很高,但是遇到同步异步结合的页面,沟通成本高,Ajax为主的SPA型开发模式,比较适合开发APP类型的场景,但是有SEO的问题
  2. 分清前后端职责,代码分别为虎
  3. 开发效率
  4. 前端发挥的局限

前后端分离,需要Web服务实现Controller层的功能,就是Node.js中间层

TB10.2tPVXXXXbUXXXXXXXXXXXX-590-611.png

这样的架构会引出一些问题:

(1)SPA中,后端已经提供了所需的数据接口,View前端也可以控制,为什么多加Node.js这一层?

让前端控制Controller层,Node对于前端来说是很高效率的选择

(2)多加一层性能怎么样?

分层就涉及每层之间的通讯,肯定有一定的性能损耗,但是合理的分层能让职责清晰,也方便协作,会大大提高开发效率。分层的损失可以在其他方面弥补回来。

另外,如果决定分层,可以通过优化通讯方式、通信协议,把性能损耗降到最低。

例如可以在Node层合并请求,利用Bigpipe优化数据传输。淘宝这方面的实践已经很多。

(3)多加一层,前端的工作量是不是增加了?

增加是肯定的,但是可以减少联调、沟通的时间,效率会提升

(4)多加一层的风险怎么解决?

随着Node.js大规模使用,系统/运维/安全部门的同学也一定会加入到基础建设中,共同晚上各个环节出现的问题,保证系统稳定属性

淘宝基于Node.js的前后端分离架构如下所示:

  • 最上端是服务端,提供各种接口,因为有了Node,所以也不局限是什么形式的服务。对于后端来说,只需要关心业务代码的接口实现
  • 服务端下面是Node.js中间层,中间层有一层Model Proxy与服务端进行通讯,这一层主要是抹平对不同接口的调用方式,封装一些View层需要的model。Node层使用的框架有开发者自己决定,输出方式也是完全由场景决定的
  • 浏览器层在架构中没有变化

淘宝前端团队基于前后端分离的模版探索

在传统的开发模式中,模板是前、后端中间的模糊地带,最容易与后端开发纠缠不清

浏览器端渲染的好处

  • 摆脱业务逻辑与呈现逻辑在Java模版引擎中的耦合与混乱。
  • 针对多终端应用,更容易以接口化的形式。在浏览器端搭配不同的模版,呈现不同的应用。
  • 脱离对于后端开发、发佈流程的依赖。
  • 方便联调。

浏览器端渲染造成的坏处:

  • 模版分离在不同的库。有的模版放在服务端(JAVA),而有的放在浏览器端(JS)。前后端模版语言不相通。
  • 需要等待所有模版与组件在浏览器端载入完成后才能开始渲染,无法即开即看。
  • 首次进入会有白屏等待渲染的时间,不利于用户体验
  • 开发单页面应用时,前端Route与服务器端Route不匹配,处理起来很麻烦。
  • 重要内容都在前端组装,不利于SEO

引入了Node中间层后,前端可以自由选择模板是在服务端(Node)还是在浏览器端做渲染

淘宝的模板共享实践经验:

(1)复杂交互应用(如购物车、下单页面)

  • 状况:全部的HTML都是在前端渲染完成,服务端仅提供接口。
  • 问题:进入页面时,会有短暂白屏。
  • 解答:首次进入页面,在Node层进行全页渲染,并下载相关的模版。后续交互操作,在浏览器端完成局部刷新

(2)SPA页面

  • 状况:使用Client-side MVC框架,在浏览器换页。
  • 问题:渲染与换页都在浏览器端完成,直接输入网址进入或F5刷新时,无法直接呈现同样的内容。
  • 解答:在浏览器端与Node层共享同样的Route设定,浏览器端换页时,在浏览器端进行Route变更与页面内容渲染,直接输入同样的网址时,在Node.js端进行“页面框架 + 页面内容”渲染(如何做?)

各种开发框架的对比

Middelway

淘宝前端团队推出的Web全栈应用开发框架,文档在这里

使用了Egg.js作为Web层容器,可以使用Egg.js的插件,它的特点是

  1. 使用IoC机制将应用依赖管理起来,最大程度帮助应用在Web开发中提升可维护行和可扩展性(IoC模块可独立使用)

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
更多的看维基百科这篇文章

  1. 使用TypeScript进行应用开发
  2. 兼容Koa社区和Egg.js插件

一些细节也可以通过这篇文章了解。

Egg.js:蚂蚁金服死马:企业级Node.js Web框架研发与落地

这是视频这是PPT

Chair BFF是基于Egg.js的研发框架,在Egg.js基础上,添加了很多定制话的插件,内部集成了很多蚂蚁内部的服务,即便开源也无法使用

但是包括Egg.js在内,与业务无关的插件,蚂蚁都已经开源了。正确的做法是基于Egg.js进行BFF的引入,充分利用Egg的社区生态、插件,随着业务开发编写定制化的插件和维护。当积累到一定程度后,可以形成适合于自己公司的BFF研发框架(Mi BFF),当上下游研发流程打通后,形成属于自己的解决方案(Mi-TWA)

Egg.js有如下的优点:

  • 蚂蚁金服的最佳实践的封装(通过插件的形式,已开源)
  • 文档友好
  • 更适合企业级开发:灵活定制,遵循约定
  • npm上插件众多,配套完善
  • 可以使用Koa的中间件
  • 有配套的研发平台的支持

社区模式VS企业模式:个人项目使用Koa和Egg区别不大,但是对企业内部,使用Egg的正确姿势是基于Egg封装自己的东西。

蚂蚁金服有了很多年的积累,才形成了Chair BFF和TWA这样的一站式研发平台和配套设施,对于新引入BFF的团队而言,可以做到渐进增强的研发体验:

(1)首先实现,使用Node.js实现功能,保证代码质量

(2)在使用Node实现功能之后,很容易被人挑战,那么就需要一步步优化,比如统一编码规范,让团队可以更好的写代码

(3)提升稳定性、可维护性,对接企业内部服务

(4)通过完善的研发流程平台,提供更好的变成体验

(5)让产品工程师可以专注于产品

Egg的缺点:为了保证框架利用的最大化、代码的可维护性,需要学习、遵守框架的大量的约定

egg-vue-webpack-boilerplate

基于Egg + Vue + Webpack多页面和单页面服务端渲染同构工程骨架项目。需要配套使用基于Webpack封装的easywebpack-cli创建项目,可以创建各种项目的模板:

1
2
3
4
5
6
7
Create Vue Application 
Create React Application
Create Egg + Vue Application
Create Egg + React Application
Create Weex Application
Create HTML Application
Create NPM package Application

选择Egg + Vue项目,可以选择多种渲染方式:

1
2
3
4
5
6
Create Egg + Element Admin Application 
Create Egg + Vue Single Page SSR Application
Create Egg + Vue Multil Page SSR Application
Create Egg + Vue Single And Multil Page SSR Application
Create Egg + Vue + TypeScript SSR Application
Create Ves Framework - Node Vue TypeScript Isomorphic Framework Application

对于使用Egg+Vue/React实现服务端渲染,可以使用这个解决方案

nodejs-backend-for-frontend

IBM的BFF解决方案,配合使用Express,内置了IMB的云开发工具,针对IBM的云服务进行了一些特殊化定制,Github的star数16,提供了对Swagger的支持,自动生成Mock数据

使用Koa或者Express自行搭建中间层

使用Koa或者Express,自行搭建一个中间层,实现接口的转发、数据处理等比较基础的功能。

作为BFF引入时,可以将前端工程代码和BFF层代码分为两个仓库管理,也可以在一个仓库管理。

小米笔记本BFF项目部署

这个部分只是BFF项目使用Docker的部署过程,不作为引入方案,只作为部署经验进行介绍。

Node做中间层,做接口的转发服务,功能比较浅,重业务的逻辑也放到了后端。

接口转发、数据处理、Mock服务(Swagger)

好处:

  1. 更好的前后端分离
  2. 不需要考虑跨域
  3. 接口的聚合不再需要服务端的支持(减少沟通成本)
  4. 能自主完成一些小的功能开发

缺点

  1. 需要独立完成一套功能的实现
  2. 增加维护成本

基于Ocean平台使用Docker容器技术进行部署,具体代码参考Wiki

总结

框架 | 维护团队 | Star | 优点 | 缺点
—|—|—|—|—|—
Egg.js | 阿里 | 12404 | 1. 蚂蚁金服的最佳实践;
2. 文档友好;
3. 适合企业级开发;
4. 插件众多,配套完善;
5. 且可以使用Koa的中间件;
6. 有配套的研发平台的支持 | 1. 需要学习、遵守框架的大量的约定
Middleway | 淘宝 | 713 | 1. 淘宝的最佳实践;
2. 使用IoC机制管理应用依赖;
3. 使用TypeScript进行应用开发;
4. 兼容Koa社区和Egg.js插件 | 1. 使用不广泛;
2. 有些概念比较复杂,暂无引入必要;
egg-vue-webpack-boilerplate | easy-team | 881 | 1. 有Vue和React等一系列工程解决方案;
2. 可以扩展多页面和单页面服务端渲染 | 1. 使用不广泛;
2. 可定制性比较差
nodejs-backend-for-frontend | IBM | 16 | 1. 支持Swagger,自动生成Mock数据 | 1. 使用不广泛;
2. 内置了IBM云服务相关的功能
Express | Express | 43507 | 1. 应用广泛;
2. 社区繁荣;
3. 使用灵活;| 1. 需要自搭建项目;
2. 需要自行选择中间件;
3. 灵活性过高,导致后期可能维护困难
Koa | Koa | 25884 | 1. 异步流程控制优于Express;
2. 社区繁荣;
3. 使用灵活;| 1. 需要自搭建项目;
2. 需要自行选择中间件;
3. 灵活性过高,导致后期可能维护困难

落地方案

总体建议

一开始引入Node作为BFF层,在一些小的项目上进行改造,通过HTTP请求从后端服务获取数据,实现一些接口转发、数据处理之类比较简单的功能,将项目中所有接口都改造为接入BFF Server。

通过简单的项目,把代码管理、开发、Review,测试、部署、维护全流程(或大部分流程)走通后,逐渐扩展边界和功能(接口文档和接口的对应、自动生成Mock数据、性能监控等)

待确定的规范和方案

主要流程中要确定的一些代码管理规范和方案:

1 代码管理

(1)前端后端代码放到一个代码仓库里面统一管理。(可能需要处理依赖混在一起的问题)

  1. 前后端代码分离两个目录

  2. 以前端代码目录为主,添加服务端代码文件夹

  3. 以前端代码目录为主,添加前端代码文件夹

(2)前端后端代码放到两个代码仓库里面分别管理。

2 框架选择

  1. 自行搭建(前端项目+Koa/Express/Egg),demo地址在这里
  2. Egg.js(完全使用)
  3. Esay-Team egg-vue-webpack-boilerplate
  4. IBM的方案IBM/nodejs-backend-for-frontend

3 本地开发

  1. 前端在一个端口,起一个服务,负责渲染页面,Node服务在另外一个端口,其一个服务,负责提供接口和数据;二者处于开发模式热更新都是各自的构建工具实现的
  2. 前端、后端在同一个端口开发(使用egg-view-assets或者Esay-Team egg-vue-webpack-boilerplate)

4 构建流程

  1. 多页面/单页面服务端渲染
  2. 前端渲染
  3. 静态页面

前两种方式需要结合Vue-SSR或者React-SSR,最后一种方式只需要配置对应的构建目录,通过静态资源的方式构建

如果当前阶段只是实现接口转发的功能可以涉及不到第一步骤。

5 部署

  1. 前端打包后,手动上传到CDN,后端服务打包后为tar,手动上传至服务器,开启服务
  2. 前端打包后,手动上传到CDN,后端服务构建一个Docker镜像,推到服务器(或者结合平台)开启服务

参考