基于 Koa2 的 BFF 层框架实现

概述

本篇为学习《大前端全栈实践》(抖音:哲玄前端) 里程碑-1后的学习笔记。下面将简单介绍该实践中基于 Koa2 的 BFF 层框架 elpis-core 实现过程。

BFF

BFF“Backend for Frontend”,直译为 “面向前端的后端” ,是一种在现代软件开发架构中被广泛应用的模式,用于解决前端与后端交互过程中的特定问题。BFF层主要可以再细分为接入层业务层以及服务层,下面是简单的层级关系。

  • BFF
    • 接入层
      • router路由分发
      • router-schema路由规则
      • middleware中间件
      • 整体作用:接入层在系统架构中起入口与调控作用。通过 router 实现请求按 URL 和方法精准路由分发;依 router - schema 规则验证请求,筛除非合规请求;借 middleware 执行通用操作,如鉴权、日志记录。它适配多客户端,隐藏内部细节,提供统一接口,增强通用性与灵活性,保障系统稳定运行 。
    • 业务层
      • controller处理器
      • env环境分发
      • config提取
      • extend服务拓展
      • schedule定时任务
      • 整体作用:业务层处于整个架构的中间位置,起到承上启下的作用。它从接入层接收请求,调用服务层的原子级方法,按照业务规则进行数据处理和逻辑组装,最终返回处理结果给接入层。通过这种方式,实现了具体的业务功能,同时也对服务层进行了业务上的编排和调用。
    • 服务层
      • service处理器
      • 整体作用:服务层为业务层提供了基础的操作接口。它将数据层的操作进行封装,使得业务层无需关心具体的数据存储和获取细节,只需要调用服务层提供的方法即可。这种分层设计有助于提高代码的模块化程度,当数据层的实现方式发生变化(如从一种数据库切换到另一种数据库)时,只需要在服务层进行修改,而不会影响到业务层和接入层的代码。同时,服务层的原子级方法可以在不同的业务场景中复用,提高了代码的复用性。

BFF在现代开发架构中扮演着举足轻重的角色,具有多方面关键作用与重要意义:

  • 优化请求处理流程:承担接口转发任务,并非简单转送,还整合接口合并功能,减少前端请求次数,优化数据获取效率。通过将 “页面 -> API” 请求转变为 “页面 -> BFF -> API”,有效解决跨域难题,利用服务端间不受同源策略限制的特性,确保数据交互顺畅。同时,收拢对外请求接口,把原有的(外网 -> 内网)*n模式转变为 外网 -> (BFF 层 -> 内网)*n,极大缩短内外网访问时间,并突破浏览器对同一域名下请求的并发上限,提升整体性能。
  • 实现前后端深度解耦:解耦后端与前端展示逻辑,后端仅聚焦接口业务逻辑实现,由 BFF 层负责获取数据并按需组装,再传递给前端用于展示。这种分工模式让前后端开发更加独立,提升开发效率与代码维护性。
  • 强化安全保障措施:将原本暴露于前端的签名、密钥置于 BFF 层进行管理,避免前端直接暴露敏感信息,有效防止被盗取,为服务端 API 筑牢安全防线。
  • 拓展前端应用能力:BFF 层具备 SSR(服务器端渲染)能力,为前端在性能优化、安全策略制定、业务功能拓展等多方面创造更多可能性,赋予前端开发更大的灵活性与创新空间,使前端应用能更好地满足多样化的业务需求。
    总之,BFF从性能、安全、开发模式等多个维度对系统进行优化与升级,有力推动前后端高效协作,提升整体架构的稳健性与可扩展性。

BFF层实现

elpis-core依靠其中的一系列loader旨在将项目文件中的各层实现文件加载解析并挂载到运行时的全局Koa实例app上,以供系统运行
在这里插入图片描述
应用目录结构

Elpis
	|-- app
	|-- |-- controller
	|-- |-- extend
	|-- |-- middleware
	|-- |-- pages
	|-- |-- public
	|-- |-- router
	|-- |-- router-schema
	|-- |-- service
	|-- |-- view
	|-- |-- webpack
	|-- |-- middleware.js
	|-- elpis-core
	|-- |-- loader
	|-- |-- |-- config.js
	|-- |-- |-- controller.js
	|-- |-- |-- extend.js
	|-- |-- |-- middleware.js
	|-- |-- |-- router-schema.js
	|-- |-- |-- router.js
	|-- |-- |-- service.js
	|-- |-- env.js
	|-- |-- index.js
	|-- config
	|-- logs
	|-- node_modules
	|-- .eslingignore
	|-- .eslintrc
	|-- .gitignore
	|-- index.js
	|-- package.json

