跳转到主文档

运行时框架

如前面章节所说,目前 Modern.js 的 BFF 支持 4 种主流的 Server 框架,可以根据自身偏好选择。

多框架支持也是【 一体化 BFF 】中重要的一环,多数情况下,开发者直接使用钩子文件来扩展 BFF 函数,无需关心通过框架启动服务、日志输出等应用级别的问题。

所有框架均支持 BFF 函数的所有能力,并且使用方式是相同的,例如:

  • RESTful API
  • Schema 模式
  • Hooks(useContext)
  • 不同的数据类型
  • 动态路由
  • 一体化调用

Modern.js BFF 中兼容了这些框架大部分的规范,开发者可以直接使用对应 Server 框架的约定和生态。

每一种框架都提供了两类扩展写法 BFF 函数的方式,分别是【 函数写法 】和【 框架写法 】。

函数写法

在上一章节中,简单的演示了 Express 扩展 BFF 函数的示例。

函数写法就是通过添加钩子文件 _app.ts 的方式,编写中间件逻辑来扩展 BFF 函数。

Express

Express 的函数写法,可以通过在 _app.[tj]s 下添加 express 的中间件:

import { hook } from "@modern-js/runtime/server";

export default hook(({ addMiddleware }) => {
addMiddleware(async (req, res, next) => {
req.query.id = "express";
await next();
});
});

Nest

Nest 支持添加两种类型的内容:Express 的函数中间件和 Nest 中的 Module

Nest 的函数中间件的添加与 Express 段中的示例相同,Module 写法如下:

api/_app.ts
import { hook } from "@modern-js/runtime/server";
import { Module, Injectable, Controller, Get } from "@nestjs/common";

@Controller("cats")
export class CatsController {
constructor(private readonly catsService: CatsService) {}

@Get()
getHello() {
return this.catsService.getHello();
}
}

@Injectable()
class CatsService {
getHello(): string {
return "Hello world!";
}
}

@Module({
controllers: [CatsController],
providers: [CatsService],
})
class CatsModule {}

export default hook(({ addMiddleware }) => {
addMiddleware(CatsModule);
});

Koa

Koa 函数写法下,可以通过在 _app.[tj]s 下添加 koa 的中间件:

import { hook } from "@modern-js/runtime/server";

export default hook(({ addMiddleware }) => {
addMiddleware(async (ctx, next) => {
console.info(`access url: ${ctx.url}`);
next();
});
});

Egg

Egg 函数写法下,可以通过在 _app.[tj]s 下添加 egg 的中间件:

import { hook } from "@modern-js/runtime/server";

export default hook(({ addMiddleware }) => {
addMiddleware((options) => async (ctx, next) => {
console.info(`access url: ${ctx.url}`);
next();
});
});

也可以添加 egg 第三方的中间件,且给第三方中间件注入参数:

import { hook } from "@modern-js/runtime/server";

export default hook(({ addMiddleware }) => {
addMiddleware([
"eggMiddleware", // 此处为包名
{
name: "modernjs",
},
]);
});

框架写法

框架写法是一种使用框架分层结构来扩展 BFF 函数的方式。

和函数写法相比,框架写法虽然能够利用更多框架的结构,在复杂场景下让整个 BFF Server 更加清晰,但也相的更加复杂,需要关心更多框架层面的内容。

多数情况下,函数写法就能覆盖大多数 BFF 函数的定制需求。只有当你的项目服务端逻辑比较复杂,代码需要分层,或者需要使用更多框架的元素时(如 egg 插件),才需要使用框架写法。

框架写法中,所有的 BFF 函数都需要写在 api/lambda/ 目录下,并且无法使用钩子文件 _app.[tj]s

Express

Express 的框架写法支持可在 api/app.[tj]s 定义 API Server 的启动逻辑,执行应用的初始化工作,添加全局中间件,声明路由,甚至扩展原有框架等。

注意这里是 app.[tj]s,而不是函数写法中的钩子文件 _app.[tj]s

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

api/app.ts
import express from "express";

const app = express();

app.put("/user", function (req, res) {
res.send("Got a PUT request at /user");
});

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

export default app;

如果需要在 BFF 函数注册路由后添加中间件,错误处理等,可以使用 afterLambdaRegisted hook,该 hook 中的代码会在 BFF 函数注册路由后执行:

api/app.ts
const app = express();
// 其他代码...
export default app;

