MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular核心概念进阶

2023-11-012.3k 阅读

1. 深入理解Angular的模块系统

在Angular中,模块是一种组织代码的方式,它把相关的组件、指令、管道和服务组合在一起。

1.1 NgModule基础

NgModule是Angular的模块定义机制,通过装饰器@NgModule来定义。一个简单的NgModule示例如下:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

这里imports数组指定了该模块依赖的其他模块,declarations数组包含了该模块中定义的组件、指令和管道,bootstrap数组指定了应用的根组件。

1.2 模块类型

  • 根模块:通常是AppModule,它是应用启动时首先加载的模块。根模块负责引导应用,它的bootstrap属性指定了应用的根组件。
  • 特性模块:用于将应用功能划分成不同的特性区域。比如一个电商应用,可能有产品模块、订单模块等。以产品模块为例:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductListComponent } from './product - list.component';
import { ProductDetailComponent } from './product - detail.component';

@NgModule({
  imports: [CommonModule],
  declarations: [ProductListComponent, ProductDetailComponent],
  exports: [ProductListComponent, ProductDetailComponent]
})
export class ProductModule {}

这里exports数组将模块内的组件暴露出去,以便其他模块使用。

  • 共享模块:用于存放多个模块共享的组件、指令和管道。例如,我们创建一个SharedModule
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoadingSpinnerComponent } from './loading - spinner.component';
import { AlertComponent } from './alert.component';

@NgModule({
  imports: [CommonModule],
  declarations: [LoadingSpinnerComponent, AlertComponent],
  exports: [LoadingSpinnerComponent, AlertComponent]
})
export class SharedModule {}

其他模块只需导入SharedModule,就可以使用其中的组件。

1.3 模块的懒加载

懒加载是一种延迟加载模块的技术,能提高应用的初始加载性能。通过在路由配置中使用loadChildren属性实现。

const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => import('./product.module').then(m => m.ProductModule)
  }
];

这样,当用户访问/products路径时,ProductModule才会被加载。

2. 组件交互的高级技巧

组件是Angular应用的基本构建块,组件之间的交互至关重要。

2.1 父子组件交互

  • 父传子:通过@Input()装饰器实现。父组件模板中传递数据:
<app - child - component [inputValue]="parentData"></app - child - component>

子组件定义@Input()属性接收数据:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app - child - component',
  templateUrl: './child - component.html'
})
export class ChildComponent {
  @Input() inputValue: string;
}
  • 子传父:通过@Output()装饰器和EventEmitter实现。子组件触发事件:
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app - child - component',
  templateUrl: './child - component.html'
})
export class ChildComponent {
  @Output() childEvent = new EventEmitter();

  onButtonClick() {
    this.childEvent.emit('Data from child');
  }
}

父组件在模板中监听事件:

<app - child - component (childEvent)="handleChildEvent($event)"></app - child - component>
export class ParentComponent {
  handleChildEvent(data: string) {
    console.log(data);
  }
}

2.2 非父子组件交互

  • 通过服务:创建一个共享服务,不同组件通过注入该服务来共享数据。例如,创建DataService
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private data: string;

  setData(value: string) {
    this.data = value;
  }

  getData() {
    return this.data;
  }
}

组件A设置数据:

import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app - component - a',
  templateUrl: './component - a.html'
})
export class ComponentA {
  constructor(private dataService: DataService) {}

  onSetData() {
    this.dataService.setData('Data from ComponentA');
  }
}

组件B获取数据:

import { Component } from '@angular/core';
import { DataService } from './data.service';

@Component({
  selector: 'app - component - b',
  templateUrl: './component - b.html'
})
export class ComponentB {
  data: string;
  constructor(private dataService: DataService) {
    this.data = this.dataService.getData();
  }
}
  • 通过SubjectBehaviorSubjectSubject是一种可以多播给多个观察者的ObservableBehaviorSubject会保存“当前”值,并将其发送给新的订阅者。以BehaviorSubject为例:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SharedSubjectService {
  private subject = new BehaviorSubject<string>('Initial value');
  public observable$ = this.subject.asObservable();

  updateValue(value: string) {
    this.subject.next(value);
  }
}

组件订阅:

