您现在的位置是:网站首页 > Angular中的依赖注入模式文章详情

Angular中的依赖注入模式

依赖注入的基本概念

依赖注入(Dependency Injection,简称DI)是一种设计模式,它允许类从外部源接收依赖项而不是自己创建它们。在Angular中,DI系统负责创建应用程序所需的依赖项实例,并在需要时将它们注入到组件、服务或其他对象中。这种方式提高了代码的可测试性和可维护性,因为依赖关系更加明确且易于替换。

// 一个简单的服务示例
@Injectable({
  providedIn: 'root'
})
export class DataService {
  fetchData() {
    return ['data1', 'data2', 'data3'];
  }
}

Angular中的依赖注入系统

Angular的DI系统是整个框架的核心部分。它通过注入器(Injector)来管理依赖项的创建和注入。每个Angular应用都有一个根注入器,模块和组件也可以有自己的注入器,形成层次结构。这种层次结构使得依赖项的查找和解析更加高效。

// 组件中注入服务
@Component({
  selector: 'app-example',
  templateUrl: './example.component.html',
  styleUrls: ['./example.component.css']
})
export class ExampleComponent {
  constructor(private dataService: DataService) {}

  getData() {
    return this.dataService.fetchData();
  }
}

提供者的配置方式

在Angular中,可以通过多种方式配置提供者(Provider),这决定了依赖项如何被创建和注入。最常见的方式是在@Injectable装饰器中指定providedIn属性,或者直接在模块或组件的providers数组中声明。

// 在模块中提供服务的两种方式
@NgModule({
  providers: [DataService], // 方式一
  // 或者
  providers: [
    { provide: DataService, useClass: DataService } // 方式二
  ]
})
export class AppModule { }

依赖注入的层次结构

Angular的注入器形成树状结构,从根注入器开始,向下延伸到各个组件。当请求一个依赖项时,Angular会先在当前组件的注入器中查找,如果没有找到,就会向上级注入器查找,直到根注入器。这种机制使得依赖项的共享和隔离变得灵活。

// 组件级提供者示例
@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  providers: [DataService] // 这个服务实例只对该组件及其子组件可用
})
export class ChildComponent {
  constructor(private dataService: DataService) {}
}

使用@Injectable装饰器

@Injectable装饰器用于标记一个类可以被注入器实例化。虽然技术上任何类都可以被注入,但使用@Injectable装饰器是一个最佳实践,特别是当服务本身有依赖项需要注入时。

@Injectable({
  providedIn: 'root'
})
export class LoggerService {
  log(message: string) {
    console.log(`[LOG]: ${message}`);
  }
}

@Injectable({
  providedIn: 'root'
})
export class AdvancedDataService {
  constructor(private logger: LoggerService) {}

  fetchData() {
    this.logger.log('Fetching data...');
    return ['advanced1', 'advanced2'];
  }
}

可选依赖和@Optional装饰器

有时依赖项是可选的,Angular提供了@Optional装饰器来处理这种情况。如果被标记为@Optional的依赖项不存在,Angular不会抛出错误,而是会注入null

import { Optional } from '@angular/core';

@Component({
  selector: 'app-optional',
  template: '<p>Optional dependency example</p>'
})
export class OptionalComponent {
  constructor(@Optional() private optionalService: OptionalService) {
    if (this.optionalService) {
      this.optionalService.doSomething();
    } else {
      console.log('Optional service not available');
    }
  }
}

多提供者和@Inject装饰器

Angular支持为同一个令牌注册多个提供者,这在需要提供多种实现时非常有用。@Inject装饰器可以用来明确指定要注入的依赖项,特别是在使用非类令牌时。

const API_URL = new InjectionToken<string>('api.url');

@NgModule({
  providers: [
    { provide: API_URL, useValue: 'https://api.example.com' }
  ]
})
export class AppModule { }

@Component({
  selector: 'app-api',
  template: '<p>API Component</p>'
})
export class ApiComponent {
  constructor(@Inject(API_URL) private apiUrl: string) {
    console.log('API URL:', this.apiUrl);
  }
}

