# 前后端实现BFF架构(二)

# 零、写在前面

在**前后端实现BFF架构(一)**里面我们使用Koa2及一系列的插件简单的搭出了Node服务的MVC框架,这篇文章是上一篇文章的后续,来完成接下来的一些工作。这篇文章我们将从以下几个点来完善我们的BFF架构:

  • 对KOA2项目做一些容错

  • 将YII的后端项目写成只提供接口的形式,给NodeJS使用

  • PlayWright完成页面测试

    mocha完成Node接口测试

  • 业务逻辑用ES6编写,并将浏览器支持分开,不支持的用babel编译system.js加载

  • 每一次新增记录的点击用函数式编程的方式稀释(节流)。

# 一、添加错误日志记录

添加错误日志记录需要使用一个库log4js。我们先安装这个插件,因为我们的日志记录要在线上使用,所以就不需要只安装在开发环境中了:

$ npm install log4js
1

安装完之后我们在app.js中引入log4js

/* app.js */

const Koa = require('koa');
const render = require('koa-swig');
const co = require('co');
const server_static = require('koa-static');
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
const app = new Koa();
const log4js = require("log4js");		// 引入log4js
const config = require('./config');
const initController = require('./controllers/index');
const errorHandle = require('./middleware/ErrorHandle');

// log4js的初始化和设置
const logger = log4js.getLogger();
// 设置日志级别为debug级别,也可以设置为其他的级别。
logger.level = "debug";
// 这条日志会在控制台输出
logger.debug("Some debug messages");

app.use(server_static(config.staticDir));
app.use(historyApiFallback({ index: '/', whiteList: ['/api', '/activity'] }));
app.context.render = co.wrap(render({
  // 模板文件的路径和是否缓存尽量的放在配置文件中,可以随意配置。
  root: config.viewsDir,
  cache: config.cache
}));

errorHandle.error(app);

initController(app);

