Angular 控制流与延迟视图揭秘

2023年11月8日 Angular 团队发布了 Angular 17 开发预览版,在新的版本,Angular 添加了许多激动人心的特性,其中就包含新的控制流和延迟视图

新版 Angular 增加一个 Block的概念,

Block是模板中的一种新语法结构,控制流和延迟视图也是基于这种语法结构来实现的。

控制流

什么是控制流?

控制流是指程序中决定语句执行顺序的机制。它通过顺序、选择(条件判断)、循环等结构,使程序能够根据不同条件或规则执行不同的代码块,实现灵活的逻辑控制,代码中常见的 if-else  for 都属于控制流语法。

Angular 中的控制流

Angular 16 及之前的版本中的控制流是基于微语法和结构指令来实现的,比如:

html 复制代码
<div *ngIf="condition; else elseBlock">
  Content to render when the condition is true.
</div>
<ng-template #elseBlock>
   Content to render when the condition is false.
</ng-template>

html 复制代码
<ng-container [ngSwitch]="tab">
  <basic-info *ngSwitchCase="'basic'"></basic-info>
  <attachment-list *ngSwitchCase="'attachment'"></attachment-list>
  <version-list *ngSwitchCase="'version'"></version-list>
  <invaild-tab *ngSwitchDefault></invaild-tab>
</ng-container>
html 复制代码
<ng-container [ngSwitch]="tab">
  <ng-container *ngSwitchCase="'version'">
    <version-list></version-list>
  </ng-container>
  <ng-container *ngSwitchCase="'attachment'"> <attachment-list></attachment-list> </ng-container>
  <ng-container *ngSwitchCase="'basic'">
    <basic-info></basic-info>
  </ng-container>
  <ng-container *ngSwitchDefault>
    <invaild-tab></invaild-tab>
  </ng-container>
</ng-container>


开发体验不好:

  • 不够直观简洁
  • 灵活度差
  • 微语法模型也不支持控制流语句的多个关联子模板,所以 Angular 无法解决 *ngIf 的情况 else 使用起来非常尴尬的问题
  • 独立组件需要引入

新的控制流

关注 Angular 的同学了解,Angular 一直在推动 Zoneless 和 Singals 的工作,Angular 中现有的基于 Zone 的控制流指令将无法在 Zoneless 的应用中运行的,在考虑修改现有指令以支持 Zoneless 应用时,Angular 团队决定不这样做,因为潜在的重大更改(需要兼容旧的应用程序)和代码复杂性增加。相反,他们选择引入一种新的内置控制流语法,该语法既支持无区域应用程序,又解决微语法长期存在的 DX(开发体验)问题。

语法

官方在 RFC 时候提出了 #-syntax 类似 HTML 的标签语法,如 {#if} 、 {:else} 和 {/if}

