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

Angular独特魅力大揭秘

2021-10-154.4k 阅读

模块化架构

在前端开发领域,模块化架构是提升代码可维护性与复用性的关键策略。Angular 基于模块化架构设计,为开发者提供了一套强大的工具来组织和管理代码。

Angular 中的模块使用 @NgModule 装饰器来定义。例如,一个简单的 Angular 应用模块可能如下所示:

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 {}

在上述代码中,@NgModule 装饰器定义了一个模块。imports 数组用于导入其他模块,这里导入了 BrowserModule,它包含了在浏览器环境中运行 Angular 应用所需的基础功能。declarations 数组用于声明该模块内的组件、指令和管道,这里声明了 AppComponentbootstrap 数组指定了应用的根组件,即 AppComponent

Angular 的模块具有清晰的层次结构。应用可以由多个特性模块组成,每个特性模块专注于实现特定的功能。例如,一个电商应用可能有用户模块、产品模块、订单模块等。以用户模块为例:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserListComponent } from './user - list/user - list.component';
import { UserDetailComponent } from './user - detail/user - detail.component';

@NgModule({
  imports: [CommonModule],
  declarations: [UserListComponent, UserDetailComponent],
  exports: [UserListComponent, UserDetailComponent]
})
export class UserModule {}

这里的 UserModule 导入了 CommonModule,它提供了一些通用的指令和管道。declarations 声明了 UserListComponentUserDetailComponent 两个组件,并且通过 exports 将这两个组件暴露出去,以便其他模块可以使用。

这种模块化架构带来了诸多好处。首先,它提高了代码的可维护性。当应用规模增长时,每个模块的功能相对独立,修改某个模块的代码不会轻易影响到其他模块。其次,增强了代码的复用性。模块可以在不同的应用或同一应用的不同部分重复使用,减少了代码的重复编写。

组件化开发

组件是 Angular 应用的基本构建块,它将 UI 与逻辑紧密结合。一个 Angular 组件由一个 TypeScript 类和一个 HTML 模板组成,还可以包含 CSS 样式。

以下是一个简单的 Angular 组件示例:

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

@Component({
  selector: 'app - hello - world',
  templateUrl: './hello - world.component.html',
  styleUrls: ['./hello - world.component.css']
})
export class HelloWorldComponent {
  message: string = 'Hello, Angular!';
}

在上述代码中,@Component 装饰器定义了一个组件。selector 是组件的选择器,在 HTML 中可以通过 <app - hello - world> 标签来使用这个组件。templateUrl 指向组件的 HTML 模板文件,styleUrls 指向组件的 CSS 样式文件。组件类 HelloWorldComponent 定义了一个 message 属性,用于在模板中显示文本。

组件的 HTML 模板 hello - world.component.html 可能如下:

<div>
  <p>{{message}}</p>
</div>

这里通过 {{message}} 语法将组件类中的 message 属性值显示在页面上。

Angular 组件支持属性绑定和事件绑定。例如,我们可以给组件传递属性值:

<app - hello - world [message]="customMessage"></app - hello - world>

在组件类中,需要通过 @Input() 装饰器来接收传递进来的属性:

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

@Component({
  selector: 'app - hello - world',
  templateUrl: './hello - world.component.html',
  styleUrls: ['./hello - world.component.css']
})
export class HelloWorldComponent {
  @Input() message: string;
}

事件绑定方面,假设组件中有一个按钮,当点击按钮时触发一个方法:

<button (click)="onButtonClick()">Click me</button>

在组件类中定义 onButtonClick 方法:

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

@Component({
  selector: 'app - hello - world',
  templateUrl: './hello - world.component.html',
  styleUrls: ['./hello - world.component.css']
})
export class HelloWorldComponent {
  onButtonClick() {
    console.log('Button clicked!');
  }
}

组件化开发使得代码的结构更加清晰,每个组件负责自己的 UI 和逻辑,易于开发、测试和维护。同时,组件可以嵌套使用,构建出复杂的用户界面。

双向数据绑定

双向数据绑定是 Angular 的一个强大特性,它使得模型与视图之间的数据同步变得轻松。在 Angular 中,双向数据绑定通过 [(ngModel)] 指令实现。

