Skip to content

组件与依赖注入 ​

Cell 应用的每个部分都是组件:由 DI 容器创建、装配和持有的类。控制器、服务、中间件和 RPC 服务都是组件。容器是 Cell 自己实现的(@celljs/core),不依赖第三方 IoC 库。

声明与注入 ​

ts
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 的约定:

ts
// src/common/mailer.ts
export const Mailer = Symbol('Mailer');

export interface Mailer {
    send(to: string, text: string): Promise<void>;
}
ts
// 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 则替换内置实现。

生命周期 ​

ts
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 会创建组件,但不启动应用。

日志 ​

ts
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 服务默认被代理):

ts
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 文档。