html 复制代码
{#if cond.expr}
  Main case was true!
{:else if other.expr}
  Extra case was true!
{:else}
  False case!
{/if}


不过在 RFC 讨论阶段,大部分人更喜欢 @-syntax 的语法,最终 Angular 团队通过调查和研究发现,大部分人也都认可这个方案,最终确定了使用了 @-syntax 的语法

html 复制代码
@if (cond.expr){
   Main case was true!
} @else {
   False case!
}


使用

IF-ELSE

html 复制代码
<div class="status-label" *ngIf="status === 1; else invalid">正常</div>
<div class="status-label" *ngIf="status === 2; else invalid">进行中</div>
<div class="status-label" *ngIf="status === 3; else invalid">完成</div>

<ng-template #invalid>
  <div class="status-label">无效状态</div>
</ng-template>

html 复制代码
<div class="status-label">
  @if (status === 1) {
    正常
  } @else if (status === 2) {
    进行中
  } @else if (status === 3) {
    完成
  } @else {
    无效状态
  }
</div>

@if (users$ | async; as users) {
    {{ users.length }}
}


Switch

html 复制代码
<ng-container [ngSwitch]="tab">
  <basic-info *ngSwitchCase="'basic'"></basic-info>
  <attachment-list *ngSwitchCase="'attachment'"></attachment-list>
  <version-list *ngSwitchCase="'version'"></version-list>
  <invaild-tab *ngSwitchDefault></invaild-tab>
</ng-container>

For Loop

默认的 trackBy

@Component({

template: `

  • <ng-container *ngIf="users?.length > 0; else empty">
    <li *ngFor="let user of users; trackBy: trackFn; let i = index">
    No.${{ i + 1 }} {{ user.name }}

    <ng-template #empty>没有用户</ng-template>

`,

})

export class DemoComponent {

trackFn(index: number, item: Item) {

return item.id;

}

}

  • @for (user of users; track user.id; let i = index, e = even) {
  • No.${{ i + 1 }} {{ user.name }}
  • } @empty { 没有用户 }

  • @for (user of (users \| async); track user.id; let i = index, e = $even) {
  • No.${{ i + 1 }} {{ user.name }}
  • }

延迟视图

在 Angular 16 中如何实现延迟加载组件?

@Component({

template: <ng-container #viewContainer />,

})

export class OldLazyComponent implements AfterViewInit {

@ViewChild('viewContainer', { read: ViewContainerRef })

viewContainerRef!: ViewContainerRef;

async ngAfterViewInit() {

const { DemoComponent } = await import('./demo/demo.component');

const componentRef = this.viewContainerRef.createComponent(DemoComponent);

}

}

@defer

Angular 支持通过 Router 延迟加载应用程序的某些部分(延迟路由),单个组件的延迟加载可以通过 dynamic import() 和 ngComponentOutlet 实现,但这种方法可能很复杂且容易出错,因此 Angular 17 在核心框架中引入一种更符合人体工程学的延迟加载组件的方法 @defer , @defer 不仅仅支持延迟组件,同时也支持延迟加载指令和管道,随着新的 @defer 的引入,我们可以更细颗粒度的控制我们的加载资源,优化应用初始包的体积,提升用户加载的速度。

使用限制

@defer 块中的组件必须是独立组件,非独立组件不支持延迟加载并且会立即加载,及时他被包裹在 @defer 块中

不能在 @defer 块以外引用这个组件,包括使用 @ViewChild 查询这个组件

使用

@defer

@defer 块中的内容最初不会显示,当满足指定的触发器或条件并获取依赖项,块中的内容才会展示,默认情况下,当浏览器状态变为空闲状态(Idle)时,会触发 @defer 加载。

@defer {

}

@defer {

{{name | transterName}}

}

@placeholder

默认情况下, @defer 块在触发之前不会呈现任何内容,我们可以定义 @placeholder 可选的块,声明在触发延迟块之前要显示的内容,当延迟内容加载完成后, @placeholder 块中的内容会销毁,需要注意的是在 @placeholder 块中的内容永远都是立即记载的

@defer {

} @placeholder {

}

@placeholder 块接受一个可选参数 minimum 来指定应显示此占位符的时间,单位支持 s 和 ms。 minimum 是为了防止延迟项加载过快导致内容闪烁。

@defer {

} @placeholder(minimum 500ms) {

}

@loading

@loading 与 @placeholder 一样,也是是一个可选块,允许我们指定加载依赖期间需要展示的内容,与 @placeholder 类似,它也是立即加载的,它可以接收两个参数 after 和 minimum

@defer {

}@loading((after 100ms; minimum 1s)) {

}

@error

@error 比较好理解,我们可以在 @error 中指定依赖加载失败时候展示的内容,与 @placeholder 和 @loading , @error 块的内容也是立即加载的,并且也是可选的

@defer {

}@error{
组件加载错误
}  触发器 Triggers 默认情况下,当浏览器状态变为空闲状态(Idle)时,会触发 @defer 加载,不过我们也可以根据自己的需求指定其他的触发器,Angular 提供了两种触发方式  When 和  On  When 条件触发 指定一个条件,当满足这个条件时触发   @defer (when cond) { }  On on 支持指定以下几种内置的触发器  Tigger Desc Example on idle 浏览器闲时触发,利用浏览器的 requestIdleCallback 特性   @defer (on idle) { }  on immediate 应用渲染完后立即触发   @defer (on immediate) { }  on timer 指定一个时间间隔后触发   @defer (on timer(500ms)) { }  on viewport Placeholder 或指定元素进入可视区域后触发,利用的 IntersectionObserver 特性,组员保证 Placeholder 必须是一个DOM节点   @defer (on viewport) { }@placeholder {
Placeholder
}

const componentFixture = TestBed.createComponent(ComponentA);

// Retrieve the list of all defer block fixtures and get the first block.

const deferBlockFixture = componentFixture.getDeferBlocks()[0];

const deferBlockFixture = componentFixture.getDeferBlocks()[0];

// Render loading state and verify rendered output.

await deferBlockFixture.render(DeferBlockState.Loading);

expect(componentFixture.nativeElement.innerHTML).toContain('Loading');

expect(componentFixture.nativeElement.innerHTML).toContain('加载');

// Render final state and verify the output.

await deferBlockFixture.render(DeferBlockState.Completed);

expect(componentFixture.nativeElement.innerHTML).toContain('');

});

插件支持

Prettier

npm i prettier@3.1 --save-dev

QA

支持自定义 Block 吗?

@defer 可以定义自己的 on 的触发器吗?

控制流会支持自动迁移吗?

现有结构指令会废弃吗?

新的控制流会不会影响 ViewChild 查询结果?

新的控制流有更好的性能?

CDK 的虚拟滚动会收到影响吗?

CDK 目前将继续为虚拟滚动和其他用例提供其现有的结构指令。Angular 将研究将 CDK 的一些结构指令转换为内置语法,或者将扩展点添加到现有语法中供 CDK 构建(例如,支持虚拟 for 滚动)

相关推荐
MiyueFE20 小时前
🚀🚀五个前端开发者都应该了解的TS技巧
前端·typescript
ttod_qzstudio1 天前
基于typescript严格模式以实现undo和redo功能为目标的命令模式代码参考
typescript·命令模式
张志鹏PHP全栈1 天前
TypeScript 第十天,TypeScript面向对象之Class(二)
前端·typescript
慧一居士1 天前
ESLint 完整功能介绍和完整使用示例演示
前端·javascript·typescript
enzeberg2 天前
TypeScript 工具类型(Utility Types)
typescript
難釋懷2 天前
TypeScript类
前端·typescript
杰哥焯逊2 天前
基于TS封装的高德地图JS APi2.0实用工具(包含插件类型,基础类型)...持续更新
前端·javascript·typescript
工业甲酰苯胺3 天前
TypeScript枚举类型应用:前后端状态码映射的最简方案
javascript·typescript·状态模式
土豆骑士4 天前
简单理解Typescript 装饰器
前端·typescript
ttod_qzstudio4 天前
彻底移除 HTML 元素:element.remove() 的本质与最佳实践
前端·javascript·typescript·html