扩展 BFF Server

部分应用中,开发者可能希望对所有 BFF 函数做统一的处理,例如鉴权、日志、数据处理等。

Modern.js 提供了两种方式,允许开发者根据运行时框架自由扩展 BFF Server。

中间件

开发者可以在编写 api/_app.ts 文件中编写中间件,用来扩展 BFF Server。以 Express 作为运行时框架为例,介绍如何手写一个中间件,添加权限校验:

api/_app.ts
import { hook } from '@modern-js/runtime/server';
import { Request, Response, NextFunction } from 'express';

export default hook(({ addMiddleware }) => {
  addMiddleware(async (req: Request, res: Response, next: NextFunction) => {
    if (req.url !== '/api/login') {
      const sid = req?.cookies?.sid;
      if (!sid) {
        res.status(400);
        res.json({ code: -1, message: 'need login' });
      } else {
        next();
      }
    } else {
      next();
    }
  });
});

然后添加一个普通的 BFF 函数 api/lambda/hello.ts

api/lambda/hello.ts
export default async () => {
  return 'Hello Modern.js';
};

接下来,在前端 src/routes/page.tsx 添加接口的访问代码,直接使用一体化的方式调用:

src/routes/page.tsx
import { useState, useEffect } from 'react';
import { get as hello } from '@api/hello';

export default () => {
  const [text, setText] = useState('');

  useEffect(() => {
    async function fetchMyApi() {
      const { message } = await hello();
      setText(message);
    }

    fetchMyApi();
  }, []);

  return <p>{text}</p>;
};

现在运行 dev 命令启动项目,访问 http://localhost:8080/ 会发现 /api/hello 的请求被拦截了:

Network

最后修改前端代码 src/routes/page.tsx,在访问 /api/hello 前先调用登录接口:

NOTE

此处没有真实实现登录接口,代码仅作为演示。

import { useState, useEffect } from 'react';
import { get as hello } from '@api/hello';
import { post as login } from '@api/login';

export default () => {
  const [text, setText] = useState('');

  useEffect(() => {
    async function fetchAfterLogin() {
      const { code } = await login();
      if (code === 0) {
        const { message } = await hello();
        setText(message);
      }
    }
    fetchAfterLogin();
  }, []);

  return <p>{text}</p>;
};

刷新页面,可以看到 /api/hello 访问成功:

Network

以上代码模拟了在 /api/_app.ts 中添加中间件的方式,实现了简易的登录功能。同样,可以在这个钩子文件中实现其他功能来扩展 BFF Server。

INFO

不同运行时框架中,中间件的写法不一定相同,详情可见运行时框架

定义 Server 实例

除了中间件之外,还可以通过 api/app.ts 文件来定义 BFF Server 的实例。开发者需要默认导出一个能被运行时框架插件识别的实例。这里简单展示一下 Koa 和 Express 如何定义 Server 实例。

Express.js
Koa.js
api/app.ts
import express from 'express';

const app = express();
app.use(async (req, res, next) => {
  console.info(`access url: ${req.url}`);
  next();
});

export default app;

在复杂的 BFF 逻辑中,定义 Server 实例可以更方便通过一个入口来组织代码逻辑,设计目录结构。在这个文件中,可以执行初始化逻辑,添加全局中间件,声明路由,甚至扩展原有框架。

BFF 函数定义的路由会在 app.ts 文件定义的路由之后注册,所以在这里你也可以拦截 BFF 函数定义的路由,进行预处理或是提前响应。

NOTE

此时,如果应用中同时存在 api/_app.ts,则定义的中间件会被放在 api/app.ts 导出实例的最后执行。多数情况下,中间件就能覆盖大多数 BFF 函数的定制需求。只有当应用的服务端逻辑比较复杂时,才需要自定义 Server 实例。

CAUTION

当应用中没有 api/app.ts 的时候,Modern.js 默认会添加 koa-body。当应用中存在 api/app.ts 时,如果开发者希望使用带有 Body 的 BFF 函数,需要自行添加 koa-body