跳到主要内容
版本:2.0.0

多框架研发

所谓的多框架启动,指的是多个能提供服务的上层框架,在一个进程中同时提供服务。

这里的多个上层框架,指的是 midway 提供的 @midwayjs/web , @midwayjs/koa , @midwayjs/express , @midwayjs/socektio , @midwayjs/grpc , @midwayjs/rabbitmq  等。

这些框架都能独立对外提供服务,暴露某个协议。比如 @midwayjs/web (包装 Egg.js,提供 HTTP   服务),@midwayjs/grpc(包装 grpc.js,提供 gRPC 服务)。

框架(Framework)概念

Midway 现有的框架(Framework)每个是独立的,每一个框架都可以单独在进程中运行,理论上来说,每个框架都是一个独立的依赖注入容器,加上特定框架包含的三方库的组合。

这些独立的框架,都遵循 IMidwayFramewok 的接口定义,由 @midwayjs/bootstrap 库加载起来。

所以在提供的单进程部署方案中,我们可以通过一个 bootstrap.js 入口来启动应用。

// bootstrap.js

const WebFramework = require('@midwayjs/koa').Framework;
const web = new WebFramework().configure({
port: 7001,
});

const { Bootstrap } = require('@midwayjs/bootstrap');
Bootstrap.load(web).run();

多个框架启动文件(主副框架)

如果我们需要启动多个上层框架,可以利用 load  方法加载多次  。

// bootstrap.js

const WebFramework = require('@midwayjs/koa').Framework;
const GRPCFramework = require('@midwayjs/grpc').Framework;
const { Bootstrap } = require('@midwayjs/bootstrap');

const web = new WebFramework().configure({
port: 7001,
});

const grpcService = new GRPCFramemwork().configure({
services: [
{
protoPath: join(__dirname, 'proto/helloworld.proto'),
package: 'helloworld',
},
],
});

Bootstrap.load(web).load(grpcService).run();
信息

注意,所有的上层框架都会遵循规范,导出一个 Framework 属性。

这里有一个主、副框架的概念。

第一个被 load 的框架为主框架,后面被 load 的都为副框架。

比如,上面示例的 @midwayjs/koa  为主框架, @midwayjs/grpc  为副框架。

主框架只能一个,而副框架可以有多个。

信息

主框架在使用时略微有一些优势。

启动多框架

多框架启动需要依赖启动文件。

在本地开发时,我们之前使用 midway-bin dev --ts  命令,需要增加一个 entryFile  的入口参数。

$ cross-env NODE_ENV=local midway-bin dev --ts --entryFile=bootstrap.js
信息

bootstrap.js  会自动判断 ts 环境,在本地开发时会加载 src 下的 ts 文件。

在服务器部署时,由于脚手架自带了 bootstrap.js  文件,直接修改即可,启动文件依旧为 start 命令(注意,启动前需要执行 npm run build 先将 ts 构建为 js)。

$ cross-env NODE_ENV=production node bootstrap.js

多框架场景不支持使用 egg-scripts 部署。

多框架生命周期

业务代码中生命周期的 onReady  方法在一个进程只执行一次。

信息

在 egg 场景下,只会在 worker 进程生效。

import { Configuration } from '@midwayjs/decorator';
import { Application } from '@midwayjs/koa';
import { Application as GRPCApplication } from '@midwayjs/grpc';

@Configuration()
export class AutoConfiguration {
async onReady() {
// 这个 onReady 方法只会执行一次
}
}

全局的依赖注入容器

所有的框架将共享同一个依赖注入容器。

如下图,启动器(@midwayjs/bootstrap)模块将提前初始化一个依赖注入容器 A,在后续所有的框架中,都将复用这个依赖注入容器 A。

这意味这,在任意框架注入到容器中的单例,在其他框架也可以取到。

多框架获取应用(app)对象

如果需要不同框架的 app ,比如需要 web app 加载中间件,以及 grpc app 做初始化,我们可以通过 @App 的参数来注入不同框架的 app 实例。

如果不传递 @App  装饰器的参数,默认注入的 app 为主框架 app

如果需要副框架的 app,请传递框架类型参数。框架的类型,是一个 MidwayFrameworkType  类型的枚举值。

import { Configuration, MidwayFrameworkType } from '@midwayjs/decorator';
import { Application } from '@midwayjs/koa';
import { Application as GRPCApplication } from '@midwayjs/grpc';

@Configuration()
export class AutoConfiguration {
@App()
app: Application;

// 注入不同的 app
@App(MidwayFrameworkType.MS_GRPC)
grpcApp: GRPCApplication;

async onReady() {}
}

对于其他服务类来说,一般不会需要获取和框架相关联的 app 属性,常用的 API 比如 getLogger , getApplicationContext , getBaseDir  等方法,在所有的 app 返回是一致的,一般直接使用主框架的 app 即可(事实上你不需要知道是哪个框架)。

import { Provide, App } from '@midwayjs/decorator';
import { IMidwayApplication } from '@midwayjs/core';

@Provide()
export class UserService {
@App()
app: IMidwayApplication; // 推荐使用这个定义,所有的框架都会实现这个定义
}

多框架上下文处理

对于接入层(Controller,Socket 等暴露服务的),都是固定某一种框架和协议的,所以注入的 Context 是固定的。

import { Provide, Inject, Get } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';