例如,我们有一个输入框和一个显示文本的段落:

<input [(ngModel)]="userInput" />
<p>You entered: {{userInput}}</p>

在组件类中,只需要定义 userInput 属性即可:

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

@Component({
  selector: 'app - data - binding',
  templateUrl: './data - binding.component.html',
  styleUrls: ['./data - binding.component.css']
})
export class DataBindingComponent {
  userInput: string;
}

当用户在输入框中输入内容时,userInput 属性的值会实时更新,同时段落中的文本也会随之改变。反之,当通过代码修改 userInput 属性的值时,输入框中的内容也会相应更新。

双向数据绑定在表单处理中非常实用。比如一个登录表单:

<form>
  <label for="username">Username:</label>
  <input type="text" id="username" [(ngModel)]="username" />
  <br />
  <label for="password">Password:</label>
  <input type="password" id="password" [(ngModel)]="password" />
  <br />
  <button type="submit" (click)="onSubmit()">Submit</button>
</form>

在组件类中:

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

@Component({
  selector: 'app - login - form',
  templateUrl: './login - form.component.html',
  styleUrls: ['./login - form.component.css']
})
export class LoginFormComponent {
  username: string;
  password: string;

  onSubmit() {
    console.log('Username:', this.username);
    console.log('Password:', this.password);
  }
}

通过双向数据绑定,我们可以方便地获取表单输入的值,并进行后续的处理。这种机制大大简化了视图与模型之间的数据交互,提高了开发效率。

依赖注入

依赖注入是 Angular 实现松耦合架构的核心机制。它允许将一个组件或服务所依赖的对象,通过外部提供的方式注入到组件或服务中,而不是在组件或服务内部创建依赖对象。

在 Angular 中,使用 @Injectable() 装饰器来定义一个可注入的服务。例如,我们创建一个简单的日志服务:

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

@Injectable()
export class LoggerService {
  log(message: string) {
    console.log('Log:', message);
  }
}

然后,在组件中使用这个日志服务。假设我们有一个 AppComponent

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

