JavaScript 依赖注入:大型应用的救星 (Angular/NestJS 实践)
大家好!欢迎来到今天的 JavaScript 依赖注入 (DI) 讲座。我是你们的老朋友,江湖人称“代码老中医”,专治各种大型应用“代码臃肿、难以测试、牵一发动全身”的疑难杂症。今天,咱们就来聊聊 DI 这个能让你的代码变得更加灵活、可维护、可测试的“灵丹妙药”。
什么是依赖? 什么是依赖注入?
在开始之前,我们先来搞清楚什么是“依赖”。
想象一下你开了一家咖啡馆,需要各种原料才能制作咖啡,比如咖啡豆、牛奶、糖浆等等。你的咖啡馆就“依赖”这些原料才能正常运转。如果哪天咖啡豆供应商罢工了,你的咖啡馆就没法制作咖啡了,这就是“依赖”带来的问题。
在编程世界里,“依赖”指的是一个类或模块需要另一个类或模块才能正常工作。例如,一个 UserService
类可能需要 UserRepository
类来访问数据库。
class UserRepository {
getUserById(id) {
// 从数据库获取用户
return { id: id, name: '张三' };
}
}
class UserService {
constructor() {
this.userRepository = new UserRepository(); // UserService 依赖 UserRepository
}
getUser(id) {
return this.userRepository.getUserById(id);
}
}
const userService = new UserService();
const user = userService.getUser(1);
console.log(user); // { id: 1, name: '张三' }
上面的例子中,UserService
直接创建了 UserRepository
的实例,这就产生了紧耦合。如果有一天你想换一个 UserRepository
的实现,比如使用不同的数据库,你就需要修改 UserService
的代码。
这时候,依赖注入就闪亮登场了!它就像一个“媒婆”,负责把 UserService
需要的 UserRepository
“嫁”给它,而不是让 UserService
自己去“找对象”。
依赖注入 (Dependency Injection, DI) 的定义
DI 是一种设计模式,其核心思想是:不要让类自己去创建或查找它所依赖的对象,而是通过外部容器将依赖项“注入”到类中。
简单来说,就是把“创建依赖”的责任从类自身转移到外部,让类只专注于自己的业务逻辑。
DI 的好处:
- 解耦合 (Decoupling): 类不再直接依赖于具体的实现,而是依赖于接口或抽象类。这使得你可以轻松地替换不同的实现,而无需修改类的代码。
- 可测试性 (Testability): 可以方便地使用 Mock 对象或 Stub 对象来模拟依赖项,从而更容易地对类进行单元测试。
- 可维护性 (Maintainability): 代码更加模块化,易于理解和修改。
- 可复用性 (Reusability): 类可以被不同的应用程序或模块复用。
DI 的实现方式:
DI 主要有三种实现方式:
- 构造函数注入 (Constructor Injection): 通过类的构造函数来注入依赖项。这是最常用的方式。
- Setter 注入 (Setter Injection): 通过 Setter 方法来注入依赖项。
- 接口注入 (Interface Injection): 通过接口定义注入方法来注入依赖项 (不常用)。
接下来,我们用代码示例来演示这三种方式。
构造函数注入 (Constructor Injection)
// 定义 UserRepository 接口
class IUserRepository {
getUserById(id) {
throw new Error('Method not implemented.');
}
}
// UserRepository 的具体实现
class UserRepository extends IUserRepository {
getUserById(id) {
// 从数据库获取用户
return { id: id, name: '张三 (来自数据库)' };
}
}
// UserService 使用构造函数注入 UserRepository
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
getUser(id) {
return this.userRepository.getUserById(id);
}
}
// 创建 UserRepository 实例
const userRepository = new UserRepository();
// 通过构造函数注入 UserRepository 到 UserService
const userService = new UserService(userRepository);
const user = userService.getUser(1);
console.log(user); // { id: 1, name: '张三 (来自数据库)' }
// 单元测试示例:使用 Mock UserRepository
class MockUserRepository extends IUserRepository {
getUserById(id) {
return { id: id, name: '测试用户 (来自 Mock)' };
}
}
const mockUserRepository = new MockUserRepository();
const userServiceWithMock = new UserService(mockUserRepository);
const userFromMock = userServiceWithMock.getUser(1);
console.log(userFromMock); // { id: 1, name: '测试用户 (来自 Mock)' }
Setter 注入 (Setter Injection)
// 定义 UserRepository 接口
class IUserRepository {
getUserById(id) {
throw new Error('Method not implemented.');
}
}
// UserRepository 的具体实现
class UserRepository extends IUserRepository {
getUserById(id) {
// 从数据库获取用户
return { id: id, name: '李四 (来自数据库)' };
}
}
// UserService 使用 Setter 注入 UserRepository
class UserService {
constructor() {
this.userRepository = null;
}
setUserRepository(userRepository) {
this.userRepository = userRepository;
}
getUser(id) {
if (!this.userRepository) {
throw new Error('UserRepository 未设置');
}
return this.userRepository.getUserById(id);
}
}
// 创建 UserRepository 实例
const userRepository = new UserRepository();
// 创建 UserService 实例
const userService = new UserService();
// 使用 Setter 方法注入 UserRepository
userService.setUserRepository(userRepository);
const user = userService.getUser(2);
console.log(user); // { id: 2, name: '李四 (来自数据库)' }
接口注入 (Interface Injection)
// 定义注入接口
class IUserRepositoryInjector {
setUserRepository(userRepository) {
throw new Error('Method not implemented.');
}
}
// 定义 UserRepository 接口
class IUserRepository {
getUserById(id) {
throw new Error('Method not implemented.');
}
}
// UserRepository 的具体实现
class UserRepository extends IUserRepository {
getUserById(id) {
// 从数据库获取用户
return { id: id, name: '王五 (来自数据库)' };
}
}
// UserService 实现了注入接口
class UserService extends IUserRepositoryInjector {
constructor() {
super();
this.userRepository = null;
}
setUserRepository(userRepository) {
this.userRepository = userRepository;
}
getUser(id) {
if (!this.userRepository) {
throw new Error('UserRepository 未设置');
}
return this.userRepository.getUserById(id);
}
}
// 创建 UserRepository 实例
const userRepository = new UserRepository();
// 创建 UserService 实例
const userService = new UserService();
// 使用接口方法注入 UserRepository
userService.setUserRepository(userRepository);
const user = userService.getUser(3);
console.log(user); // { id: 3, name: '王五 (来自数据库)' }
依赖注入容器 (Dependency Injection Container)
在大型应用中,手动管理依赖项会变得非常繁琐。这时候,我们就需要一个“依赖注入容器”来帮助我们管理依赖项的创建和注入。
依赖注入容器是一个框架或库,它可以自动地创建和注入依赖项。它通常提供以下功能:
- 注册 (Registration): 注册类及其依赖项。
- 解析 (Resolution): 根据类的依赖关系,自动创建实例并注入依赖项。
- 生命周期管理 (Lifecycle Management): 管理依赖项的生命周期,例如单例模式、瞬态模式等。
Angular 中的依赖注入
Angular 框架内置了强大的依赖注入系统。它使用 TypeScript 的装饰器 (Decorators) 来声明依赖关系,并使用依赖注入容器来管理依赖项。
import { Injectable } from '@angular/core';
// 定义 UserService
@Injectable({
providedIn: 'root' // 声明 UserService 为可注入的服务,并指定在根模块中提供
})
export class UserService {
constructor(private userRepository: UserRepository) { // 构造函数注入 UserRepository
}
getUser(id: number) {
return this.userRepository.getUserById(id);
}
}
// 定义 UserRepository
@Injectable({
providedIn: 'root' // 声明 UserRepository 为可注入的服务,并指定在根模块中提供
})
export class UserRepository {
getUserById(id: number) {
// 从数据库获取用户
return { id: id, name: '赵六 (来自 Angular)' };
}
}
// 在组件中使用 UserService
import { Component } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user',
template: `
<p>User ID: {{ user?.id }}</p>
<p>User Name: {{ user?.name }}</p>
`
})
export class UserComponent {
user: any;
constructor(private userService: UserService) { // 构造函数注入 UserService
this.user = this.userService.getUser(4);
}
}
Angular DI 的关键概念:
@Injectable()
装饰器: 用于声明一个类为可注入的服务。providedIn
属性指定了服务在哪个模块中提供。'root'
表示在根模块中提供,这意味着整个应用程序都可以访问该服务。- 构造函数注入: Angular 使用构造函数注入来自动注入依赖项。
providers
数组: 在模块或组件的providers
数组中可以指定依赖项的提供者。useClass
,useValue
,useFactory
,useExisting
: 这些属性用于配置依赖项的提供方式。
属性 | 描述 | 示例 |
---|---|---|
useClass |
使用一个类来提供依赖项。这是最常用的方式。 | typescript providers: [{ provide: IUserRepository, useClass: UserRepository }] |
useValue |
使用一个静态值来提供依赖项。 | typescript providers: [{ provide: 'API_URL', useValue: 'https://api.example.com' }] |
useFactory |
使用一个工厂函数来提供依赖项。这可以用于创建复杂的依赖项或根据条件创建不同的依赖项。 | typescript providers: [{ provide: UserService, useFactory: (userRepository: UserRepository) => { return new UserService(userRepository); }, deps: [UserRepository] }] 注意 deps 数组指定了工厂函数所需的依赖项。 |
useExisting |
使用一个已存在的令牌来提供依赖项。这可以用于创建一个别名或将一个依赖项绑定到另一个依赖项。 | typescript providers: [{ provide: AnotherService, useExisting: UserService }] AnotherService 现在会指向 UserService 的实例。 |
NestJS 中的依赖注入
NestJS 是一个基于 Node.js 的渐进式框架,用于构建高效、可靠和可扩展的服务器端应用程序。它深受 Angular 的影响,也内置了强大的依赖注入系统。
import { Injectable, Inject } from '@nestjs/common';
// 定义 UserService
@Injectable()
export class UserService {
constructor(
@Inject('UserRepository') private readonly userRepository: IUserRepository, // 构造函数注入 UserRepository,使用 @Inject 装饰器指定令牌
) {}
getUser(id: number) {
return this.userRepository.getUserById(id);
}
}
// 定义 UserRepository 接口
export interface IUserRepository {
getUserById(id: number): any;
}
// 定义 UserRepository
@Injectable()
export class UserRepository implements IUserRepository {
getUserById(id: number): any {
// 从数据库获取用户
return { id: id, name: '钱七 (来自 NestJS)' };
}
}
// 在 Module 中注册 providers
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { IUserRepository } from './interfaces/user-repository.interface';
@Module({
providers: [
UserService,
{
provide: 'UserRepository', // 使用令牌 'UserRepository'
useClass: UserRepository,
},
],
exports: [UserService], // 导出 UserService,以便其他模块可以使用
})
export class UserModule {}
// 在 Controller 中使用 UserService
import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
getUser(@Param('id') id: string) {
return this.userService.getUser(parseInt(id));
}
}
NestJS DI 的关键概念:
@Injectable()
装饰器: 用于声明一个类为可注入的服务。@Inject()
装饰器: 用于指定要注入的依赖项的令牌 (Token)。providers
数组: 在模块的providers
数组中可以指定依赖项的提供者。- 令牌 (Token): 用于标识依赖项的唯一标识符。可以是字符串、类或符号 (Symbol)。
useClass
,useValue
,useFactory
: 与 Angular 类似,用于配置依赖项的提供方式。
DI 容器的原理 (简化版):
DI 容器的核心是一个映射表,它存储了令牌 (Token) 和提供者 (Provider) 之间的对应关系。
当需要解析一个依赖项时,DI 容器会根据令牌在映射表中查找对应的提供者,然后使用提供者来创建依赖项的实例并注入到目标类中。
DI 的注意事项:
- 过度使用 DI: DI 是一种强大的工具,但不要过度使用。对于简单的应用,手动管理依赖项可能更简单。
- 循环依赖 (Circular Dependency): 避免出现循环依赖,例如 A 依赖于 B,B 又依赖于 A。DI 容器通常会检测并抛出异常。
- 依赖注入框架的选择: 选择适合你的项目和团队的依赖注入框架。
总结:
依赖注入是一种重要的设计模式,可以提高代码的灵活性、可测试性和可维护性。在大型 JavaScript 应用中,使用依赖注入容器可以更好地管理依赖项,降低代码的复杂性。Angular 和 NestJS 都内置了强大的依赖注入系统,可以帮助你轻松地实现依赖注入。
希望今天的讲座能帮助你更好地理解 JavaScript 中的依赖注入。记住,代码就像人,需要精心呵护才能健康成长。依赖注入就是一种“呵护”代码的方式,让你的代码更加健壮、易于维护,最终为你省下大把的加班时间!
好了,今天的讲座就到这里。谢谢大家!