我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!
背景
在前端领域的快速发展状态下,前端的开发早已从MVC的传统模式转变为现如今的前后端分离架构。
MVC的开发模式中,前后端开发过分耦合,导致开发效率低下,分工不均。出于解耦的目的,提出了前后端分离的架构。前端通过AJAX调用后端接口进行交互,实现前后端项目分离。
随着前端领域的不断扩大,后端服务在复杂的前端业务背景下,为兼容不同的业务逻辑变得臃肿而难以维护。
在这样的背景下,后端微服务架构逐渐成熟,领域之间的解耦也成为后端服务的主流。然而,前端需要自身去实现数据的聚合、裁剪等功能。虽然我们可以通过前端去请求不同的服务然后做数据的操作,但是由于不同的生态下,会存在一定的限制,例如微信小程序的域名数量限制。并且,服务的底层协议也会存在一定的限制,如RPC协议。为此,BFF中间层架构是一个比较不错的选择。
BFF(Back-end for Front-end) - 服务于前端的后端。
BFF能做什么
既然BFF架构相对于基本的前后端分离能解决一些复杂的业务问题,但是具体能做什么?
构建SSR服务解决C端网站SEO和白屏问题
当我们需要开发一个C端项目(面向用户)的时候,页面的快速响应和搜索引擎优化(SEO) 是我们必须着手去解决的问题。但是SPA页面的特性会导致页面一定的白屏时间。SPA页面下源码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
/>
<title><%= VUE_APP_TITLE %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>
当用户打开页面时,页面展示白屏,通过AJAX请求后端服务加载数据后构建DOM,展示页面。
同时,对于爬虫来说,获取的内容也仅仅是模版文件的内容。
解决
在BFF中间层中,我们可以用SSR(服务端渲染) 的方式去解决以上问题。具体实现参考官方文档
import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const server = express()
server.get('/', (req, res) => {
const app = createSSRApp({
data: () => ({ count: 1 }),
template: ``
})
renderToString(app).then((html) => {
res.send(`
<template></template>
`)
})
})
server.listen(3000)
在这个过程中,就不会存在等待AJAX请求的时间。同时,渲染的DOM结构是完整的HTML而不是仅是模版的HTML。
应对不同端对同一API的不同需求
我们可能存在这样的需求:应对不同的客户端开放不同的权限和功能,如小程序端仅支持阅读而不能添加评论,如果需要评论信息,只能在APP端进行。
面对这样的需求,微服务架构下的BE(后端开发)可能不会为了这样的功能单独实现。所以我们可以通过BFF层实现自己的业务逻辑判断,实现服务自治。
主要目的是对不同的客户端进行特定的业务处理;同时,集中处理统一逻辑,降低开发成本。
聚合不同服务API的数据
我们经常会遇到一个页面的初始化需要多个接口的数据,甚至是不同服务的数据。比如用户服务、订单服务等。
通常,这些服务以依赖的关系去请求,如订单服务需要依赖用户的信息。
使用BFF架构去优化网站:减少请求数,这样就可以在客户端只发送一个请求,由BFF层去做一些数据的融合和多服务请求的操作。
同时,在数据BFF返回数据的过程中,我们可以移除部分不需要的冗余数据。
定制自己的登陆和权限控制方案
最近在做一个B端的项目,项目有这样一个需求:根据不同的角色,实现接口访问控制。关于登陆的实现可参考之前的文章关于如何基于Google OAuth2.0 搭建系统鉴权
但是,对于服务提供方来说,我只是提供服务的,并不会对权限进行控制。所以,需要实现这个需求,只能自身从BFF层考虑。
实现
整体实现基于NestJS+Typescript。
定义一个装饰器
实现Nest自定义的装饰,用于Controller层队接口进行校验拦截。
import { SetMetadata } from '@nestjs/common'
export const Permissions = (...permissions: string[]) => SetMetadata('permissions', permissions)
实现守卫
守卫的目的就是实现拦截的手段。
async canActivate(context: ExecutionContext): Promise<boolean> {
const roles: string[] = this.reflector.get('permissions', context.getHandler())
if (!roles) {
return true
}
// 获取请求Req
const req = context.switchToHttp().getRequest<any>()
const token = req.headers['Authorization'] || req.headers['authorization']
if (!token) {
throw new UnauthorizedException()
}
// 业务代码
//
//
//
return this.hasPermission(permissions || [], roles)
}
}
控制层使用
@Post()
@Permissions('0101')
async getBizData(@Body() data: QueryDto) {
return this.myService.getBis(data)
}
BFF所带来的问题
当一个架构能从一个方面带来了优势的同时,必定也会牺牲其他方面
资源问题
当BFF层多了,资源使用就成了问题,毕竟会多一个服务。尤其是使用SSR服务端渲染的时候,随着QPS(每秒请求数,就是说服务器在一秒的时间内处理了多少个请求。)的波动,CPU的使用和内存总是会有比较高的波动。不过这方面考虑使用服务提供商的伸缩性服务。
维护问题
随着BFF层的使用,前端也不仅仅只是需要关注浏览器端的知识点。同时也需要掌握后端服务的知识,如数据库、服务器、并发问题等等。会使得前端的同学维护起来较为困难。
但是,也建议大家跳出自己的舒适圈。
服务链路问题
在引入BFF层之前,可能只是单纯的服务调用。服务之间的依赖在前端比较清晰。
在使用BFF层架构之后,流程变得繁琐,要同时走前端、服务端的研发流程,多端发布、互相依赖,导致流程繁琐。出现问题的时候,需要清晰的链路追踪,这里一定要将日志的链路完善好,避免造成问题排查的困难。
结束语
2022.02.17
木更