@Component({
  selector: 'app - root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  constructor(private logger: LoggerService) {}

  ngOnInit() {
    this.logger.log('AppComponent initialized');
  }
}

AppComponent 的构造函数中,通过 private logger: LoggerServiceLoggerService 注入到组件中。这样,组件就可以使用 LoggerServicelog 方法,而不需要自己创建 LoggerService 的实例。

依赖注入的好处很多。首先,它提高了代码的可测试性。在测试 AppComponent 时,可以很方便地提供一个模拟的 LoggerService,从而避免实际的日志输出对测试造成干扰。其次,它增强了代码的可维护性和可扩展性。当 LoggerService 的实现发生变化时,只需要修改 LoggerService 本身,而不会影响到依赖它的组件。

路由

路由是单页应用(SPA)中实现页面导航和视图切换的重要机制。Angular 提供了强大的路由功能,允许开发者定义应用的路由规则,实现不同组件之间的导航。

首先,需要在模块中配置路由。例如,在 AppModule 中配置路由:

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

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

@NgModule({
  imports: [BrowserModule, RouterModule.forRoot(routes)],
  declarations: [AppComponent, HomeComponent, AboutComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

在上述代码中,通过 RouterModule.forRoot(routes) 来配置应用的根路由。routes 数组定义了路由规则,path 表示路由路径,component 表示该路径对应的组件。当路径为空时,显示 HomeComponent;当路径为 about 时,显示 AboutComponent

在 HTML 中,可以使用 <router - outlet> 来显示当前路由对应的组件:

<nav>
  <a routerLink="/">Home</a>
  <a routerLink="/about">About</a>
</nav>
<router - outlet></router - outlet>

这里通过 routerLink 指令来创建导航链接。当用户点击链接时,router - outlet 会显示对应的组件。

Angular 路由还支持路由参数。例如,我们可以定义一个带有参数的路由:

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

ProductDetailComponent 中,可以通过 ActivatedRoute 来获取参数:

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

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

  constructor(private route: ActivatedRoute) {}

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

这样,根据不同的参数值,ProductDetailComponent 可以显示不同产品的详细信息。

模板语法

Angular 的模板语法是连接组件逻辑与视图的桥梁,它提供了丰富的指令和表达式来操作 DOM 和显示数据。

插值表达式

插值表达式是最基本的模板语法,用于将组件中的数据显示在视图中。例如:

<p>{{message}}</p>

这里将组件类中的 message 属性值显示在 <p> 标签内。

指令

  1. 结构指令 结构指令用于改变 DOM 的结构。常见的结构指令有 *ngIf*ngFor*ngIf 用于根据条件显示或隐藏元素。例如:
<div *ngIf="isLoggedIn">
  <p>Welcome, user!</p>
</div>

isLoggedIntrue 时,<div> 及其内部内容会显示在页面上;当 isLoggedInfalse 时,<div> 及其内部内容会从 DOM 中移除。

*ngFor 用于遍历数组或对象,并为每个元素创建一个模板实例。例如,遍历一个用户列表:

<ul>
  <li *ngFor="let user of users">{{user.name}}</li>
</ul>

在组件类中定义 users 数组:

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

@Component({
  selector: 'app - user - list',
  templateUrl: './user - list.component.html',
  styleUrls: ['./user - list.component.css']
})
export class UserListComponent {
  users = [
    { name: 'Alice' },
    { name: 'Bob' },
    { name: 'Charlie' }
  ];
}

这样,*ngFor 会为 users 数组中的每个用户创建一个 <li> 元素,并显示用户的名字。

  1. 属性指令 属性指令用于改变元素的属性。例如,[disabled] 指令可以根据条件禁用按钮:
<button [disabled]="isButtonDisabled">Click me</button>

isButtonDisabledtrue 时,按钮会被禁用。

管道

管道用于对数据进行转换和格式化。Angular 提供了一些内置管道,如 DatePipeUpperCasePipeLowerCasePipe 等。

例如,使用 DatePipe 来格式化日期:

<p>{{today | date:'yyyy - MM - dd'}}</p>

在组件类中定义 today 属性:

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

@Component({
  selector: 'app - date - formatting',
  templateUrl: './date - formatting.component.html',
  styleUrls: ['./date - formatting.component.css']
})
export class DateFormattingComponent {
  today = new Date();
}

这里通过 | 符号将 today 数据传递给 date 管道,并指定格式化的格式为 yyyy - MM - dd

开发者还可以自定义管道。例如,我们创建一个将字符串反转的管道:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name:'reverseString'
})
export class ReverseStringPipe implements PipeTransform {
  transform(value: string): string {
    return value.split('').reverse().join('');
  }
}

在模板中使用这个自定义管道:

<p>{{'Hello' | reverseString}}</p>

这样,Hello 字符串会被反转显示为 olleH

响应式编程

响应式编程在处理异步操作和事件流方面具有显著优势。Angular 对响应式编程提供了良好的支持,主要通过 RxJS(Reactive Extensions for JavaScript)库来实现。

例如,处理 HTTP 请求时,Angular 的 HttpClient 返回的是一个 Observable 对象。假设我们有一个获取用户列表的服务:

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

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

  getUsers(): Observable<any[]> {
    return this.http.get<any[]>('/api/users');
  }
}

在组件中使用这个服务:

import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
import { Observable } from 'rxjs';

@Component({
  selector: 'app - user - list - component',
  templateUrl: './user - list - component.html',
  styleUrls: ['./user - list - component.css']
})
export class UserListComponent implements OnInit {
  users$: Observable<any[]>;

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.users$ = this.userService.getUsers();
  }
}

在模板中,可以使用 async 管道来订阅 Observable 对象,并显示数据:

<ul>
  <li *ngFor="let user of users$ | async">{{user.name}}</li>
</ul>

async 管道会自动订阅 Observable,并在数据可用时更新视图,同时在组件销毁时自动取消订阅,避免内存泄漏。

RxJS 还提供了丰富的操作符来处理 Observable。例如,map 操作符可以对 Observable 发射的数据进行转换:

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

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

  getUsers(): Observable<string[]> {
    return this.http.get<any[]>('/api/users').pipe(
      map(users => users.map(user => user.name))
    );
  }
}