import { Component } from '@angular/core';
import { SharedSubjectService } from './shared - subject.service';

@Component({
  selector: 'app - subscribing - component',
  templateUrl: './subscribing - component.html'
})
export class SubscribingComponent {
  data: string;
  constructor(private sharedSubjectService: SharedSubjectService) {
    this.sharedSubjectService.observable$.subscribe((value) => {
      this.data = value;
    });
  }
}

组件更新:

import { Component } from '@angular/core';
import { SharedSubjectService } from './shared - subject.service';

@Component({
  selector: 'app - updating - component',
  templateUrl: './updating - component.html'
})
export class UpdatingComponent {
  constructor(private sharedSubjectService: SharedSubjectService) {}

  onUpdate() {
    this.sharedSubjectService.updateValue('New value');
  }
}

3. 深入RxJS在Angular中的应用

RxJS(Reactive Extensions for JavaScript)是一个用于处理异步操作和事件流的库,在Angular中被广泛应用。

3.1 基本概念

  • Observable:表示一个可观察的序列,它可以发出零个或多个值,并且可以在完成时或发生错误时通知观察者。例如:
import { Observable } from 'rxjs';

const observable = new Observable((observer) => {
  observer.next(1);
  observer.next(2);
  observer.complete();
});

observable.subscribe((value) => {
  console.log(value);
});

这里subscribe方法用于注册观察者,next方法用于发出值,complete方法表示序列结束。

  • Observer:是一个对象,它有nexterrorcomplete三个回调函数,用于处理Observable发出的值、错误和完成通知。
  • Subscription:当我们订阅一个Observable时,会返回一个Subscription对象。它有一个unsubscribe方法,用于取消订阅,防止内存泄漏。例如:
import { Observable } from 'rxjs';

const observable = new Observable((observer) => {
  let count = 0;
  const intervalId = setInterval(() => {
    observer.next(count++);
    if (count === 5) {
      observer.complete();
      clearInterval(intervalId);
    }
  }, 1000);
});

const subscription = observable.subscribe((value) => {
  console.log(value);
});

// 取消订阅
setTimeout(() => {
  subscription.unsubscribe();
}, 3000);

3.2 RxJS操作符

  • map:用于将Observable发出的每个值映射为一个新的值。例如,将数组中的每个数字加倍:
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

const numbers = [1, 2, 3];
const observable = new Observable((observer) => {
  numbers.forEach((number) => {
    observer.next(number);
  });
  observer.complete();
});

observable.pipe(
  map((number) => number * 2)
).subscribe((value) => {
  console.log(value);
});
  • filter:用于过滤Observable发出的值,只允许满足条件的值通过。例如,只输出偶数:
import { Observable } from 'rxjs';
import { filter } from 'rxjs/operators';

const numbers = [1, 2, 3, 4, 5];
const observable = new Observable((observer) => {
  numbers.forEach((number) => {
    observer.next(number);
  });
  observer.complete();
});

observable.pipe(
  filter((number) => number % 2 === 0)
).subscribe((value) => {
  console.log(value);
});
  • switchMap:用于将Observable发出的值映射为另一个Observable,并取消之前的Observable订阅。常用于处理HTTP请求等异步操作。例如,根据用户输入搜索数据:
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app - search - component',
  templateUrl: './search - component.html'
})
export class SearchComponent {
  searchTerm: string;
  results: Observable<any>;

  constructor(private http: HttpClient) {}

  onSearch() {
    this.results = this.http.get(`https://api.example.com/search?q=${this.searchTerm}`).pipe(
      switchMap((response) => {
        return this.http.get(`https://api.example.com/detail?id=${response.id}`);
      })
    );
  }
}

3.3 RxJS在Angular服务中的应用

在Angular服务中,常使用RxJS来处理异步数据。例如,创建一个获取用户数据的服务:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor(private http: HttpClient) {}

  getUserData(): Observable<any> {
    return this.http.get('https://api.example.com/user');
  }
}

组件中使用该服务:

import { Component } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app - user - component',
  templateUrl: './user - component.html'
})
export class UserComponent {
  userData;
  constructor(private userService: UserService) {
    this.userService.getUserData().subscribe((data) => {
      this.userData = data;
    });
  }
}

4. Angular的依赖注入

