业务逻辑与依赖注入
zenweb 最核心的设计理念 依赖注入 功能,设计上参考了 Spring 方案并做了一些调整
定义一个最简单的业务类
// 定义并导出类
export class UserService {
getByUsername(username: string) {
return "You name is: " + username;
}
}
在控制器中使用业务类实例
export class UserController {
@Get()
getByService(userService: UserService) { // 只需要在控制器方法参数中标识需要使用的类即可
return userService.getByUsername("Bob");
}
}
在业务类中注入业务类
import { Injectable } from "zenweb";
import { UserService } from "./user_service";
@Injectable // 使用 Injectable 注解标记可注入的类
export class AuthService {
constructor(
private userService: UserService,
) {}
login(username: string) {
return this.userService.getByUsername(username);
}
}
作用域级别
类作用域是指一个类的实例所处的上下文生命周期
zenweb 注入类的作用域一个分为一下三种:
singleton: 单例模式,对象只会初始化一次并一直存活在整个应用生命周期中- 此级别下对象不可注入 request 级别的对象
request: 请求模式,当前请求中只会被初始化一次,同一个请求中上下文中共享同一个实例对象- 被标记为 request 作用域的对象不能注入到 singleton 中
- 默认级别
prototype: 原型模式,每次注入时都会被初始化,每一个注入对象都是新的- 因为每次都新建一个实例,通常情况下不使用
- 任何级别都可以注入此级别的对象
- 控制器的默认级别
默认作用域级别为 request,而并非 singleton,
因为在 web 项目中这更常用,也避免错误的使用 singleton 造成共享变量污染。
修改类的作用域
import { Injectable } from "zenweb";
// 标记类为单例类型
@Injectable("singleton")
export class DbService {
// 通常来说数据库连接只需要一次即可,避免每次请求都连接数据浪费资源
conn = new DatabaseConnection();
query(sql: string) {
return this.conn.query(sql);
}
}
在非业务类中取得类实例
我们知道在业务类中可以通过 @Injectable 或 @Inject 自动注入类实例
如果在 中间件 中,这种非业务类的方法中如何获得一个类实例呢?
其实在 Context 上下文对象中存在一个 injecter 属性,可以通过他来取得需要的对象实例。
function someMiddleware(): Middleware {
return async function (ctx, next) {
// 注意这里一定要使用 await
const someService = await ctx.injector.getInstance(SomeService);
// ...
return next();
}
}
类的异步初始化
依赖注入有以下几点需要注意:
一般情况下推荐使用
@Injectable和类构造器constructor()来注入业务类的依赖,这也是最推荐的方式@Inject依赖注入会先初始化一个类后再设置依赖属性到实例中- 这样就会造成一个问题,在
constructor()构造器中无法调用到有效的依赖属性
- 这样就会造成一个问题,在
一个标准的 js class 是不可以把类构造器
constructor()声明为异步的- 如果你需要在构造器内
await一些方法是不成立的,如果需要异步初始化,可以在类中声明一个方法并使用@Init装饰器来告诉注入器此类需要初始化操作。
- 如果你需要在构造器内
import { Injectable, Init } from "zenweb";
class TargetClass {}
@Injectable
export class SomeClass {
constructor(
private target: TargetClass,
) {}
constructor() {
console.log('constructor: %o', this.target); // 构造器里无法获得依赖类的实例
// output: constructor: undefined
}
// 使用 @Init 装饰器来指定类方法为初始化方法
// 在类需要初始化时依赖注入器会自动调用此方法
@Init
initMethod() {
console.log('initMethod: %o', this.target); // 此时已经可以取得依赖类的实例
// output: initMethod: TargetClass {}
}
// 你甚至可以定义多个初始化方法
// 并且可以使用异步方法
@Init
async otherInitMethod() {
await Promise();
}
// 你也可以像使用控制器方法那样,使用方法参数注入依赖
// 例如这个依赖我只想要在初始化方法内使用,不想暴露出来
@Init
likeControllerMethod(target: TargetClass) {
}
}
@Inject 属性注入
除了通过构造函数参数注入依赖外,还可以使用 @Inject 装饰器直接在类属性上进行注入。
采用这种方式注入的场景:类继承,父类构造器需要传参,不想手动初始化父构造器。
import { Context, Injectable, Inject } from 'zenweb';
@Injectable
class BaseService {
constructor(private someService: SomeService) {}
}
export class AuthService extends BaseService {
// 使用 @Inject 装饰器注入 Context
@Inject ctx!: Context;
async login(name: string, password: string) {
// 在方法中通过 this.ctx 访问请求上下文
const user = await User.find({ name }).get();
if (!user) {
throw new Error('用户名或密码错误');
}
return user;
}
}
@Inject 属性注入是在对象初始化完成之后才设置的,因此在 constructor() 构造函数中无法访问到被 @Inject 标注的属性。如果需要在初始化时使用依赖,请使用 @Init 装饰器或构造函数参数注入。
与构造函数注入的对比
// 方式一:构造函数参数注入(推荐)
@Injectable
export class OrderService {
constructor(
private userService: UserService,
private paymentService: PaymentService,
) {}
}
// 方式二:@Inject 属性注入
@Injectable
export class OrderService {
@Inject userService!: UserService;
@Inject paymentService!: PaymentService;
}
两种方式的效果相同,选择依据:
- 构造函数注入:依赖关系清晰,TypeScript 类型检查完整
@Inject属性注入:适合后期添加依赖时使用,避免修改构造函数签名
在 Service 层使用全局模式 [v4.0+]
从 ZenWeb v4.0 开始,框架引入了全局模式(基于 asyncLocalStorage),你可以在 Service 层直接使用 $ 前缀的全局函数,无需通过 @Inject 注入 ctx,也不需要将 ctx 作为参数层层传递。
import { $query, $log, $mysql, $ctx } from 'zenweb';
export class ChatRoomService {
// 无需注入 ctx,直接使用全局函数
async create(name: string) {
const userId = $ctx.user!.id!;
$log.info('创建聊天室: %s, 操作者: %d', name, userId);
const result = await $mysql.sql`
INSERT INTO chat_room (name, creator_id) VALUES (${name}, ${userId})
`;
return result;
}
async getMessages(roomId: number, limit: number = 50) {
const messages = await $mysql.sql`
SELECT * FROM chat_room_message
WHERE room_id = ${roomId}
ORDER BY created_at DESC
LIMIT ${limit}
`;
return messages;
}
}
全局模式 vs 传统模式对比
import { Context, Inject } from 'zenweb';
export class ChatRoomService {
@Inject ctx!: Context;
async create(name: string) {
const userId = this.ctx.user!.id!;
// ...
}
}
import { $ctx, $log, $mysql } from 'zenweb';
export class ChatRoomService {
async create(name: string) {
const userId = $ctx.user!.id!;
$log.info('创建聊天室: %s', name);
// ...
}
}
使用全局模式需要通过 $initCore() 初始化应用。详细信息请参考 全局模式与 $ 快捷方式。
$getInstance 全局获取实例
$getInstance 提供了一种在任意位置获取注入容器中实例的方式。如果在请求上下文中调用,从请求级容器获取;否则从全局容器获取。
import { $getInstance } from 'zenweb';
import { EmailService } from './email';
import { SmsService } from './sms';
export class NotificationService {
async send(userId: number, message: string) {
// 在方法内部按需获取其他 Service 实例
// 无需通过构造函数提前注入所有可能的依赖
const emailService = await $getInstance(EmailService);
await emailService.send(userId, message);
const smsService = await $getInstance(SmsService);
await smsService.send(userId, message);
}
}
这个特性在以下场景特别有用:
- 依赖关系复杂,不想在构造函数中注入过多参数
- 按条件动态获取实例,避免不必要的初始化开销
- 在非业务类(如工具函数)中需要访问 Service 实例
依赖注入作用域规则说明
ZenWeb 的依赖注入容器采用分层结构,不同作用域之间存在严格的依赖规则,以防止生命周期不匹配导致的问题。
三种作用域
| 作用域 | 生命周期 | 说明 |
|---|---|---|
singleton | 应用启动到关闭 | 全局唯一实例,适合无状态服务 |
request | 单次 HTTP 请求 | 同一请求中共享实例,默认作用域 |
prototype | 每次注入时 | 每次获取都创建新实例 |
依赖规则矩阵
singleton 可以依赖: singleton, prototype
request 可以依赖: singleton, request, prototype
prototype 可以依赖: singleton, request, prototype
常见错误
如果违反了作用域规则,注入器会抛出异常:
Error: 'request' level cannot be injected to 'singleton' level.
这意味着你在一个 singleton 作用域的类中注入了 request 作用域的类,这是不允许的,因为 singleton 类在整个应用生命周期中只创建一次,而 request 类在每次请求结束时会被销毁。
// 错误示例
@Injectable('singleton')
export class CacheManager {
constructor(
private userService: UserService, // UserService 默认是 request 作用域
) {}
}
// 修正方式一:将 UserService 改为 singleton
@Injectable('singleton')
export class UserService {
// 注意:此时不能注入 ctx 等 request 级别的对象
}
// 修正方式二:将 CacheManager 改为 request
@Injectable // 默认 request
export class CacheManager {
constructor(
private userService: UserService,
) {}
}
注入器查找流程
当调用 getInstance() 获取实例时,注入器按以下流程查找:
- 在当前作用域容器中查找是否已存在实例,存在则直接返回
- 检查目标类的作用域是否与当前容器作用域匹配
- 如果不匹配且存在父容器,委托给父容器处理
- 如果不匹配且没有父容器,抛出作用域冲突异常
- 作用域匹配时,创建新实例并缓存(
prototype除外)