@Provide()
@Controller()
export class HomeController {
@Inject()
ctx: Context; // 这里注入的一定是匹配当前框架的上下文

@Get()
async getMethod() {}
}

对于服务层来说,由于代码是一份,上下文对象有可能随着请求的不同,而随着框架变动。

我们不建议在 Service 层去获取跟 Controller(协议层)相关的代码,当前的上下文对象透传只是为了传递一些请求链路的数据(比如 open-tracing)。

比如在 web 场景下:

import { Provide, Inject, Get } from '@midwayjs/decorator';

@Provide()
export class UserService {
@Inject()
ctx: any;

async getUser() {
// ctx.query.id // 不推荐在 service 调用协议相关的代码
}
}

推荐的用法是使用和协议层无关的定义:

import { Provide, Inject, Get } from '@midwayjs/decorator';
import { IMidwayContext } from '@midwayjs/core';

@Provide()
export class UserService {
@Inject()
ctx: IMidwayContext; // 这个定义仅有一些特定的属性

async getUser() {
// ctx.logger
}
}

在实在需要特定框架的上下文的场景下,依旧可以使用原框架的定义(不太推荐)。

import { Provide, Inject, Get } from '@midwayjs/decorator';
import { Context } from '@midwayjs/koa';
import { Context as GRPCContext } from '@midwayjs/grpc';

@Provide()
export class UserService {
@Inject()
ctx: Context & GRPCContext;

async getUser() {
// ctx.xxxxx
}
}

入口文件加载环境配置

如果框架初始化的配置较长或者有时候用户希望放到 src/config.default.ts  下,可以使用我们提供的回调写法,回调的参数为当前环境的配置对象。

比如 config.default.ts  中。

// config.default
export const cluster = {
port: 7001,
};

export const grpcServer = {
services: [
{
protoPath: join(__dirname, 'proto/helloworld.proto'),
package: 'helloworld',
},
],
};

那么在入口的地方,也可以这么写:

const WebFramework = require('@midwayjs/koa').Framework;
const GRPCFramework = require('@midwayjs/grpc').Framework;
const { Bootstrap } = require('@midwayjs/bootstrap');

Bootstrap.load((config) => {
return new WebFramework().configure(config.cluster);
})
.load((config) => {
return new GRPCFramemwork().configure(config.grpcServer);
})
.run();

这样的好处是,入口的配置可以随着环境变化。

注意,这个功能是由 Midway 提供的,必须要在 configuration.ts  中开启 importConfigs  功能。

// src/configuration.ts
import { Configuration } from '@midwayjs/decorator';
import { join } from 'path';

@Configuration({
importConfigs: [
join(__dirname, './config/'), // 该功能依靠这段代码查找配置
],
})
export class ContainerLifeCycle {}
信息

只会读取到用户代码和 midway 组件的配置,egg 插件的配置不会被读取到。

框架前异步逻辑(异步配置)

我们可以在所有的框架启动之前,做一些异步的行为,比如比较常见的在启动前使用异步加载配置。

Bootstrap  提供了 before  方法用于在所有的框架前执行异步操作。

Bootstrap.load(webFramework)
.load(grpcFramework)
.before(async (container) => {
// ...
})
.run();

以一个异步配置加载为例,我们的需求是在应用启动前,从远端拉取配置,并合并到业务的配置中。

首先定义一个异步获取配置的类,比如 src/remoteConfigService.ts 。

import { App, Provide, Init } from '@midwayjs/decorator';
import { IMidwayApplication } from '@midwayjs/core';

@Provide()
export class RemoteConfigService {
@App()
app: IMidwayApplication;

@Init()
async syncConfig() {
// 这里获取一个远端的配置,HTTP,或者订阅其他的配置协议
const remoteConfig = await this.getRemote();

// 将配置合并到全局的配置服务中
this.app.addConfigObject(remoteConfig);
}
}

然后在所有框架启动时激活它。

Bootstrap.load(webFramework)
.load(grpcFramework)
.before(async (container) => {
await container.getAsync(RemoteConfigService);
})
.run();

这里的 container 是我们的全局依赖注入容器,等价于 app.getApplicationContext() ,所以获取的服务是单例

由于我们使用了 @Init  装饰器,所以在创建实例的时候就会被触发,并且保留在内存中。代码的流程和在应用中相同。

这里使用了 app.addConfigObject  方法和应用中的配置合并,后续业务中使用 @Config  获取配置的时候,就能拿到最终的配置了。

多框架测试

传统单框架,我们使用 createApp  方法进行测试,获取到 app 对象后做操作,但是在多框架下,稍有不同,会创建出多个框架的 app 实例。

@midwayjs/mock  提供了 createBootstrap  方法做启动文件类型的测试。我们可以将入口文件 bootstrap.js  作为启动参数传入,这样 createBootstrap  方法会通过入口文件来启动代码。

import { createBootstrap } from '@midwayjs/mock';
import { MidwayFrameworkType } from '@midwayjs/decorator';

describe('/test/new.test.ts', () => {
it('should GET /', async () => {
// create app
const bootstrap = await createBootstrap(join(process.cwd(), 'bootstrap.js'));
const app = bootstrap.getApp(MidwayFrameworkType.WEB_KOA);

// expect and test

// close bootstrap
await bootstrap.close();
});
});

具体请参考 使用 bootstrap 文件测试