app.listen(config.port, () => {
  console.log(`server is running at http://loaclhost:${config.port}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

根据上面的代码注释,我们可以看到控制台中输出了Some debug message字样的日志。log4js比自带的日志打印优越的地方还在于,它带有时间戳,可以更加细致的去定位问题。除了日志的时间,log4js还会区分日志的级别和日志的分类。

node-app.png

我们可以看一下log4js都支持什么日志级别:

/* log4js日志级别 */

const logger = log4js.getLogger("cheese");

logger.trace("Entering cheese testing");
logger.debug("Got cheese.");
logger.info("Cheese is Comté.");
logger.warn("Cheese is quite smelly.");
logger.error("Cheese is too ripe!");
logger.fatal("Cheese was breeding ground for listeria.");
1
2
3
4
5
6
7
8
9
10

我们可以看到log4js支持的日志级别一共是六种级别,分别是tracedebuginfowarnerrorfatel。我们在生产中遇到问题排查的时候一般都是去查看问题的日志,但是日志如果只在控制台中输出,这样的话不利于管理,那么为了解决这个问题,我们需要对日志就行本地的持久化,也就是把日志写入到日志文件中去。下面我们来实现一下日志的本地化。

/* app.js */

const Koa = require('koa');
const render = require('koa-swig');
const co = require('co');
const server_static = require('koa-static');
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
const app = new Koa();
const log4js = require("log4js");		// 引入log4js
const config = require('./config');
const initController = require('./controllers/index');
const errorHandle = require('./middleware/ErrorHandle');

// log4js的初始化和设置
const logger = log4js.getLogger();
// 设置日志级别为debug级别,也可以设置为其他的级别。
logger.level = "debug";
// 这条日志会在控制台输出
logger.debug("Some debug messages");
// 配置log输出文件
log4js.configure({
  appenders: { globalError: { type: "file", filename: "./logs/error.log" } },
  categories: { default: { appenders: ["globalError"], level: "error" } }
});

app.use(server_static(config.staticDir));
app.use(historyApiFallback({ index: '/', whiteList: ['/api', '/activity'] }));
app.context.render = co.wrap(render({
  // 模板文件的路径和是否缓存尽量的放在配置文件中,可以随意配置。
  root: config.viewsDir,
  cache: config.cache
}));

errorHandle.error(app);

initController(app);

app.listen(config.port, () => {
  console.log(`server is running at http://loaclhost:${config.port}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

通过我们的设置我们可以清楚的看到,记录的错误的名称是globalErrortype: "file"就是以文件的形式存储在filename所指定的./logs/error.log文件中。categories中的appenders是错误类型 ”globalError“对应了 appenders中的globalError。level: "error",这个设置的是error级别或者大于error级别的的错误日志才会记录到文件中。

那么,现在配置好之后我们来测试一下,首先我们在app.js中手动输出全部等级的log:

/* app.js */

const Koa = require('koa');
const render = require('koa-swig');
const co = require('co');
const server_static = require('koa-static');
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
const app = new Koa();
const log4js = require("log4js");		// 引入log4js
const config = require('./config');
const initController = require('./controllers/index');
const errorHandle = require('./middleware/ErrorHandle');

// 配置log输出文件
log4js.configure({
  appenders: { globalError: { type: "file", filename: "./logs/error.log" } },
  categories: { default: { appenders: ["globalError"], level: "error" } }
});

// log4js的初始化和设置,日志类型配置成globalError,这里要与前面的日志文件的错误名称配置相同。
const logger = log4js.getLogger('globalError');
// 我们手动输出所有的日志类型
logger.trace("Entering cheese testing");
logger.debug("Got cheese.");
logger.info("Cheese is Comté.");
logger.warn("Cheese is quite smelly.");
logger.error("Cheese is too ripe!");
logger.fatal("Cheese was breeding ground for listeria.");

app.use(server_static(config.staticDir));
app.use(historyApiFallback({ index: '/', whiteList: ['/api', '/activity'] }));
app.context.render = co.wrap(render({
  // 模板文件的路径和是否缓存尽量的放在配置文件中,可以随意配置。
  root: config.viewsDir,
  cache: config.cache
}));

errorHandle.error(app);

initController(app);

app.listen(config.port, () => {
  console.log(`server is running at http://loaclhost:${config.port}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

保存代码,自动执行之后,我们在看一下我们的项目目录:

node-app.png

我们可以看到项目中多了一个log目录,里面有一个error.log日志文件,里面记录的日志和我们手动输出的日志是相同的,日记的级别也和我们的设置相同,说明日志的本地化功能是生效的。

那么我们现在就要在容错的功能中加入错误日志的记录,这个功能毫无疑问要添加在errorHandle的中间件中(/middleware/errorHandle),日志的记录需要log4js的getterLogger,所以我们要把logger实例传到errorHandle的error方法中:

/* app.js */

const Koa = require('koa');
const render = require('koa-swig');
const co = require('co');
const server_static = require('koa-static');
const { historyApiFallback } = require('koa2-connect-history-api-fallback');
const app = new Koa();
const log4js = require("log4js");
const config = require('./config');
const initController = require('./controllers/index');
const errorHandle = require('./middleware/ErrorHandle');

// 配置log输出文件
log4js.configure({
  appenders: { globalError: { type: "file", filename: "./logs/error.log" } },
  categories: { default: { appenders: ["globalError"], level: "error" } }
});

// log4js的初始化和设置,日志类型配置成globalError,这里要与前面的日志文件的错误名称配置相同。
const logger = log4js.getLogger('globalError');

app.use(server_static(config.staticDir));
app.use(historyApiFallback({ index: '/', whiteList: ['/api', '/activity'] }));
app.context.render = co.wrap(render({
  // 模板文件的路径和是否缓存尽量的放在配置文件中,可以随意配置。
  root: config.viewsDir,
  cache: config.cache
}));

// 传入logger实例
errorHandle.error(app, logger);

initController(app);

app.listen(config.port, () => {
  console.log(`server is running at http://loaclhost:${config.port}`);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* middleware/errorHandle.js */

class ErrorHandle {
  // app传入的logger实例
  static error(app, logger) {
    app.use(async (ctx, next) => {
      try {
        await next();
        if(ctx.status === 404) {
          ctx.body = '友好的404页面';
        }
      } catch(err) {  // catch把错误对象传入
        // 发生错误的时候,记录日志
        logger.error(err.message);
        ctx.body = '500请求,正在积极修复。';
      }
    });
  }
}

module.exports = ErrorHandle;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* conteollers/indexController.js */

const Controller = require('./Controller');

class IndexController extends Controller {
  constructor () {
    super();
  }

  async actionIndex(ctx, next) {
    throw Error('测试log4js日志本地化');
    ctx.body = await ctx.render('home/index');
    next();
  }
}

module.exports = IndexController;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

以上的代码就是log4js的所有测试代码,我们来简单的分析一下。首先我们在app.js中初始化logger实例,然后把logger实例传入错误捕获中间件的errorHandle.error中,再看错误捕获中间件的处理,需要拿到logger实例,然后再出错的分支下,记录错误日志。这是后的try……catch必须要传入err对象。以便记录错误的详细信息。最后我们在indexController中手动抛出一个错误。我们来请求一下首页,首页毫无疑问会出现500请求,正在积极修复。,看看最终的错误日志:

node-app.png

到此为止,我们使用log4js实现错误日志的本地化存储功能全部实现。最后我们还要解决一个问题,如果日志文件过大怎么解决?

解决日志文件过大的问题:

  • 设置一个最大日志数以及清理日志的时间间隔,
  • 以清理日志时间间隔开启一个定时任务
  • 获取所有日志,按日志生成时间(日志名)做一个倒序,按照排列顺序保留前最大数值的文件。其余删除。

# 二、ES6模块化改造

我们项目的编写现在都是使用的CommonJS,在Node中也可以使用ES6的模块化进行开发,所以我们可以使用把现在项目中的CommonJS改写成ESModule形式。

在代码的书写形式上做如下修改:

// 修改之前
const xxx = require('xxx');

// 修改之后
import xxx from 'xxx';
1
2
3
4
5

但是如果我们只是在引入的时候做这种改写,代码必定会挂掉,原因很简单我们只是在引入的时候使用了ESModule的形式,但是在导出的时候还是使用的CommonJS,这必定会出问题。我们自己编写的模块要改成ESModule的导出形式,但是问题时Koa等相当一部分Node库是使用CommonJS编写的,并不支持ESModule。

为了解决上述的问题我们可以使用@babel/node 插件解决,安装@babel/node:

$ npm install @babel/node -D
1

除了@babel/node我们还需要安装一个babel套件,,在开发环境进行代码的转化:

$ npm install @babel/preset-env -D
1

安装完这些之后我们还需要一些简单的配置:

// .babelrc
{
  "presets": ["@babel/preset-env"]
}
1
2
3
4

配置完之后我们需要对更改package.json中的启动命令,让babel转化一下app.js:

{
  "name": "BFF-Project",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "NODE_ENV=development nodemon --exec 'babel-node ./app.js'"
  },
  "dependencies": {
    "@koa/router": "^10.0.0",
    "co": "^4.6.0",
    "koa": "^2.13.1",
    "koa-static": "^5.0.0",
    "koa-swig": "^2.2.1",
    "koa2-connect-history-api-fallback": "^0.1.3",
    "log4js": "^6.3.0"
  },
  "devDependencies": {
    "@babel/core": "^7.14.5",
    "@babel/node": "^7.14.5",
    "@babel/preset-env": "^7.14.5"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

最后我们把自己编写的代码,导出方式全部改写成ESModule的形式。

# 三、添加Model层调用后端接口

BFF层需要通过HTTP请求PHP后端的数据:

  • 首先我们先改写PHP的后端代码,把PHP的代码改成接口:
/* basic/controllers/BooksController.js */

public function actionIndex()
{
    $searchModel = new BooksSearch();
    $dataProvider = $searchModel->search(Yii::$app->request->queryParams);

    // return $this->render('index', [
    //     'searchModel' => $searchModel,
    //     'dataProvider' => $dataProvider,
    // ]);
  	// 可以看到这里我们把actioninIndex的数据注入,改写成了返回JSON数据,接口改造完成
    Yii::$app->response->format = Response::FORMAT_JSON;
    return $dataProvider -> getModels();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

我们把YII单体应用的controller里面的方法改写成了接口,用以返回JSON数据。

  • 然后我们在Node项目下的models目录下创建一个BooksModel,代码如下:
/* /models/BooksModel.js */

import axios from 'axios';

class BooksModel {
  // 获取图书列表
  getBookList() {
    // 这里请求的是PHP后端服务器的接口
    return axios.get("http://localhost/basic/web/index.php?r=books/index");
  }
  
  // 查找图书
  findBooB(id) {
    
  }
}

export default BooksModel;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

我们在Node中使用axios请求PHP后端的接口。然后作为getBookList方法的返回值,在这里由于我们的Node也是MVC模式,所以我们要创建一个BooksModel类来管理所有关于图书的增删改查的操作,每个操作我们使用一个方法来实现。getBookList就是获取如数列表的方法。

  • 现在我们准备好了Model,就需要controller管理路由,然后返回接口数据了。
/* controllers/ApiController.js */

import Controller from './Controller';
import BooksModel from '../models/BooksModel';

class ApiController extends Controller {
  constructor () {
    super();
  }
  
  async actionBooksList(ctx, next) {
    const booksModel = new BooksModel();
    const result = await booksModel.getBookList();
    ctx.body = result.data;
  }
}

export default ApiController;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

之前的ApiController是返回的一个写死的JSON,现在我们改写它使控制器对象上的实例方法返回动态数据,首先引入BooksModel,然后actionBooksList方法中实例化BooksModel对象,调用实例方法,取得从PHP后台请求到Node的数据,要注意的是当前的请求时HTTP请求是异步操作。所以使用了async await。最后把JSON数据返回。

下面我们来添加一下Node的getBookList路由:

/* controllers/index.js */

import Router from "@koa/router";
const router = new Router();
import IndexController from './IndexController';
const indexController = new IndexController();
import ApiController  from './ApiController';
const apiController = new ApiController();
import ActivityController from './ActivityController';
const actionActivity = new ActivityController();
import VueController from './VueController';
const vueController = new VueController();
import BooksController from './BooksController';
const booksController = new BooksController();

function initController(app) {
  router.get('/', indexController.actionIndex);
  // 这里添加了bookslist接口路由
  router.get('/api/bookslist', apiController.actionBooksList);
  router.get('/vue', vueController.actionVue);

  app
    .use(router.routes())
    .use(router.allowedMethods());
}

export default initController;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 最后我们在浏览器地址栏输入 http://localhost:8000/api/booklist
node-app.png

到现在为止我们的BFF层调用后端的接口这一功能就全部实现了。

# 四、BFF请求后端数据渲染到页面

我们在Node中请求后端的接口,获得数据之后希望渲染到页面上,这个功能的实现需要以下几步:

  • 编写BooksController控制器:

    /* controllers/BookController */
    
    import Controller from './Controller';
    import BooksModel from '../models/BooksModel'
    
    class BooksController extends Controller {
      constructor () {
        super();
      }
    
      async actionBooksListPage(ctx, next) {
        const booksModel = new BooksModel();
        const result = await booksModel.getBookList();
        ctx.body = await ctx.render('books/list', { data: result.data });
        next();
      }
    }
    
    export default BooksController;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    引入BooksModel之后,在actionBooksListPage方法中调用getBookList方法获取数据,最后把数据注入到view模板中。

  • 编写books/list模板

    <!-- views/books/list.html -->
    <!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">
      <title>图书列表页</title>
    </head>
    <body>
      <h1>图书列表页</h1>
      {% for item in data %}
      <div>{{ item.name }} - {{ item.price }}</div>
      {% endfor %}
    </body>
    </html>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    循环渲染bookslist数据。

到这里我们就明确的讲完了怎么把Node请求回来的JSON数据渲染到模板中。

到现在为止,我们整体上BFF的流程就完全的讲解完毕了,代码也进行了完全的实现。实现完成之后我们还有很多工作要做:

  1. 对于axios我们需要进行封装,一般不能直接使用,因为我们不能完全信任从后端请求过来的数据。需要在axios这一步骤中进行容错的处理。
  2. 还要对前端的页面进行自动化的测试。

# 对Axios进行简单的容错处理

我们在请求后端的数据时,有可能后端没有给我们正确的数据,这时候很可能导致我们的页面崩溃。所以我们有必要封装一下axios,对请求做一些容错。

在utils目录中新建一个safeRequest.js:

/* utils/safeRequest.js */

import axios from 'axios';

class SafeRequest {
  // 统一设置返回的响应状态码和状态信息,这里的响应状态码一般都是公司内部指定的一套标准。

  static fetch(url) {
    let result = {
      code: 0,
      message: '',
      data: null
    }

    return new Promise(resolve => {
      axios(url).then(res => {
        result.message = 'OK';
        result.data = res.data;
        resolve(result);
      }).catch(err => {
        // 这里可以做统一的错误日志捕获
        result.message = err.message;
        result.code = -1;
        resolve(result);
      });
    });
  }
}

export default SafeRequest;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

我们看到,在封装之后的axios中,我们可以定义一些公司内部使用的响应状态规范,然后根据不同的状态组装result对象,最后返回。而且在出错的时候还可以统一的做错误日志的捕获。

这样封装的好处在于我们对于请求能够集中的管理,不至于写重复的代码。其实axios的功能十分强大,还可以做请求的拦截和响应的拦截,让我们在请求发出之前和收到响应之前做一些业务需要的事情。在真正的业务场景中,可以根据业务的独特性,不断的丰富和健壮这一部分代码。

# 五、如何编写一个自己的函数式编程库

我们如果想编写一个自己的函数式编程库,那么就需要阅读一下经典的函数式编程库的源码:

  • Underscore
  • Lodash

以上这两个都是非常优秀的函数式编程库,笔者建议大家先熟练使用Lodash,然后再尝试读一下Lodash的源码。下面我们就尝试阅读源码,源码的阅读我在github上已经分析过了,接下来我们直接写一个自己的函数式编程库:

/* utils/myUtils */

/* 编写自己的库 */

// 写一个闭包  函数级作用域去隔离
(function () {
    // 判断执行环境
    var root = typeof self == 'object' && self.self === self && self ||
        typeof global == 'object' && global.global === global && global ||
        this || {};

    // 核心
    // 构造安全的构造函数
    var _ = function (obj) {
        // 
        if (obj instanceof _) return obj;
        console.log('new之前',this);
        if (!(this instanceof _)) return new _(obj);

        // 如果前边两个条件都不满足
        this._wrapped = obj;
    };

     // 功能非常丰富
     _.map = function () {}
    _.each = function (arr, fn) {
        for (let i = 0; i < arr.length; i++) {
            fn(arr[i], i);
        }
        return arr;
    }

    _.functions = function (obj) {
        var names = [];
        for (var key in obj) {
            if (_.isFunction(obj[key])) names.push(key);
        }
        return names.sort();
    };
    _.isFunction = function (obj) {
        return typeof obj == 'function' || false;
    };
    /* 
      节流函数
      一般打三个点
      1. 在一定的时间内,函数只执行一次
      2. 第一次触发,会立即执行
      3. 如果再间隔时间内触发,会在间隔时间末尾再执行一次
    */
   _.throttle = function(cb,t){
        let isFirst = true;
        let execDate = + new Date();
        let timeoutId = null;
        return function(){
            if(isFirst){
                // 立即执行
                cb();
                execDate = + new Date();
                isFirst = false;
            }else{
                // 如果不是第一次
                const currentDate = +new Date();
                if(currentDate - execDate >=t){
                    cb();
                    execDate = + new Date();
                }else{
                    timeoutId && clearTimeout(timeoutId);
                    // 要间隔的时间 - 已经过了多长时间(currentDate-execDate)
                    const timeWait = t - (currentDate - execDate);
                    timeoutId = setTimeout(()=>{
                        cb();
                        execDate = + new Date();
                    },timeWait)
                }
            }
        }
   }
   _.xxx = function(){
       
   }

    var push = Array.prototype.push;

    _.mixin = function (obj) {
        // 1. 遍历undescore 上的所有方法 name-方法名
        _.each(_.functions(obj), function (name) {
            // console.log(name);
            // _[name] = obj[name] => 扩展自定义方法
            var func = _[name] = obj[name];
            //  第二种方法执行的一个关键
            _.prototype[name] = function () {
                console.log(this);
                // 参数的一个合并
                // console.log(this._wrapped,arguments);
                /* 第一种方法 */
                var args = [this._wrapped];
                push.apply(args, arguments);
                // return chainResult(this, func.apply(_, args));
                /* ES6的方式 */
                args.push(...arguments);
                return func.apply(_, args);
            };
        });
        return _;
    };
    _.mixin(_);


    /* 全局挂载 */
    root._ = _;
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

函数式编程库一般都是一种可插拔的方式,可插拔的方式的特点就是代码的核心非常小,但是功能非常丰富。

函数式编程的两个特点:

  • 通过函数对数据进行转换
  • 通过串联多个函数来求结果

这里的函数式编程不做讲解,如果想看具体的函数式编程的介绍,可以前往第十一卷。

# 六、前端测试

# I、e2e测试

测试框架:

playWright,微软开源的测试框架,我们来看一下是怎么使用的,要注意的是playWright是没有断言库的,所以我们还要是用chai断言库进行断言:

安装chai断言库:

$ npm install chai -D
1

安装playWright:

$ npm install playWright -D
1

编写测试脚本:

/* tests/e2e.test.js */

const playwright = require('playwright');
const expect = require('chain').expect;

(async () => {
  for (const browserType of ['chromium', 'firefox', 'webkit']) {
    const browser = await playwright[browserType].launch();
    const context = await browser.newContext();
    const page = await context.newPage();
    // 测试项目中的图书列表页
    await page.goto('http://localhost:8000/bookslist');
    // 模拟用户点击
    const content = await page.textContent("#btn");
    expect(content).equal('点击');
    await page.screenshot({ path: `report/example-${browserType}.png` });
    await browser.close();
  }
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

编写测试命令(package.json):

...
"scripts": {
    "start": "NODE_ENV=development nodemon --exec 'babel-node ./app.js'",
    "test:e2e": "node tests/e2e.test.js"
  },
...
1
2
3
4
5
6

# II、前端接口测试

我们在平时的开发中面对后端给我们的接口我们不知道对不对,就需要事先测试一下,这时候我们一般会用psotman进行接口测试,但是如果接口的数量比较多,我们用postman测试是手动测试的,这样会比较浪费时间,所以在前端我们还可以使用接口的自动化测试工具,测试一下后端的接口好不好用。这里我们使用mocha和supertest来进行接口的自动化测试:

首先安装mocha和supertest

$ npm install mocha -D

$ npm install supertest -
1
2
3

安装完成之后我们来编写测试脚本:

/* tests/api.test.js */

const request = require('supertest');

describe('nodejs api test', function() {
  it('获取图书列表接口', function() {
    request('http://localhost:8000')
      .get('api/getBooksList')
    	.expect(200)
    	.end((err, res) => {
      	console.log('res--->', res.body)
    });
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

编写完之后我们还要添加一个测试接口的命令:

...
"scripts": {
    "start": "NODE_ENV=development nodemon --exec 'babel-node ./app.js'",
    "test:e2e": "node tests/e2e.test.js",
  	"test:api": "mocha --file ./tests/api.test.js"
  },
...
1
2
3
4
5
6
7

另外接口测试还可以搭配断言库,来检查返回数据的长度,以及返回数据的格式是否正确。

# 七、总结

BFF架构的实现,到这里也就告一段落了,但这也知识核心的实现部分,还有许多工作要完善。 比如如果要上线的话,Node项目的上线,维护等工作。这就需要PM2来做进程管理和负载均衡。还有就是webpack的打包,这一点很重要我们会在以后慢慢的细化。