概述
本篇为学习《大前端全栈实践》(抖音:哲玄前端) 里程碑-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 的浅显理解,不免出现错误理解之处和待优化点。
466

被折叠的 条评论
为什么被折叠?