工厂提供者和useFactory

当依赖项的创建需要复杂逻辑时,可以使用工厂提供者。useFactory允许你指定一个工厂函数来创建依赖项实例,并且可以注入其他依赖项作为工厂函数的参数。

@Injectable()
export class ConfigService {
  constructor() {}
  getConfig() {
    return { timeout: 3000 };
  }
}

@NgModule({
  providers: [
    ConfigService,
    {
      provide: 'TIMEOUT',
      useFactory: (config: ConfigService) => config.getConfig().timeout,
      deps: [ConfigService]
    }
  ]
})
export class AppModule { }

@Component({
  selector: 'app-factory',
  template: '<p>Factory Example</p>'
})
export class FactoryComponent {
  constructor(@Inject('TIMEOUT') private timeout: number) {
    console.log('Timeout:', timeout);
  }
}

值提供者和useValue

对于简单的值或已经存在的实例,可以使用useValue提供者。这种方式常用于提供配置对象或全局常量。

const APP_CONFIG = {
  title: 'My App',
  version: '1.0.0'
};

@NgModule({
  providers: [
    { provide: 'APP_CONFIG', useValue: APP_CONFIG }
  ]
})
export class AppModule { }

@Component({
  selector: 'app-config',
  template: '<p>Config Example</p>'
})
export class ConfigComponent {
  constructor(@Inject('APP_CONFIG') private config: any) {
    console.log('App Title:', config.title);
  }
}

类提供者和useClass

useClass提供者允许你指定一个类作为提供者,当请求某个令牌时,Angular会实例化这个类。这在需要提供不同实现时特别有用,比如在测试时替换真实服务。

abstract class StorageService {
  abstract save(data: any): void;
}

@Injectable()
export class LocalStorageService extends StorageService {
  save(data: any) {
    localStorage.setItem('data', JSON.stringify(data));
  }
}

@Injectable()
export class MockStorageService extends StorageService {
  save(data: any) {
    console.log('Mock save:', data);
  }
}

@NgModule({
  providers: [
    { provide: StorageService, useClass: environment.production ? LocalStorageService : MockStorageService }
  ]
})
export class AppModule { }

别名提供者和useExisting

useExisting提供者允许你为一个已有的提供者创建别名。当请求别名时,Angular会返回原始提供者的实例。

@Injectable()
export class PrimaryService {
  doWork() {
    console.log('Primary work');
  }
}

@Injectable()
export class SecondaryService {
  constructor(private primary: PrimaryService) {}

  doWork() {
    this.primary.doWork();
    console.log('Secondary work');
  }
}

@NgModule({
  providers: [
    PrimaryService,
    SecondaryService,
    { provide: 'MAIN_SERVICE', useExisting: PrimaryService }
  ]
})
export class AppModule { }

@Component({
  selector: 'app-alias',
  template: '<p>Alias Example</p>'
})
export class AliasComponent {
  constructor(
    @Inject('MAIN_SERVICE') private mainService: PrimaryService,
    private secondary: SecondaryService
  ) {
    this.mainService.doWork();
    this.secondary.doWork();
  }
}

依赖注入在测试中的应用

依赖注入模式极大地简化了单元测试的编写。在测试中,可以轻松地用模拟对象替换真实的服务,从而隔离被测组件。

