Angular v17带来了新的控制流语法!我试了一下迁移命令

这篇文章是 Angular Advent Calender 2023 第6天的文章。
第5天的文章是rch850先生使用Angular 17的View Transitions API进行玩耍。

Angular v17带来了新的控制流语法!

在过去的Angular版本中,作为模板功能的“分支”和“循环”是通过结构型指令(ngIf、ngFor、ngSwitch)来实现的。

从 Angular v17 开始,将新增控制流语法 @if、@for 和 @switch,旧有的结构型指令可以全部替换为新的控制流语法。

只需要一种选项:
详细信息可参阅官方文件或者

 

如果你能阅读Angular Advent Calender 2023第二天的文章,了解carimatics关于引入嵌入式控制流以及其背景的贡献,那么你也能理解我的动机,并且会很好。

将迁移命令重写为新的控制流语法

Angular团队已经准备了一个迁移命令,用于将现有的结构性指令转换为新的控制流程语法。

ng generate @angular/core:control-flow

这个命令真方便,一键搞定!

我们立刻来试一试吧。以以下实施为例,

<ng-container *ngIf="greeting">
  <span>hello</span>
</ng-container>
<ng-container *ngFor="let number of numbers">
  <span>{{ number }}</span>
</ng-container>
<ng-container [ngSwitch]="num">
  <span *ngSwitchCase="1">one</span>
  <span *ngSwitchCase="2">two</span>
  <span *ngSwitchCase="3">three</span>
</ng-container>
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent {
  greeting: boolean = true;
  numbers: number[] = [1,2,3,4,5];
  num: number = 2;
}

执行命令。

$ ng generate @angular/core:control-flow
? Which path in your project should be migrated? ./
? Should the migration reformat your templates? Yes
    IMPORTANT! This migration is in developer preview. Use with caution.
UPDATE src/app/app.component.html (248 bytes)
UPDATE src/app/app.component.ts (366 bytes)

您可以在对话框中指定路径和选择重新格式化。实际的差异如下所示。

$ git diff -w src
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 781f3e5..2aef72f 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,11 +1,17 @@
-<ng-container *ngIf="greeting">
+@if (greeting) {
   <span>hello</span>
-</ng-container>
-<ng-container *ngFor="let number of numbers">
+}
+@for (number of numbers; track number) {
   <span>{{ number }}</span>
-</ng-container>
-<ng-container [ngSwitch]="num">
-  <span *ngSwitchCase="1">one</span>
-  <span *ngSwitchCase="2">two</span>
-  <span *ngSwitchCase="3">three</span>
-</ng-container>
+}
+@switch (num) {
+  @case (1) {
+    <span>one</span>
+  }
+  @case (2) {
+    <span>two</span>
+  }
+  @case (3) {
+    <span>three</span>
+  }
+}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 9a57e6b..4bfd92f 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,11 +1,11 @@
 import { Component } from '@angular/core';
-import { CommonModule } from '@angular/common';
+
 import { RouterOutlet } from '@angular/router';
 
 @Component({
   selector: 'app-root',
   standalone: true,
-  imports: [CommonModule, RouterOutlet],
+  imports: [RouterOutlet],
   templateUrl: './app.component.html',
   styleUrl: './app.component.scss'
 })

你已经做出很好的改变了。

对于要求使用 “@for” 指定的 track,应该直接指定循环变量本身。这里需要对其进行修正,以确保成为唯一的属性。

+@for (number of numbers; track number) {

我也注意到了少了 CommonModule 的引用,看来是有意想要减小文件大小的意愿呢。

-  imports: [CommonModule, RouterOutlet],
+  imports: [RouterOutlet],

我尝试在实际生活中的业务代码中进行测试。

好了,接下来进入正题。

重要!此迁移正在开发者预览中,请谨慎使用。

既然在执行时会产生这样的输出,我便很好奇在实际的业务代码中,它能以多大的精度替代。让我们立即试一试。

虽然无法公开具体业务代码,但我可以大致告诉你它的规模。

# コンポーネント数
grep -r '@Component' src | wc -l
     156

# テンプレートファイルの行数
find src -name "*.html" -type f | xargs cat | wc -l
   22403

# ngIfの総数
grep -r '*ngIf' src | wc -l
     360

# ngForの総数
grep -r '*ngFor' src | wc -l
     106

# ngSwitchの総数
grep -r 'ngSwitch' src | wc -l
      50

从第一个提交开始已经过去了三年多,现在仍然是一个经验丰富的项目。

看看执行迁移后的差异结果

执行顺利完成,没有出现任何特别的错误。

我对指定了ngFor的trackBy后会发生什么变化感到好奇,所以进行了确认。

-<ng-container *ngFor="let option of options; trackBy: trackByOption">
+@for (option of options; track trackByOption($index, option)) {

看起来已经进行了很好的转换。不错呢。
我大致看了一遍,但没有特别值得一提的内容,只是进行了一些改写。

在构建过程中发生错误的情况

在尝试构建时,发现了两种导致构建错误的情况。

错误案例1

错误 TS2348: 类型 ‘typeof of’ 的值不可被调用。您是不是想要包含 ‘new’?

由于在 *ngFor 的变量中使用了保留词 class,造成了以下问题的原因。

<ng-container *ngFor="let class of classes">
  <span>{{ class }}</span>
</ng-container>

这个版本可以通过构建,但是修改后的版本会出现构建错误。

@for (class of classes; track class) {
  <span>{{ class }}</span>
}

构造指令的语法与控制流语法的评估方式似乎有所不同(考虑到更严格地应用类型检查,这是可以理解的)。如果被告知在ngFor循环变量中使用保留字是错误的,那确实如此。如果不小心在ngFor循环变量中使用保留字,可能会遇到问题。

错误案例2

错误NG5002:@switch块只能包含@case块和@default块。

在 ngSwitch 的容器下方存在 ngIf 的情况下,出现了故障。

其实我自己也不太明白自己在写什么,而且对于为什么会变成这样的实现方式也感到很迷惑,但是在现有的业务代码中,有很多东西要考虑的呢…

以下是具体的代码。

<ng-container [ngSwitch]="num">
  <ng-container *ngIf="greeting">
    <span>hello</span>
  </ng-container>
</ng-container>

无论实施目的如何,上述代码实际上可以成功编译通过。如果将其迁移,结果将如下所示。

@switch (num) {
  @if (greeting) {
    <span>hello</span>
  }
} 

看起来在 @switch 块里不能放置 @if 块呢。

话虽如此,根本不应该写出这样的代码!而且,通常人们也不会写这样的代码,一般情况下也不需要担心。如果能够以趣味性的方式记住这个知识点就太好了。

总结

通过迁移命令进行修改后,已修复上述错误情况的应用程序成功构建,并且目前在验证环境中正常运行。太棒了!

只要代码不是过于混乱到让人束手无策,对于自动测试已经相当完善的项目,在迁移命令中使用机械化的重写方式,再通过自动测试来进行确认,感觉应该是可以的。

使用新的控制流语法不仅可以提升开发体验,还能提高应用程序的性能。我们应该积极进行重写。

明天是beltway7先生。请多关照!

广告
将在 10 秒后关闭
bannerAds