依赖注入(Dependency Injection,简称DI)是Angular的核心特性之一,它有助于实现松耦合的代码结构。

4.1 基本概念

  • 依赖:一个组件或服务所需要的其他对象。例如,一个UserComponent可能依赖于UserService来获取用户数据。
  • 注入器:负责创建和管理依赖对象的实例,并将它们提供给需要的组件或服务。Angular有一个分层的注入器系统,包括根注入器和组件级注入器。

4.2 创建和提供服务

通过@Injectable装饰器创建服务,并使用providedIn属性指定服务的提供范围。例如:

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

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

这里providedIn: 'root'表示该服务由根注入器提供,应用中的任何组件都可以使用。

4.3 注入服务

组件通过构造函数注入服务。例如:

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app - example - component',
  templateUrl: './example - component.html'
})
export class ExampleComponent {
  constructor(private logger: LoggerService) {
    this.logger.log('Component initialized');
  }
}

4.4 自定义注入器

在某些情况下,我们可能需要自定义注入器。可以通过在组件的providers数组中提供服务来创建组件级注入器。例如:

import { Component } from '@angular/core';
import { LoggerService } from './logger.service';

@Component({
  selector: 'app - custom - injector - component',
  templateUrl: './custom - injector - component.html',
  providers: [LoggerService]
})
export class CustomInjectorComponent {
  constructor(private logger: LoggerService) {
    this.logger.log('Component in custom injector');
  }
}

这里CustomInjectorComponent有自己的组件级注入器,它创建的LoggerService实例与其他组件使用的根注入器提供的实例不同。

4.5 依赖注入的优势

  • 松耦合:组件不需要自己创建依赖对象,只需要声明依赖,这使得组件更易于测试和维护。例如,在测试ExampleComponent时,可以很容易地替换LoggerService为模拟服务。
  • 可复用性:服务可以被多个组件复用,提高了代码的复用性。例如,LoggerService可以被多个组件用于记录日志。

5. 指令的高级应用

指令是Angular中用于修改DOM元素行为的代码块,分为属性指令和结构指令。

5.1 自定义属性指令

自定义属性指令用于改变元素的外观或行为。例如,创建一个HighlightDirective,当鼠标悬停时高亮元素:

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef) {}

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

在模板中使用:

<p appHighlight>Hover over me to highlight</p>

5.2 自定义结构指令