describe('ExampleComponent', () => {
  let component: ExampleComponent;
  let fixture: ComponentFixture<ExampleComponent>;
  let mockDataService: jasmine.SpyObj<DataService>;

  beforeEach(async () => {
    mockDataService = jasmine.createSpyObj('DataService', ['fetchData']);
    mockDataService.fetchData.and.returnValue(['mock1', 'mock2']);

    await TestBed.configureTestingModule({
      declarations: [ExampleComponent],
      providers: [
        { provide: DataService, useValue: mockDataService }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(ExampleComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should get mock data from service', () => {
    expect(component.getData()).toEqual(['mock1', 'mock2']);
    expect(mockDataService.fetchData).toHaveBeenCalled();
  });
});

依赖注入的性能考虑

虽然依赖注入带来了许多好处,但也需要注意性能问题。Angular的DI系统在启动时需要解析所有的依赖关系,这可能会影响应用的初始加载时间。对于大型应用,可以考虑使用懒加载模块来分散这种开销。

// 懒加载模块中的服务提供
@Injectable({
  providedIn: 'any' // 为每个注入器创建新实例
})
export class LazyService {
  constructor() {
    console.log('LazyService instantiated');
  }
}

@NgModule({
  providers: [LazyService]
})
export class LazyModule { }

依赖注入与单例模式

默认情况下,在根模块或使用providedIn: 'root'注册的服务是单例的,整个应用共享同一个实例。但在组件级提供的服务,每个组件实例都会获得自己的服务实例。

@Injectable({
  providedIn: 'root'
})
export class SingletonService {
  instanceId = Math.random();
}

@Component({
  selector: 'app-singleton',
  template: '<p>Instance ID: {{service.instanceId}}</p>',
  providers: [SingletonService] // 这里会覆盖根提供者
})
export class SingletonComponent {
  constructor(public service: SingletonService) {}
}

依赖注入与循环依赖

循环依赖是指两个或多个服务相互依赖的情况。Angular的DI系统可以处理简单的循环依赖,但复杂的循环依赖可能导致运行时错误。最佳实践是重新设计服务结构以避免循环依赖。

// 不推荐的做法:循环依赖
@Injectable()
export class ServiceA {
  constructor(private b: ServiceB) {}
}

@Injectable()
export class ServiceB {
  constructor(private a: ServiceA) {}
}

// 推荐的做法:使用中间服务或观察者模式
@Injectable()
export class MediatorService {
  // 解决循环依赖
}

依赖注入与动态组件

在动态创建组件时,也需要考虑依赖注入。Angular的ComponentFactoryResolverViewContainerRef可以用于动态创建组件,同时保持依赖注入的工作。

@Component({
  selector: 'app-dynamic',
  template: '<ng-template #container></ng-template>'
})
export class DynamicComponent {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;

  constructor(
    private resolver: ComponentFactoryResolver,
    private injector: Injector
  ) {}

  createDynamicComponent() {
    const factory = this.resolver.resolveComponentFactory(DynamicChildComponent);
    this.container.createComponent(factory, 0, this.injector);
  }
}

@Component({
  selector: 'app-dynamic-child',
  template: '<p>Dynamic Child</p>'
})
export class DynamicChildComponent {
  constructor(private dataService: DataService) {
    console.log('Dynamic child created with data:', this.dataService.fetchData());
  }
}

依赖注入与第三方库集成

当集成第三方库时,可能需要将这些库的服务注入到Angular应用中。可以通过创建包装服务或使用@Inject装饰器来实现。

// 假设有一个第三方库的全局服务
declare const ThirdPartyLib: {
  service: {
    doSomething: () => void;
  };
};

@Injectable()
export class ThirdPartyWrapper {
  doSomething() {
    ThirdPartyLib.service.doSomething();
  }
}

// 或者直接注入全局对象
@Component({
  selector: 'app-third-party',
  template: '<p>Third Party Integration</p>'
})
export class ThirdPartyComponent {
  constructor(@Inject('ThirdPartyLib') private thirdParty: any) {
    this.thirdParty.service.doSomething();
  }
}

@NgModule({
  providers: [
    ThirdPartyWrapper,
    { provide: 'ThirdPartyLib', useValue: ThirdPartyLib }
  ]
})
export class AppModule { }

依赖注入与AOT编译

Angular的AOT(Ahead-of-Time)编译会对依赖注入产生影响。在AOT模式下,所有的依赖关系必须在编译时确定,不能使用动态的依赖关系。这要求开发者在编写代码时更加明确依赖关系。

// 以下代码在AOT模式下会失败
function getService() {
  return DataService;
}

@NgModule({
  providers: [
    { provide: SomeToken, useClass: getService() } // 运行时才能确定
  ]
})
export class AppModule { }

// 正确的AOT兼容写法
@NgModule({
  providers: [
    { provide: SomeToken, useClass: DataService }
  ]
})
export class AppModule { }

依赖注入与变更检测

依赖注入与Angular的变更检测系统密切相关。服务可以用于在组件之间共享状态,而状态变化会触发变更检测。理解这种关系对于优化应用性能很重要。

@Injectable()
export class StateService {
  private _data = new BehaviorSubject<string>('initial');
  data$ = this._data.asObservable();

  updateData(newValue: string) {
    this._data.next(newValue);
  }
}

@Component({
  selector: 'app-producer',
  template: '<button (click)="update()">Update Data</button>'
})
export class ProducerComponent {
  constructor(private state: StateService) {}

  update() {
    this.state.updateData('new value');
  }
}

@Component({
  selector: 'app-consumer',
  template: '<p>Current value: {{value}}</p>'
})
export class ConsumerComponent implements OnInit {
  value: string;

  constructor(private state: StateService) {}

  ngOnInit() {
    this.state.data$.subscribe(val => {
      this.value = val;
    });
  }
}

依赖注入与路由守卫

Angular的路由守卫也利用了依赖注入系统。开发者可以创建自定义守卫并注入所需的服务来实现复杂的路由逻辑。

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean | Observable<boolean> | Promise<boolean> {
    if (this.authService.isAuthenticated()) {
      return true;
    } else {
      this.router.navigate(['/login']);
      return false;
    }
  }
}

const routes: Routes = [
  {
    path: 'protected',
    component: ProtectedComponent,
    canActivate: [AuthGuard]
  }
];

依赖注入与HTTP拦截器

HTTP拦截器是Angular中利用依赖注入实现横切关注点的典型例子。多个拦截器可以组成管道,每个拦截器都能处理请求和响应。

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authToken = this.auth.getToken();
    const authReq = req.clone({
      headers: req.headers.set('Authorization', `Bearer ${authToken}`)
    });
    return next.handle(authReq);
  }
}

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    console.log('Outgoing request:', req);
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          console.log('Incoming response:', event);
        }
      })
    );
  }
}