这里通过 map 操作符将获取到的用户对象数组转换为用户名字符串数组。

性能优化

随着 Angular 应用规模的增长,性能优化变得至关重要。以下是一些常见的 Angular 性能优化策略。

懒加载

懒加载是一种延迟加载模块的技术,只有在需要时才加载模块及其相关资源。在路由配置中,可以很方便地实现懒加载。例如:

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

这里使用 loadChildren 来指定延迟加载的模块。当用户导航到 /feature 路径时,才会加载 FeatureModule 及其相关的组件、服务等资源,从而提高应用的初始加载速度。

变更检测优化

Angular 使用变更检测机制来检测数据变化并更新视图。默认情况下,Angular 会在每个事件循环周期检查所有组件的变化。对于一些性能敏感的应用,可以通过调整变更检测策略来优化性能。

例如,对于一些不需要频繁检测变化的组件,可以将其变更检测策略设置为 OnPush。在组件类中:

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

@Component({
  selector: 'app - on - push - component',
  templateUrl: './on - push - component.html',
  styleUrls: ['./on - push - component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {}

当组件的 changeDetection 策略设置为 OnPush 时,Angular 只会在以下情况下检测组件变化:

  1. 组件输入属性引用发生变化。
  2. 组件接收到事件。
  3. 可观察对象(Observable)发射新值。

这样可以减少不必要的变更检测,提高应用性能。

优化 DOM 操作

在 Angular 中,尽量避免在组件类中直接操作 DOM,而是使用 Angular 的模板语法和指令来间接操作 DOM。因为 Angular 的变更检测机制是基于组件状态的,直接操作 DOM 可能会导致变更检测机制无法正确检测到变化,从而影响性能。

例如,不要在组件类中使用 document.getElementById('elementId').innerHTML = 'new content'; 这样的方式来修改 DOM。而是通过在模板中绑定数据和使用指令来实现相同的效果,如:

<div [innerHTML]="content"></div>

在组件类中:

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

@Component({
  selector: 'app - dom - manipulation',
  templateUrl: './dom - manipulation.component.html',
  styleUrls: ['./dom - manipulation.component.css']
})
export class DomManipulationComponent {
  content = 'new content';
}

通过这种方式,Angular 可以更好地管理 DOM 操作,提高性能。

与其他技术的集成

Angular 具有良好的兼容性,可以与其他前端技术和后端服务进行集成。

与第三方 UI 库集成

许多流行的第三方 UI 库,如 Bootstrap、Material Design 等,都可以与 Angular 集成。以 Angular Material 为例,它是基于 Material Design 的 Angular UI 组件库。

首先,通过 npm 安装 Angular Material:

npm install @angular/material @angular/cdk

然后,在 AppModule 中导入需要的组件模块,如 MatButtonModule

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

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

在模板中就可以使用 Angular Material 的按钮组件:

<button mat - button>Click me</button>

这样就可以快速为应用添加美观且符合 Material Design 规范的 UI 组件。

与后端服务集成

Angular 可以通过 HttpClient 与各种后端服务进行通信,如 RESTful API。例如,向一个后端 API 发送 POST 请求来创建用户:

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

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

  createUser(user: any): Observable<any> {
    return this.http.post('/api/users', user);
  }
}

在组件中使用这个服务:

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

@Component({
  selector: 'app - create - user',
  templateUrl: './create - user.component.html',
  styleUrls: ['./create - user.component.css']
})
export class CreateUserComponent {
  newUser = { name: '', email: '' };

  constructor(private userService: UserService) {}

  onSubmit() {
    this.userService.createUser(this.newUser).subscribe(response => {
      console.log('User created:', response);
    });
  }
}

通过 HttpClient,Angular 可以方便地与后端进行数据交互,实现完整的前后端应用。

综上所述,Angular 在模块化架构、组件化开发、双向数据绑定、依赖注入、路由、模板语法、管道、响应式编程、性能优化以及与其他技术的集成等方面都展现出独特的魅力,为开发者提供了一套全面且高效的前端开发解决方案,帮助开发者构建高质量、可维护的大型前端应用。