自定义结构指令用于改变DOM的结构。例如,创建一个UnlessDirective,与*ngIf相反的逻辑:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {
  private hasView = false;

  constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) {}

  @Input() set appUnless(condition: boolean) {
    if (!condition &&!this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

在模板中使用:

<div *appUnless="isLoggedIn">
  <p>Please log in</p>
</div>

5.3 指令的生命周期

指令和组件一样有生命周期钩子。例如,ngOnInit用于在指令初始化后执行逻辑,ngOnDestroy用于在指令销毁时清理资源。以HighlightDirective为例,添加ngOnInit钩子:

import { Directive, ElementRef, HostListener, OnInit } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective implements OnInit {
  constructor(private el: ElementRef) {}

  ngOnInit() {
    this.highlight('lightgray');
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

这样,指令初始化时元素会有一个初始的背景色。

6. Angular中的表单处理

表单是Web应用中常见的交互元素,Angular提供了强大的表单处理功能,包括模板驱动表单和响应式表单。

6.1 模板驱动表单

模板驱动表单通过在模板中使用指令来创建和管理表单。例如,创建一个简单的登录表单:

<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm.value)">
  <div>
    <label for="username">Username:</label>
    <input type="text" id="username" name="username" ngModel required>
  </div>
  <div>
    <label for="password">Password:</label>
    <input type="password" id="password" name="password" ngModel required>
  </div>
  <button type="submit">Submit</button>
</form>

组件中处理表单提交:

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

@Component({
  selector: 'app - login - form',
  templateUrl: './login - form.html'
})
export class LoginFormComponent {
  onSubmit(formData) {
    console.log(formData);
  }
}

这里ngModel指令用于双向数据绑定,#loginForm="ngForm"创建了一个NgForm实例,(ngSubmit)绑定表单提交事件。

6.2 响应式表单

响应式表单通过在组件类中创建和管理表单模型。例如,创建一个注册表单:

import { Component } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app - register - form',
  templateUrl: './register - form.html'
})
export class RegisterFormComponent {
  registerForm: FormGroup;

  constructor() {
    this.registerForm = new FormGroup({
      'username': new FormControl('', Validators.required),
      'email': new FormControl('', [Validators.required, Validators.email]),
      'password': new FormControl('', Validators.minLength(6))
    });
  }

  onSubmit() {
    if (this.registerForm.valid) {
      console.log(this.registerForm.value);
    }
  }
}

模板中使用:

<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
  <div>
    <label for="username">Username:</label>
    <input type="text" id="username" formControlName="username">
    <div *ngIf="registerForm.get('username').hasError('required') && (registerForm.get('username').touched || registerForm.get('username').dirty)">
      Username is required
    </div>
  </div>
  <div>
    <label for="email">Email:</label>
    <input type="email" id="email" formControlName="email">
    <div *ngIf="registerForm.get('email').hasError('required') && (registerForm.get('email').touched || registerForm.get('email').dirty)">
      Email is required
    </div>
    <div *ngIf="registerForm.get('email').hasError('email') && (registerForm.get('email').touched || registerForm.get('email').dirty)">
      Please enter a valid email
    </div>
  </div>
  <div>
    <label for="password">Password:</label>
    <input type="password" id="password" formControlName="password">
    <div *ngIf="registerForm.get('password').hasError('minlength') && (registerForm.get('password').touched || registerForm.get('password').dirty)">
      Password must be at least 6 characters
    </div>
  </div>
  <button type="submit">Submit</button>
</form>

这里FormGroupFormControl用于创建表单模型,formControlName将模板中的输入元素与表单模型关联。

6.3 表单验证

  • 内置验证器:Angular提供了许多内置验证器,如requiredemailminLength等。在上面的响应式表单示例中已经使用到。
  • 自定义验证器:可以创建自定义验证器。例如,创建一个验证用户名是否唯一的自定义验证器:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

export function uniqueUsernameValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    // 模拟异步验证
    const isUnique = true; // 实际应通过服务检查用户名是否唯一
    return isUnique? null : { 'usernameNotUnique': true };
  };
}

在响应式表单中使用:

this.registerForm = new FormGroup({
  'username': new FormControl('', [Validators.required, uniqueUsernameValidator()]),
  'email': new FormControl('', [Validators.required, Validators.email]),
  'password': new FormControl('', Validators.minLength(6))
});

7. Angular的路由与导航

路由是Angular应用中实现页面导航和视图切换的关键机制。

7.1 基本路由配置

通过@angular/router模块进行路由配置。例如,创建一个简单的路由配置:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

在模板中使用router - outlet显示路由组件:

<router - outlet></router - outlet>

使用routerLink指令创建导航链接:

<ul>
  <li><a routerLink="">Home</a></li>
  <li><a routerLink="about">About</a></li>
</ul>

7.2 路由参数

路由参数用于传递数据。例如,创建一个产品详情路由,通过参数传递产品ID:

const routes: Routes = [
  { path: 'products/:id', component: ProductDetailComponent }
];

ProductDetailComponent中获取参数:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app - product - detail',
  templateUrl: './product - detail.html'
})
export class ProductDetailComponent implements OnInit {
  productId: string;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.productId = this.route.snapshot.paramMap.get('id');
  }
}

导航到产品详情页:

<a [routerLink]="['/products', product.id]">{{ product.name }}</a>

7.3 嵌套路由

嵌套路由用于创建具有层次结构的路由。例如,一个产品模块可能有产品列表和产品详情,产品详情又有评论子路由:

const productRoutes: Routes = [
  {
    path: 'products', component: ProductListComponent,
    children: [
      { path: ':id', component: ProductDetailComponent,
        children: [
          { path: 'comments', component: ProductCommentsComponent }
        ]
      }
    ]
  }
];

ProductDetailComponent的模板中添加router - outlet用于显示子路由组件:

<router - outlet></router - outlet>

7.4 路由守卫

路由守卫用于保护路由,控制是否允许访问。例如,创建一个AuthGuard来确保只有登录用户可以访问某些路由:

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

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

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

在路由配置中使用:

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