接入层

router

负责根据客户端请求的 URL,将请求引导到对应的处理程序。就像是一个交通枢纽,根据目的地(URL)来决定把请求 “输送” 到哪里。
routerLoader 核心代码:

// 找到路由文件路径
const routerPath = path.resolve(app.businessPath, `.${sep}router`)
// 实例化 KoaRouter
const router = new KoaRouter()
// 注册所有路由
const fileList = glob.sync(path.resolve(routerPath, `.${sep}**${sep}**.js`))
fileList.forEach(file => {
  require(path.resolve(file))(app, router)
})
// 路由兜底 (健壮性)
router.get('*', async (ctx, next) => {
  ctx.status = 302 // 临时重定向
  ctx.redirect(`${app?.options?.homePage ?? '/'}`)
})
router-schema

定义了如何匹配 URL 以及请求方法(如 GET、POST、PUT、DELETE 等)。这些规则就像是一套 “交通规则”,明确了什么样的请求能被什么样的处理程序接收。
routerSchemaLoader核心代码:

// 读取 app/router-schema/**.js 下的所有文件
const routerSchemaPath = path.resolve(app.businessPath, `.${sep}router-schema`)
const fileList = glob.sync(path.resolve(routerSchemaPath, `.${sep}**${sep}**.js`))
// 注册所有 routerSchema,使其可以如下访问 app.routerSchema
let routerSchema = {}
fileList.forEach(file => {
  routerSchema = {
    ...routerSchema,
    ...require(path.resolve(file))
  }
})
middleware

在请求到达最终处理程序之前或之后,执行一些通用的操作。比如身份验证、日志记录、请求体解析等。以身份验证为例,中间件可以检查请求中的令牌,验证用户是否有权限访问请求的资源。如果没有权限,中间件可以直接返回错误响应,而不会让请求继续到达处理程序。
middlewareLoader核心代码:

// 读取 app/middleware/**/**.js 下的所有文件
const middlewarePath = path.resolve(app.businessPath, `.${sep}middleware`)
const fileList = glob.sync(path.resolve(middlewarePath, `.${sep}**${sep}**.js`))
// 遍历所有文件目录, 把内容加载到 app.middlewares 下
const middlewares = {}
fileList.forEach(file => {
  // 提取文件名称
  let name = path.resolve(file)
  // 截取文件路径
  // 例如 app/middleware/custom-module/custom-middleware.js => custom-module/custom-middleware
  name = name.substring(
    name.lastIndexOf(`middleware${sep}`) + `middleware${sep}`.length,
    name.lastIndexOf('.')
  )
  // 把 '-' 统一改为驼峰式
  // 例如: custom-module/custom-middleware.js => customModule.customMiddleware
  name = name.replace(/[-_][a-z]/ig, s => s.substring(1).toUpperCase())
  // 挂载 middleware 到 app 对象中
  let tempMiddleware = middlewares
  const nameArr = name.split(sep)
  for (let i = 0, len = nameArr.length; i < len; i++) {
    if (i === len - 1) {
      tempMiddleware[nameArr[i]] = require(path.resolve(file))(app)
    } else {
      if (!tempMiddleware[nameArr[i]]) {
        tempMiddleware[nameArr[i]] = {}
      }
      tempMiddleware = tempMiddleware[nameArr[i]]
    }
  }
})

业务层

controller

接收来自接入层的请求,调用服务层的方法,并对返回的数据进行处理和组装,以完成具体的业务逻辑。例如,在一个商品下单的业务场景中,controller可能会调用服务层获取商品库存、用户信息等方法,然后根据这些信息进行库存检查、计算总价等业务逻辑操作,最后返回给接入层一个合适的响应。
controllerLoader核心代码:

const tempController = controller
const nameArr = name.split(sep)
for (let i = 0, len = nameArr.length; i < len; i++) {
  if (i === len - 1) {
    const ControllerModule = require(path.resolve(file))(app)
    tempController[nameArr[i]] = new ControllerModule()
  } else {
    if (!tempController[nameArr[i]]) {
      tempController[nameArr[i]] = []
    }
    tempController = tempController[nameArr[i]]
  }
}

