组件与依赖注入
Cell 应用的每个部分都是组件:由 DI 容器创建、装配和持有的类。控制器、服务、中间件和 RPC 服务都是组件。容器是 Cell 自己实现的(@celljs/core),不依赖第三方 IoC 库。
声明与注入
import { Autowired, Component, Value } from '@celljs/core';
@Component()
export class TodoRepository {
protected readonly todos: string[] = [];
add(title: string) { this.todos.push(title); }
all() { return [...this.todos]; }
}
@Component()
export class TodoService {
@Autowired() // 按声明的类型注入
protected readonly repository: TodoRepository;
@Value('app.todos.max') // 配置,见 cell.yml
protected readonly max: number;
add(title: string) {
if (this.repository.all().length >= this.max) {
throw new Error('Too many todos');
}
this.repository.add(title);
}
}- 组件默认是单例:每个应用一个实例。
@Component({ scope: Scope.Transient })每次注入都创建新实例。 - 可以注入到属性(如上)或构造器参数(
constructor(@Autowired(TodoRepository) protected readonly repository: TodoRepository))。没有装饰器的构造器参数按声明的类注入。 - 文件必须被某个 DI 模块导入,否则类不会被登记。
@Value('key')读取合并后的cell.yml;不带键的@Value()以属性名为键。
接口:同名的 Symbol
TypeScript 的接口在运行时不存在,所以接口的标识是一个同名导出的 Symbol。这是整个 Cell 的约定:
// src/common/mailer.ts
export const Mailer = Symbol('Mailer');
export interface Mailer {
send(to: string, text: string): Promise<void>;
}// src/node/mailer-impl.ts
import { Component } from '@celljs/core';
import { Mailer } from '../common/mailer';
@Component(Mailer)
export class MailerImpl implements Mailer {
async send(to: string, text: string): Promise<void> { /* ... */ }
}
// 使用处
@Autowired(Mailer)
protected readonly mailer: Mailer;@Component(Mailer) 把类同时登记在这个 symbol 和类本身之下,两者取到的是同一个实例。@Component({ id: [A, B] }) 登记更多标识。
多个实现
| 需求 | 写法 |
|---|---|
| 拿到全部实现 | @Autowired(Handler) protected readonly handlers: Handler[](数组类型) |
| 允许没有 | 加 @Optional():单值为 undefined,数组为 [] |
| 按名字选 | @Component({ id: Store, name: 'redis' }) 与 @Autowired(Store) @Named('redis') |
| 按标签选 | @Component({ id: Store, tag: { tag: 'region', value: 'eu' } }) 与 @Autowired(Store) @Tagged('region', 'eu') |
| 多个里有一个默认 | @Component({ id: Store, default: true }):普通的 @Autowired(Store) 拿到它 |
| 替换框架的组件 | @Component({ id: ErrorHandler, rebind: true }):移除该标识之前的绑定 |
| 多个、排序、延迟创建 | @AutowiredProvider(Step) protected readonly steps: Provider<Step>,再 this.steps.sortSync()(priority 大的在前) |
框架的扩展点(Middleware、ErrorHandler、ApplicationLifecycle、HealthIndicator……)都是这样的多实现 symbol:在类上声明 @Component(Middleware) 就是新增一个实现,rebind: true 则替换内置实现。
生命周期
import { Application, ApplicationLifecycle, Component, PostConstruct } from '@celljs/core';
@Component(ApplicationLifecycle)
export class Warmup implements ApplicationLifecycle<Application> {
initialize(): void { /* 同步执行,在所有 onStart 之前 */ }
async onStart(app: Application): Promise<void> { /* 逐个 await */ }
onStop(app: Application): void { /* 停止时,以及 cell dev 每次重载之前 */ }
}
@Component()
export class Cache {
@PostConstruct()
protected init(): void { /* 属性注入完成后,每个实例一次 */ }
}initialize、onStart、onStop 抛出的错误只记日志,不影响其他生命周期。不能在模块加载时做的事(打开连接、定时器)放在 onStart,不要放在构造器:cell check 会创建组件,但不启动应用。
日志
import { Autowired, Component, Logger } from '@celljs/core';
@Component()
export class TodoService {
@Autowired(Logger)
protected readonly logger: Logger;
add(title: string) {
this.logger.info(`added ${title}`);
}
}级别来自 logger.level(verbose、debug、info(默认)、warn、error)。@celljs/logger 把控制台日志换成 winston。
切面(AOP)
切面在 proxy: true 的组件的方法前后执行代码(控制器和 RPC 服务默认被代理):
import { Aspect, MethodBeforeAdvice } from '@celljs/core';
@Aspect({ id: MethodBeforeAdvice, pointcut: 'Audited' })
export class AuditAdvice implements MethodBeforeAdvice {
async before(method: string | number | symbol, args: any[], target: any): Promise<void> {
console.log('call', String(method), args);
}
}
@Component({ proxy: true, sysTags: ['Audited'] })
export class AccountService { /* ... */ }AfterReturningAdvice 和 AfterThrowsAdvice 在方法返回或抛错后执行。不写 pointcut 的通知作用于所有被代理的组件。被代理的方法总是返回 Promise,同步方法也一样。aop.enabled: false 关闭代理。
容器之外
没有全局容器。运行时才决定取哪个组件的组件注入容器本身(@Autowired(Container));不是组件的代码(脚本、测试)用自己创建的容器:ApplicationFactory.createContainer(props, ...modules)(@celljs/core/lib/common/application/application-factory)返回它,createTestApp() 有 app.get()。{ detached: true } 的属性从创建实例的容器里取值,所以只对组件有效。
装配失败时
容器把装配问题报成结构化错误,npx cell check 不启动应用就能发现它们:
| 错误码 | 含义 | 通常的修法 |
|---|---|---|
CELL_DI_NO_BINDING | 某个标识没有任何绑定 | 在 DI 模块里导入该文件;加 @Component(Id);安装对应的包 |
CELL_DI_AMBIGUOUS | 单值注入匹配到多个绑定 | 改为注入数组,或用 @Named / @Tagged / default: true,或 rebind: true |
CELL_DI_CIRCULAR | 组件在创建过程中互相依赖 | 让一方延迟获取(@AutowiredProvider,或属性上的 @Autowired({ detached: true })),或把共用部分移到另一个组件 |
CELL_DI_MISSING_INJECT | 构造器参数没有标识 | 加 @Autowired(Id) |
错误信息里有缺少的标识、谁需要它以及解析链。每个装饰器的详细说明见 core 文档。