@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true }
  ]
})
export class AppModule { }

依赖注入与表单验证

依赖注入可以用于创建可重用的表单验证器。这些验证器可以注入服务来执行复杂的验证逻辑。

@Injectable()
export class UniqueUsernameValidator {
  constructor(private userService: UserService) {}

  validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
    return this.userService.checkUsername(control.value).pipe(
      map(isTaken => (isTaken ? { usernameTaken: true } : null)),
      catchError(() => of(null))
    );
  }
}

@Component({
  selector: 'app-signup',
  template: `
    <form [formGroup]="form">
      <input formControlName="username">
      <div *ngIf="form.get('username').errors?.usernameTaken">
        Username is already taken
      </div>
    </form>
  `
})
export class SignupComponent implements OnInit {
  form: FormGroup;

  constructor(
    private fb: FormBuilder,
    private validator: UniqueUsernameValidator
  ) {}

  ngOnInit() {
    this.form = this.fb.group({
      username: ['', null, [this.validator.validate.bind(this.validator)]]
    });
  }
}

依赖注入与国际化

依赖注入可以用于实现国际化功能,通过注入不同的翻译服务来支持多语言。

@Injectable()
export class TranslationService {
  private currentLang = new BehaviorSubject<string>('en');
  private translations = {
    en: { greeting: 'Hello' },
    fr: { greeting: 'Bonjour' }
  };

  setLanguage(lang: string) {
    this.currentLang.next(lang);
  }

  getTranslation(key: string): Observable<string> {
    return this.currentLang.pipe(
      map(lang => this.translations[lang][key])
    );
  }
}

@Component({
  selector: 'app-greeting',
  template: '<p>{{greeting}}</p>'
})
export class GreetingComponent implements OnInit {
  greeting: string;

  constructor(private translation: TranslationService) {}

  ngOnInit() {
    this.translation.getTranslation('greeting').subscribe(text => {
      this.greeting = text;
    });
  }
}

依赖注入与状态管理

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步