注意:Controller 被设计为类的形式,将有利于后期通过继承基类Controller统一收拢公共处理方法(之后的service与之相同)。

env

根据不同的运行环境(如开发、测试、生产),加载不同的配置和执行不同的逻辑。在开发环境中,可能会启用更详细的日志记录和调试功能,方便开发人员排查问题;而在生产环境中,则更注重性能和稳定性,可能会禁用一些调试功能。

config

从配置文件或配置中心提取应用程序运行所需的各种配置信息,如数据库连接字符串、第三方服务的 API 密钥等。这样,当这些配置发生变化时,只需要修改配置文件,而不需要修改代码。
configLoader核心代码:

 // 找到 config 目录
 const configPath = path.resolve(app.baseDir, `.${sep}config`)

 // 获取 deafalut.config
 let defaultConfig = {}
 try {
   defaultConfig = require(path.resolve(configPath, `.${sep}config.default.js`))
 } catch (e) {
   console.error("[exception] there is no default.config file")
 }
 // 获取 env.config
 let envConfig = {}
 try {
   if (app.env.isLocal()) { // 本地环境
     envConfig = require(path.resolve(configPath, `.${sep}config.local.js`))
   } else if (app.env.isBeta()) { // 测试环境
     envConfig = require(path.resolve(configPath, `.${sep}config.beta.js`))
   } else if (app.env.isProduction()) { // 生产环境
     envConfig = require(path.resolve(configPath, `.${sep}config.prod.js`))
   }
 } catch (e) {
   console.error(`[exception] there is no env.config file`)
 }
 // 覆盖并加载 config 配置
 app.config = Object.assign({}, defaultConfig, envConfig)
extend

为应用程序添加额外的功能或对现有功能进行增强。比如,通过扩展可以添加新的业务逻辑模块,或者对已有的服务进行功能升级,而不影响其他核心业务逻辑。
extendLoader核心代码:

// 读取 app/extend/**.js 下的所有文件
const extendPath = path.resolve(app.businessPath, `.${sep}extend`)
const fileList = glob.sync(path.resolve(extendPath, `.${sep}**${sep}**.js`))
// 遍历所有文件目录, 把内容加载到 app.extend 下
fileList.forEach(file => {
  // 提取文件名称
  let name = path.resolve(file)
  // 截取文件路径
  // 例如 app/extend/custom-extend.js => custom-extend
  name = name.substring(
    name.lastIndexOf(`extend${sep}`) + `extend${sep}`.length,
    name.lastIndexOf('.')
  )
  // 把 '-' 统一改为驼峰式
  // 例如: custom-extend.js => customExtend
  name = name.replace(/[-_][a-z]/, s => s.substring(1).toUpperCase())
  for (const key in app) {
    if (key === name) {
      console.error(`[extend load error] name: ${name} is already in app`)
      return
    }
  }
  // 挂载 extend 到 app 对象中
  app[name] = require(path.resolve(file))(app)
})
schedule

负责执行一些定时的操作,如定时清理过期数据、定时生成报表等。例如,每天凌晨 2 点清理数据库中一周前的临时数据,通过定时任务就可以自动完成这个操作,无需人工干预。

服务层

service

包含了数个原子级方法,这些方法通常对应着对数据层的具体操作,如数据库的增删改查、调用外部 API 等。每个方法都专注于完成一个单一的功能,尽量不包含复杂的业务逻辑,以保证代码的可复用性和可维护性。例如,
serviceLoader核心代码:

const tempService = service
const nameArr = name.split(sep)
for (let i = 0, len = nameArr.length; i < len; i++) {
  if (i === len - 1) {
    const ServiceModule = require(path.resolve(file))(app)
    tempService[nameArr[i]] = new ServiceModule()
  } else {
    if (!tempService[nameArr[i]]) {
      tempService[nameArr[i]] = []
    }
    tempService = tempService[nameArr[i]]
  }
}

loader加载顺序

在学习过程中,暂时理解到的加载顺序及应该注意的点如下:

  • router 可能依赖于 controller 中的方法,应置于 controller 之后
  • controller 可能依赖于 service 中的方法,应置于 service 之后

结语

以上就是目前对 BFF层elpis-core 的浅显理解,不免出现错误理解之处和待优化点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值