export const afterLambdaRegisted = (app: Express) => {
const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction,
) => {
if (res.headersSent) {
return next(err);
}
res.status(500).send('some error message');
}
app.use(errHandler);
}

Nest

Nest 虽然有定制的启动器,但本质与 Express、Koa 相同,所以 Modern.js 沿用了 Nest 定制启动器的默认入口:api/main.ts

按照 Nest 官方生成器生成的项目结构,在 Modern.js 中使用 Nest 框架写法时,api/ 目录结构为:

.
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── lambda
│ └── hello.ts
└── main.ts

其中 api/main.ts 中的内容与 Nest 官方生成器生成模版有所不同,应用工程中支持了两种模式:

不包含内置 Module:

api/main.ts
import { defineCustom } from "@modern-js/plugin-nest";
import { NestFactory } from "@nestjs/core";
import { Module } from "@nestjs/common";
import { AppModule } from "./app.module";

export default NestFactory.create(AppModule);

包含内置 Module:

api/main.ts
import { defineCustom } from "@modern-js/plugin-nest";
import { NestFactory } from "@nestjs/core";
import { Module } from "@nestjs/common";
import { AppModule } from "./app.module";

export default defineCustom(async (modules) => {
@Module({
imports: [AppModule, ...modules],
})
class MainModule {}

return NestFactory.create(MainModule);
});

Koa

Koa 框架写法与 Express 类似,支持在 app.[tj]s 定义 API Server 的启动逻辑,执行应用的初始化工作,添加全局中间件,声明路由,扩展原有框架等。

注意这里是 app.[tj]s,而不是函数写法中的钩子文件 _app.[tj]s

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

注意

在框架写法下,当没有 app.ts 的时候,Modern.js 默认会添加 koa-body;当有 app.ts 时,如果开发者希望使用带有 Body 的 BFF 函数,需要确保 koa-body 中间件已经添加。

api/app.ts
import koa from "koa";

const app = new Koa();

app.put("/user", function (req, res) {
res.send("Got a PUT request at /user");
});

app.use(async (ctx, next) => {
console.info(`access url: ${ctx.url}`);
await next();
});

export default app;

Egg

目录结构

Modern.js 在 egg 框架写法中添加的初始样板文件较为简单,但 Modern.js 允许开发者使用 egg 框架约定的几乎所有文件(如中间件,定时器,控制器等)。

egg 框架写法的初始结构
.
├── api/
│ ├── app/
│ │ └── middleware/
│ │ └── trace.ts
│ ├── config/
│ │ └── config.default.ts
│ └── lambda/
│ └── hello.ts
├── src/
├── modern.config.js
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json

Modern.js 的 一体化 BFF 函数 和 egg 中的 controller 有什么不同?

  • lambda/ 下的 BFF 函数文件拥有定义路由和处理 API 逻辑两种功能,当你写一体化 BFF 函数的时候;就无需再写 controller 和 router。
  • lambda/ 目录通过文件的目录结构定义路由,Egg 通过在 router.ts 文件编写代码定义路由。
  • lambda/ 下可以写 BFF 函数,去处理 BFF 的逻辑;Egg 需要定义 class 声明 controller,BFF 函数的样板代码更少。
  • lambda/ 下的文件可以使用 Modern.js 提供的 BFF API,如 useContext
  • lambda/ 下的 BFF 函数可以和 eggroutercontroller 等混合使用,但通常不建议这么做。

调用 service

在 egg 的框架写法下,同 egg 框架的使用方式一致,开发者也可以通过 ctx 调用定义的 service。

假设有以下目录结构和文件:

.
├── api/
│ ├── app/
│ │ └── service/
│ │ └── user.ts
│ ├── config/
│ │ ├── config.default.ts
│ └── lambda/
│ └── hello.ts
/app/service/user.ts
import { Service } from "egg";

class UserService extends Service {
async find(uid) {
const user = await this.ctx.db.query(
"select * from user where uid = ?",
uid
);
return user;
}
}

export default UserService;

lambda 目录下定义的 BFF 函数中,可以使用 useContext API 获取 egg 请求上下文

/api/lambda/hello.ts
import { useContext } from "@modern-js/runtime/server";

export const get = async () => {
const ctx = useContext();
const userId = ctx.params.id;
const user = await ctx.service.user.find(userId);
return user;
};

自定义启动逻辑

在 egg 的框架写法中,同样支持启动自定义,api/app[tj]sagent.[tj]s 遵循 egg 的规范。

具体可见启动自定义