自定义组件
组件(Component)是一个可复用与多框架的模块包,一般用于几种场景:
- 1、包装往下游调用的代码,包裹三方模块简化使用,比如 orm(数据库调用),swagger(简化使用) 等
- 2、可复用的业务逻辑,比如抽象出来的公共 Controller,Service 等
组件可以本地加载,也可以打包到一起发布成一个 npm 包。组件可以在 midway v3/Serverless 中使用。你可以将复用的业务代码,或者功能模块都放到组件中进行维护。几乎所有的 Midway 通用能力都可以在组件中使用,包括但不限于配置,生命周期,控制器,拦截器等。
设计组件的时候尽可能的面向所有的上层框架场景,所以我们尽可能只依赖 @midwayjs/core
。
从 v3 开始,框架(Framework)也变为组件的一部分,使用方式和组件保持统一。
开发组件
脚手架
只需执行下面的脚本,模板列表中选择 component-v3
模板,即可快速生成示例组件。
$ npm init midway@latest -y
注意 Node.js 环境要求。
组件目录
组件的结构和 midway 的推荐目录结构一样,组件的目录结构没有特别明确的规范,和应用或者函数保持一致即可。简单的理解,组件就是一个 “迷你应用"。
一个推荐的组件目录结构如下。
.
├── package.json
├── src
│ ├── index.ts // 入口导出文件
│ ├── configuration.ts // 组件行为配置
│ └── service // 逻辑代码
│ └── bookService.ts
├── test
├── index.d.ts // 组件扩展定义
└── tsconfig.json
对于组件来说,唯一的规范是入口导出的 Configuration
属性,其必须是一个带有 @Configuration
装饰器的 Class。
一般来说,我们的代码为 TypeScript 标准目录结构,和 Midway 体系相同。
同时,又是一个普通的 Node.js 包,需要使用 src/index.ts
文件作为入口导出内容。
下面,我们以一个非常简单的示例来演示如何编写一个组件。
组件生命周期
和应用相同,组件也使用 src/configuration.ts
作为入口启动文件(或者说,应用就是一个大组件)。
其中的代码和应用完全相同。
// src/configuration.ts
import { Configuration } from '@midwayjs/core';
@Configuration({
namespace: 'book'
})
export class BookConfiguration {
async onReady() {
// ...
}
}
唯一不同的是,你需要加一个 namespace
作为组件的命名空间。
每个组件的代码是一个独立的作用域,这样即使导出同名的类,也不会和其他组件冲突。
和整个 Midway 通用的 生命周期扩展 能力相同。
组件逻辑代码
和应用相同,编写类导出即可,由依赖注入容器负责管理和加载。
// src/service/book.service.ts
import { Provide } from '@midwayjs/core';
@Provide()
export class BookService {
async getBookById() {
return {
data: 'hello world',
}
}
}
一个组件不会依赖明确的上层框架,为了达到在不同场景复用的目的,只会依赖通用的 @midwayjs/core
。
组件配置
配置和应用相同,参考 多环境配置。
// src/configuration.ts
import { Configuration } from '@midwayjs/core';
import * as DefaultConfig from './config/config.default';
import * as LocalConfig from './config/config.local';
@Configuration({
namespace: 'book',
importConfigs: [
{
default: DefaultConfig,
local: LocalConfig
}
]
})
export class BookConfiguration {
async onReady() {
// ...
}
}
在 v3 有一个重要的特性,组件在加载后,MidwayConfig
定义中就会包含该组件配置的定义。
为此,我们需要独立编写配置的定义。
在根目录下的 index.d.ts
中增加配置定义。
// 由于修改了默认的类型导出位置,需要额外导出 dist 下的类型
export * from './dist/index';
// 标准的扩展声明
declare module '@midwayjs/core/dist/interface' {
// 将配置合并到 MidwayConfig 中
interface MidwayConfig {
book?: {
// ...
};
}
}
同时,组件的 package.json
也有对应的修改。
{
"name": "****",
"main": "dist/index.js",
"typings": "index.d.ts", // 这里的类型导出文件使用项目根目录的
// ...
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"index.d.ts" // 发布时需要额外带上这个文件
],
}
组件约定
组件和应用本身略微有些不同,差异主要在以下几个方面。
- 1、组件的代码需要导出一个
Configuration
属性,其 必须是一个带有@Configuration
装饰器的 Class,用于配置组件自身能力 - 2、所有 **显式导出的代码 **才会被依赖注入容器加载,简单来说,所有 被装饰器装饰 的类都需要导出,包括控制器,服务,中间件等等
比如:
// src/index.ts
export { BookConfiguration as Configuration } from './configuration';
export * from './service/book.service';
这样项目中只有 service/book.service.ts
这个文件才会被依赖注入容器扫描和加载。
以及在 package.json
中指定 main 路径。
"main": "dist/index"
这样组件就可以被上层场景依赖加载了。
测试组件
测试单独某个服务,可以通过启动一个空的业务,指定当前组件来执行。
import { createLightApp } from '@midwayjs/mock';
import * as custom from '../src';
describe('/test/index.test.ts', () => {
it('test component', async () => {
const app = await createLightApp('', {
imports: [
custom
]
});
const bookService = await app.getApplicationContext().getAsync(custom.BookService);
expect(await bookService.getBookById()).toEqual('hello world');
});
});
如果组件是 Http 协议流程中的一部分,强依赖 context,必须依赖某个 Http 框架,那么,请使用一个完整的项目示例,使用 createApp
来测试。
import { createApp, createHttpRequest } from '@midwayjs/mock';
import * as custom from '../src';
describe('/test/index.test.ts', () => {
it('test component', async () => {
// 在示例项目中,需要自行依赖 @midwayjs/koa 或其他对等框架
const app = await createApp(join(__dirname, 'fixtures/base-app'), {
imports: [
custom
]
});
const result = await createHttpRequest(app).get('/');
// ...
});
});