基于 Koa 整理 BFF 内核的设计思路

做后台项目时,前端页面经常要和多个接口打交道。有的接口返回数据不能直接用于展示,有的页面需要组合几份数据。如果这些处理都散在页面里,后面维护起来会比较麻烦。

这里整理一下 elpis-core 里的 BFF 内核思路:基于 Koa 启动服务,按照目录约定加载配置、服务、控制器、中间件和路由,把一些通用的接入逻辑收在一起。

BFF 放在哪里

BFF 可以放在前端页面和后端服务之间,根据页面需要做接口聚合、数据转换或接口适配。它不是所有项目都必须有,也不意味着每一个 BFF 都实现了这些能力,要看实际业务需要。

一个请求大致会经过下面几层:

1
前端页面 → 路由和中间件 → Controller → Service → 数据库或其他服务

路由负责把请求分发到对应处理方法;Controller 处理请求和响应;Service 放可复用的业务或数据访问逻辑。鉴权、日志、错误处理等通用逻辑则可以放在中间件里。

目录怎么划分

1
2
3
4
5
6
7
8
app/
├── router-schema/ # 请求参数规则
├── router/ # 路由
├── middleware/ # 中间件
├── controller/ # 请求处理
├── service/ # 服务逻辑
├── config/ # 配置
└── extend/ # 扩展

目录约定的好处是查问题时比较容易定位。比如要看某个接口的入参,就可以先找对应的 router-schema;想知道请求最终做了什么,再往 controller 和 service 里看。

启动和加载

核心启动流程可以拆成几步:创建 Koa 实例,设置工作目录和环境,再依次加载配置、扩展、服务、控制器、参数规则、中间件和路由,最后监听端口。下面是简化后的流程示意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function start(options = {}) {
const app = new Koa()
app.options = options
app.baseDir = options.baseDir || process.cwd()

loadConfig(app)
loadExtend(app)
loadService(app)
loadController(app)
loadRouterSchema(app)
loadMiddleware(app)
registerGlobalMiddleware(app)
loadRouter(app)

app.listen(8080, '127.0.0.1')
return app
}

这里的 loadConfig 等函数名只是为了说明流程,实际代码会拆到不同的 loader 文件中。加载顺序也有关系:例如路由要引用 controller,通常就需要先完成 controller 的加载。

Koa 的中间件执行顺序也值得注意:请求按注册顺序进入,遇到 await next() 后继续往下,后续中间件执行完再返回。因此错误捕获、日志和鉴权放在哪里,会直接影响它们能处理到什么。

一些需要注意的地方

目录自动加载能减少重复注册,但也增加了对命名和加载顺序的依赖。文件命名不符合约定、模块导出不符合预期,或者依赖的模块还没加载,都可能让问题变得不容易排查。启动日志和明确的错误提示就很有用了。

另外,BFF 有接口聚合和适配的空间,不代表所有接口都已经聚合了多个后端。实际介绍项目时,还是要结合具体接口的实现来讲。