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 滚动)