BFF 架构初探
什么是BFF 以及 node.js 在BFF 层能做的事情
BFF-服务于前端的后端,如果存在多端共用同一个后端,需要根据需求返回不同的数据,这里就需要使用中间代理层来处理,NODE使用js进行编码,前端开发人员可以快速上手。
Node模板渲染
在没有Node中间层之前前段项目部署 都是 打包完生成 dist 目录交给后端或者运维部署到 nginx 等上. 现在有了 Node 中间层后,可以通过 Node API接口吐出模版页面来。这里选择使用 koa-swig 模版引擎
npm install koa-swig koa koa-static --save
启动Node服务配置koa-swig
const Koa = require('koa');
const serve = require('koa-static');
var render = require('koa-swig');
const co = require('co');
const app = new Koa();
const { port, viewDir, memoryFlag, staticDir } = require('./config')
app.use(serve(staticDir))
app.context.render = co.wrap(render({
root: viewDir,
autoescape: true,
cache: memoryFlag, // disable, set to false
ext: 'html',
writeBody: false,
varControls: ['[[', ']]']
}));
app.listen(port, () => {
});
新建views目录,并新建index.html
编写路由并返回index.html页面
const Router = require('koa-router')
const router = new Router()
router.get('/', async (ctx) => {
ctx.body = await ctx.render('index')
})
启动Node服务, 成功访问我们刚才的页面。
提示
如果启动页面发现只有一个 {},则记住需要在 render前加上 await
Node真假路由混用
如上编写了后端路由,通常前端SPA页面也有前端路由以vie-cli打包后为例。
访问node渲染了前端SPA页面,页面在进行切换的时候没问题,但是刷新页面却发现报错,这是因为没有前端SPA的路由,因此这里需要使用到真假路由。
npm install koa2-connect-history-api-fallback
如上是koa的一个中间件,用于处理vue-router使用history模式返回index.html,让koa2支持SPA应用程序。
使用方法如下:
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
// 可自行配置白名单
app.use(historyApiFallback({ whiteList: ['/api'] }));
Node错误处理
对于异常,可以分为已知异常和 未知异常
已知异常就是程序中能够知道的异常,如:客户端参数传递错误,服务端抛出的异常,客户端无权访问等这一类就是已知异常。
未知异常就是程序中不能预想的错误,最常见的服务器程序抛出状态码500的异常,如常见的程序运行错误,这种是未知错误。
中间件的异常
如下使用中间件抛出一个异常:
app.use((ctx, next) => {
throw Error('error');
ctx.msg += 'world';
next();
});
控制台得到UnhandledPromiseRejectionWarningError:error 错误
现在加上app.onerror来拦截错误
app.use((ctx, next) => {
throw Error('error');
ctx.msg += 'world';
next();
});
app.use(ctx => {
ctx.body = 'hello word';
});
app.onerror = (err) => {
console.log('捕获到了!', err.message);
}
再次运行发现onerror没有拦截到,查阅官网记载的错误处理方法Error Handling,发现违反了koa的设计,有如下两种方法处理这个问题:
- 如果不想修改成async函数,那就在所有的next()前面加return即可
- 如果是async函数,那所有的next前面加await即可
全局异常中间件
全局异常监听
编写捕获异常处理中间件catchError.js
const catchError = async (ctx, next) => {
try {
await next()
// 处理页面404 防止页面404过多 被搜索引擎降权
if (ctx.status === 404) {
ctx.body =
'<script type="text/javascript" src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js" charset="utf-8"></script>';
}
} catch (error) {
}
}
module.exports = catchError
在app.js加载中间件
全局异常中间件监听、处理因此放在所有的中间件的最前面
const catchError = require('./middlewares/exception')
const app = new Koa()
app.use(catchError)
...
app.listen(8000)
通过如下代码测试捕获异常
const Koa = require('koa');
const app = new Koa();
const catchError = async (ctx, next) => {
try {
await next()
} catch (error) {
console.log(error);
}
}
app.use(catchError)
app.use((ctx, next) => {
console.log(a);
});
app.listen(8000);
定义异常的返回结果
如上代码中a是空值会报空指针异常,这类错误就是前面所说的未知错误。
在服务器接口开发中,一个异常的返回结果通常如下:
- msg:异常信息
- code:HTTP状态码
- errorCode:自定义的异常状态码
因此这里定义HttpException:
// 定义HttpException继承Error这个类
class HttpException extends Error {
constructor(msg = '服务器异常', errorCode = 500, code = 400) {
super()
/**
* 错误信息
*/
this.msg = msg
/**
* Http 状态码
*/
this.code = code
/**
* 自定义的异常状态码
*/
this.errorCode = errorCode
}
}
改写中间件,区分是已知异常还是未知异常
const { HttpException } = require("../core/httpException")
const catchError = async (ctx, next) => {
try {
await next()
} catch (error) {
const isHttpException = error instanceof HttpException
//判断是否是已知错误
if (isHttpException) {
ctx.body = {
msg: error.msg,
errorCode: error.errorCode,
request: `${ctx.method} ${ctx.path}`
}
ctx.status = error.code
} else {
ctx.body = {
msg: '服务器出现了未知异常',
errorCode: 999,
request: `${ctx.method} ${ctx.path}`
}
ctx.status = 500
}
}
}
启动服务访问 http://localhost:8000/发现抛出 {"msg":"服务器出现了未知异常","errorCode":999,"request":"GET /"},说明自定义的已知错误被拦截到了。
定义常见的异常状态
有了如上异常的基类,定义一些常见的异常如:参数校验失败
class ParameterExceptio extends HttpException {
constructor(msg, errorCode) {
super()
this.code = 400;
this.msg = msg || '参数错误';
this.errorCode = errorCode || 400;
}
}
成功返回
class Success extends HttpException {
constructor(msg, errorCode) {
super()
this.code = 200;
this.msg = msg || '成功';
this.errorCode = errorCode || 200;
}
}
权限认证
class AuthFaild extends HttpException {
constructor() {
super()
this.code = 401;
this.msg = '认证失败';
this.errorCode = 1004;
}
}
根据业务定义异常情况
开发环境异常查看
全局异常组件区分了已知异常和未知异常,但是返回了一样的错误对于开发环境调试不友好,需要在控制台抛出异常方便定位问题。
改写package.json启动命令
"scripts": {
"start": "set NODE_ENV=dev&& nodemon --inspect app.js"
}
启动服务后根据命令参数判定是开发环境还是生产环境,只需要在开发环境中抛出错误,代码如下:
const isHttpException = error instanceof HttpException
// 开发环境下输出 异常 error
if (process.env.NODE_ENV === 'dev' && !isHttpException) {
throw error
}
...
JS type module和systemjs
使用JavaScript模块依赖于import和export,最新的浏览器开始原生支持模块功能了,浏览器能过最优化加载模块,使他比其他库更有效率:使用库通常需要做额外的客户端处理。
首先需要把script属性type=module,来声明这个脚本是一个模块:
<script type="module">
// 1.支持module 支持nomodule
import("./js/data.js").then(_ => {
console.log(_.default);
})
</script>
浏览器中可用的JavaScript模块功能的最新部分是动态模块加载。这允许仅在需要时动态加载模块,而不必预先加载所有模块,如上:
如果浏览器不支持module,支持nomodule如下:
<script type='nomodule'>
...
</script>
这时候就需要babel进行打包编译
如果浏览器即不支持module也不支持nomodule这个时候需要用到systemjs
安装依赖
npm install @babel/cli @babel/core @babel/plugin-transform-modules-systemjs @babel/preset-env -D
配置.babelrc
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-transform-modules-systemjs"
]
}
执行打包
"build": "babel ./assets/scripts/data.js -o ./assets/scripts/data_bundle.js"
systemjs是一个通用模块加载器,支持AMD、Commonjs、ES6等各种格式的模块加载
在index.html引入systemjs
<script nomodule src="https://cdn.staticfile.org/systemjs/6.3.3/system.js"></script>
<script nomodule>
// 不支持module 不支持nomodule 下面
System.import("./scripts/data_bundle.js").then((_) => {
console.log(_.default)
});
</script>
如果支持module不支持nomodule那么如上代码会被执行两次,解决如下
<script>
(function () {
var check = document.createElement('script');
if (!('noModule' in check) && 'onbeforeload' in check) {
var support = false;
document.addEventListener('beforeload', function (e) {
if (e.target === check) {
support = true;
} else if (!e.target.hasAttribute('nomodule') || !support) {
return;
}
e.preventDefault();
}, true);
check.type = 'module';
check.src = '.';
document.head.appendChild(check);
check.remove();
}
}());
</script>
总结如下:
- 支持module、支持nomodule
- 支持module不支持nomodule代码会执行2次
- 不支持module不支持nomodule
拦截后端接口并进行格式化
新建controllers目录,新建Book.js
const axios = require('axios');
class Book {
static async getData() {
const { data } = await axios.get('http://localhost:8080/book/list')
data.forEach(item => {
delete item.content
delete item.page
});
return data
}
}
module.exports = Book
如上访问本地java接口,对返回的数据进行了处理,删除了一些无用的数据
Node BFF层进行调用
router.get('/book/list', async (ctx) => {
const data = await Book.getData()
throw new Success(data)
})
请求流程:
- 前端发送请求到Node BFF层
- Node再次发送请求到JAVA获取到数据,并对数据进行返回

小插件
解决目录引入复杂问题
require('../../../../some/very/deep/module')
安装module-alias解决如上问题,用法如下:
const moduleAlias = require('module-alias');
moduleAlias.addAliases({
'@root': __dirname,
'@models': __dirname + '/models',
'@controllers': __dirname + '/controllers',
});
// 引用 root 下的某个文件
import module from '@root/some-module'