Angular 17 中的控制流(Control Flow)

Angular 在这个月发布了 17 版本,带来了许多新的功能,其中之一就是在模板中内置了新的控制流(Control Flow)。

控制流是一种将流程控制直接写入模板的新声明性语法,从而无须使用 *ngIf*ngFor*ngSwitch 这种基于指令(Directive)的控制流。不知道大家有没有这种感觉,在使用基于指令的控制流时,经常会忘记具体的语法,需要反复查看文档,而新的控制流语法变得很简单,也更容易记住。

本篇文章将通过重构一个简单的项目,来展示一下如何将基于指令的控制流,转换为新的内置控制流写法。

项目设置

在创建项目之前,首先需要确保安装了最新版本的 Angular cli。

bash 复制代码
npm i -g @angular/cli@latest

之后创建一个新项目:

bash 复制代码
ng new control-flow-demo

我们的示例模板代码内容如下:

ts 复制代码
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [PostListComponent, UserListComponent, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault],
  template: `
    <h1>Angular 17 Control Flow</h1>

    <button (click)="fetchData()">Re-Fetch Data</button>

    <ng-container *ngIf="data as item; else noData">
      <ng-container [ngSwitch]="item.type">
        <app-post-list *ngSwitchCase="'posts'" [posts]="item.payload"> </app-post-list>
        <app-user-list *ngSwitchCase="'users'" [users]="item.payload"> </app-user-list>
        <div *ngSwitchDefault class="unknown">Unknown Data Type...</div>
      </ng-container>
    </ng-container>

    <ng-template #noData>
      <div>No data or loading...</div>
    </ng-template>
  `,
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {}

效果图:

在应用加载时,会随机从 userspostsphotos 中获取一种数据。点击按钮会重新随机获取(数据来自 jsonplaceholder)。在获取过程中显示 No data or loading...,如果获取到 users 或者 posts,会渲染出列表,否则显示 Unknown Data Type...

@if

首先让我们从替换 *ngIf 开始。

从模板代码中可以看到,我们使用 *ngIf 来判断当前是否有数据,有的话渲染对应的内容,没有的话渲染 #noData 模板。

html 复制代码
<ng-container *ngIf="data as item; else noData"> ... </ng-container>
<ng-template #noData>
  <div>No data or loading...</div>
</ng-template>

使用新的内置语法替换之后如下:

html 复制代码
@if (data; as item) {
<ng-container [ngSwitch]="item.type">
  <app-post-list *ngSwitchCase="'posts'" [posts]="item.payload"> </app-post-list>
  <app-user-list *ngSwitchCase="'users'" [users]="item.payload"> </app-user-list>
  <div *ngSwitchDefault class="unknown">Unknown Data Type...</div>
</ng-container>
} @else {
<div>No data or loading...</div>
}

与基于指令的 *ngIf 相比,有以下几个优点:

  1. 可以直接使用 @else,不需要声明额外的模板变量。
  2. 支持 @else if 语法,能够判断更多的情况,基于指令的写法是做不到的。
  3. 新的语法无须导入 CommonModule 或者 *ngIf 指令,方便使用。

@switch

接下来我们替换 *ngSwitch

我们在模板代码中,通过获取到的数据类型,来显示不同的内容。只支持 users 或者 posts,否则渲染 *ngSwitchDefault 部分。

html 复制代码
<ng-container [ngSwitch]="item.type">
  <app-post-list *ngSwitchCase="'posts'" [posts]="item.payload"> </app-post-list>
  <app-user-list *ngSwitchCase="'users'" [users]="item.payload"> </app-user-list>
  <div *ngSwitchDefault class="unknown">Unknown Data Type...</div>
</ng-container>

使用新的内置语法替换之后写法如下:

html 复制代码
@switch (item.type) {
  @case ('posts') {
    <app-post-list  [posts]="item.payload" />
  }
  @case('users') {
    <app-user-list  [users]="item.payload" />
  }
  @default {
    <div  class="unknown">Unknown Data Type...</div>
  }
}

新的语法有一个最大的优点就是可以做到类型收窄。

通过上面的图可以看到,在 @case ('posts') 分支下,typescript 能够推断出 payload 对应的类型为 Post[],同理,如果我们查看 @case('users') 下的 payload,类型就是 User[]。这在 *ngSwitch 中是不可能做到的。

@for

最后是 *ngFor,我们以 app-user-list 为例。

ts 复制代码
import { NgFor } from '@angular/common';
import { Component, Input } from '@angular/core';
import { User } from '../model';

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [NgFor],
  template: `
    <h2>Users</h2>
    <ng-container *ngFor="let user of users; trackBy: trackByFn; let i = index">
      <span class="item">{{ i + 1 }}. {{ user.name }}</span>
    </ng-container>
  `,
})
export class UserListComponent {
  @Input() users: User[] = [];

  trackByFn(index: number, item: User) {
    return item.id;
  }
}

我们使用 *ngFor 渲染了一个 users 列表,并且提供了 trackBy 函数,以及将 index 赋值给了 i 变量。

新的内置语法写法如下:

html 复制代码
@for (user of users; track user.id;) {
  <span class="item">{{ $index + 1 }}. {{ user.name }}</span>
}

需要注意的是,@for 必须提供 track 部分,这个对性能优化很重要,可以像上面代码一样,或者直接 track $index 也是可以的。

@for 还有个伙伴 @empty,我们在渲染列表时,有时候会先判断列表长度是不是为 0,这时候会用到 @if,比如这种写法:

html 复制代码
@if (users.length > 0) {
  @for (user of users; track user.id;) {
    <span class="item">{{ $index + 1 }}. {{ user.name }}</span>
  }
} @else {
  <span>No Data</span>
}

这种情况可以使用 @empty,省去 @if@else

html 复制代码
@for (user of users; track user.id;) {
  <span class="item">{{ $index + 1 }}. {{ user.name }}</span>
} @empty {
  <span>No Data</span>
}

总结

总体来说,新的内置语法与 js 语法更相似了,变得更加直观,简化了使用方式。

并且根据 Angular 团队的说法,内置控制流减少了运行时的占用空间,性能更好。

如果你的项目有升级 Angular 到最新版本的打算,那就赶快体验起来吧!

相关推荐
木子七8 分钟前
vue3-setup中使用响应式
前端·vue
廖子默18 分钟前
提供html2canvas+jsPDF将HTML页面以A4纸方式导出为PDF后,内容分页时存在截断的解决思路
前端·pdf·html
Moment21 分钟前
毕业半年,终于拥有了两个近 500 star 的开源项目了 🤭🤭🤭
前端·后端·开源
光影少年1 小时前
react和vue图片懒加载及实现原理
前端·vue.js·react.js
AndyGoWei1 小时前
react react-router-dom history 实现原理,看这篇就够了
前端·javascript·react.js
小仓桑1 小时前
深入理解 JavaScript 中的 AbortController
前端·javascript
摸鱼也很难1 小时前
解决 node.js 执行 npm下载 报无法执行脚本的错
前端·npm·node.js
换个名字不能让人发现我在摸鱼1 小时前
裁剪保存的图片黑边问题
前端·javascript
PeterJXL1 小时前
pnpm:包管理的新星,平替 npm 和 yarn
前端·npm·node.js·pnpm