Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

  • Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变已关闭评论
  • 68 次浏览
  • A+
所属分类:Web前端
摘要

我在 <初识 Angular> 文章里有提到 Angular 目前的断层问题。大部分的 Angular 用户都停留在 v9.0 版本。


前言

我在 <初识 Angular> 文章里有提到 Angular 目前的断层问题。

Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

大部分的 Angular 用户都停留在 v9.0 版本。

Why everyone stay v9.0? 

v9.0 是一个里程碑版本,Angular 从 v4.0 稳定版推出后,好几年都没有什么动静,直到 v9.0 推出了 Ivy rendering engine。

本以为 v9.0 以后 Angular 会大爆发,结果迎来的是 Angular 团队搞内讧,又...好几年没有动静。直到 v14.0 Angular 突然就...变了?。

Angular 团队大换血之后,有了新方向,原本那批人的特色 “不爱创新,爱 follow 标准,爱小题大" 现在已不复存在,新一批人的特色是 "爱 follow 市场,爱新用户,爱借其它团队的力"。

这也是为什么从 v14 以后大家都感觉 Angular 好像不是 Angular 了?。

是福是祸,现在还很难说,所以大部分人都宁愿停留在 v9.0 继续观望,反正 v9.0 到 v17 也没有推出什么新功能。

v14 到 v17 那么多改变,都离不开 "爱 follow 市场,爱新用户" 原则,所以老一批的用户看待这些改变的第一反应都是嗤之以鼻。

为什么写这篇?

很多人在静观其变,但同时心里又有些焦虑,本篇就是要带你体会一下 v14 后的 Angular,让你决定是要转投 Vue 3, React 19, Svelte 5 还是继续留在 Angular 阵营。

 

The Concept Behind the Change

Angular 长久以来一直有一个诟病 -- 学习门槛太高。

这绝对是千真万确的事情。如果有人告诉你学习 Angular 很简单,上手很快,那你要先问清楚,是他教会了很多人快速掌握 Angular,还是只是他自己快速掌握了 Angular。

这是两个完全不同的概念,他自己掌握或许只是因为他比一般人悟性高而已,千万不能以偏概全,不然会误人子弟的。

为什么 Angular 学习门槛会这么高呢?太多太多原因了,一句话总结就是 "不爱创新,爱 follow 标准,爱小题大" 再加一个 "不在乎用户"。

所以,新团队的第一个方向就是降低 Angular 的学习门槛。那要怎样降低呢?

简单丫,把一堆概念去掉,不就变得简单了吗。

  1. 去除 NgModule

    所谓的去除其实是 optional 的意思,好好的功能怎么可能删掉嘛,只是不逼着你学,不逼你用而已。

    NgModule 适合用来批量管理组件,但如果组件少的话,就会变成 1 个 NgModule 只管理 1 个组件,这就很多此一举啊,一个有什么好管理的?

  2. 去除 Decorator

    Decorator 简直是乱七八杂的东西,草案了这么久,后来又大改。虽然现在是定案了,但生态也没起来 (esbuild 就不支持 Decorator)

  3. 去除 Zone.js

    Zone.js 本来是不错的,但很遗憾,最终没能进入 ECMA。那 monkey patching 的东西谁还敢用呢?

  4. 去除 RxJS

    RxJS 是很好用,但是要学啊。必须改成 optional。

  5. 去除 Structural Directive

    结构型指令的语法叫微语法 (Syntax Reference)。

    微语法是挺灵活的,也支持扩展,但学习成本也不少。

    而但绝大部分时候,我们只有在使用原生结构型指令 *ngIf, *ngFor 时才用到微语法。

    这就很没必要学啊。

好,以上几个就是 Angular v14 以后改变的方向。未来还会不会出现 ”去除 TypeScript“ 或 "去除 OOP",那我就不晓得了?。

 

Optional NgModule の Standalone Component

Angular v14 以前,组件一定要依附在 NgModule 上,然后 NgModule import 另一个 NgModule 让组件可以相互使用,一个 NgModule 管理一批组件。

站管理角度,分组批量管理组件是正确的。但对于小项目而言,很多时候 1 个 NgModule 里面就只 declare 了一个组件,因为就没有那么多组件丫。

这种情况 NgModule 就显得很多余,为了写而写,为了管理而管理,这是不对的。

Angular v14 以后,组件可以单纯存在,不需要再依附 NgModule。组件也可以直接 import 另一个组件达到相互使用的结果。NgModule 变成 optional 了。

@Component({   selector: 'app-test',   standalone: true, // 在 @Component 声明 standalone: true 就可以了   templateUrl: './test.component.html',   styleUrl: './test.component.scss' }) export class TestComponent {}

直接 import 就能用了。

@Component({   selector: 'app-root',   standalone: true,   imports: [TestComponent], // 直接 import 组件, no more NgModule   templateUrl: './app.component.html',   styleUrl: './app.component.scss' }) export class AppComponent {}

使用

<app-test />

注:v16 支持了 self-closing-tag 写法

效果

Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

App 组件变成 Standalone Component 后,bootstrap 的方法就不同了

bootstrapApplication(   AppComponent,    { providers: [] } ) .catch((err) => console.error(err));

Provider 不写在 NgModule.providers 而是写在 bootstrapApplication 函数的参数。

想深入理解 NgModule 请看这篇 Angular 17+ 高级教程 – NgModule

 

Optional Decorator の inject, input, output, viewChildren, contentChildren

提醒:Angular 要 optional 很多概念,这个过程是循序渐进的,这里要说的 Optional Decorator 不是说整个项目完完全全不写 Decorator,目前只是部分地方可以 optional 而已。

inject 函数

下面是 Dependency Injection 依赖注入 Decorator 的写法

export class TestComponent {   constructor(      @SkipSelf() @Optional() @Inject(CONFIG_TOKEN) config: Config,     @Attribute('value') value: string   ) {     console.log(config);     console.log(value);   } }

下面是 v14 后,用 inject 函数替代 Decorator 的写法。

export class TestComponent {   constructor() {     const config = inject(CONFIG_TOKEN, { optional: true, skipSelf: true });     const value = inject(new HostAttributeToken('value'));   } }

想深入理解 Dependancy Injection 请看这两篇  Dependency Injection 和 NodeInjector

input, output 函数

下面是组件 input, output Decorator 的写法

export class TestComponent {   @Input({ required: true, transform: numberAttribute })   age!: number;    @Output('timeout')   timeoutEventEmitter = new EventEmitter(); }

注:input required 是 v16 的功能

下面是 v14 后,用 input 和 output 函数替代 Decorator 的写法。

export class TestComponent {   age = input.required({ transform: numberAttribute });    timeoutEventEmitter = output({ alias: 'timeout' }) }

v14 的写法显然没有以前整齐了 (无法一眼分辨哪些 property 是 input, output),

但没办法,为了去除 Decorator...只能牺牲整齐度了。

另外一个重点,input 函数不仅仅替代了 Decorator,它还引入了 Signal 概念。

input 函数的返回类型是 Signal 对象。

想深入理解 Signal 请看这篇 Angular 17+ 高级教程 – Signals

viewChildren, contentChildren 函数

下面是组件 query element Decorator 的写法

export class TestComponent {   @ViewChildren('item', { read: ElementRef })   itemElementRefQueryList!: QueryList<ElementRef<HTMLElement>>;    @ViewChild('item', { read: ElementRef })   itemElementRef!: ElementRef<HTMLElement>;    @ContentChildren('product', { read: ElementRef })   productRefQueryList!: QueryList<ElementRef<HTMLElement>>;    @ViewChild('product', { read: ElementRef })   productElementRef!: ElementRef<HTMLElement>; }

下面是 v14 后,用 viewChildren 和 contentChildren 函数替代 Decorator 的写法。

export class TestComponent {   itemElementRefs = viewChildren('item', { read: ElementRef });   itemElementRef = viewChild.required('item', { read: ElementRef });   productElementRefs = contentChildren('product', { read: ElementRef });   productElementRef = contentChild.required('product', { read: ElementRef }); }

它们返回的类似也是 Signal 哦。

想深入理解 Query Elements 请看这篇 Component 组件 の Query Elements

 

Optional Zone.js

Zone.js 是用来 detect ViewModel change 的,没有了它要怎样 detect change 呢?

答案是 Signal。

在 main.ts 用 ɵprovideZonelessChangeDetection 函数把 Zone.js 关掉

import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { ɵprovideZonelessChangeDetection } from '@angular/core';  bootstrapApplication(   AppComponent,    { providers: [ɵprovideZonelessChangeDetection()] } ) .catch((err) => console.error(err));

使用 signal 对象作为属性值

export class AppComponent {   value = signal(0);     startTimer() {     setInterval(() => {       this.value.update(v => v + 1);     }, 500);   } }

App Template

<p>{{ value() }}</p> <button (click)="startTimer()" >start timer</button>

效果

Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

模板会自动 tracking Signal 对象值的变化,每当值改变就会 refresh LView。

在 v14 以前,我们要实现相同的效果,需要手动 ChangeDetectorRef.markForCheck。

export class AppComponent {   constructor(private changeDetectorRef: ChangeDetectorRef) {}      value = 0;     startTimer() {     setInterval(() => {       this.value++;       this.changeDetectorRef.markForCheck();     }, 500);   } }

或者使用 RxJS + AsyncPipe

export class AppComponent {   constructor() {}    value = new BehaviorSubject(0);     startTimer() {     setInterval(() => {       this.value.next(this.value.value + 1);     }, 500);   } }

<p>{{ value | async }}</p> <button (click)="startTimer()" >start timer</button>

你更喜欢哪一种写法呢?我猜应该是...Svelte 5 吧?? 

想深入理解 Change Detection 请看这篇 Angular 17+ 高级教程 – Change Detection

 

Optional RxJS

Signal 很像 RxJS 的 BehaviorSubject,而 BehaviorSubject 也是一种 Observable,所以在一些情况下,Signal 确实可以替代 RxJS,使得 RxJS 成为 optional。

下面是 RxJS 的写法

const firstNameBS = new BehaviorSubject('Derrick'); const lastNameBS = new BehaviorSubject('lastName'); const fullName$ = combineLatest([firstNameBS, lastNameBS]).pipe(   map(([firstName, lastName]) => `${firstName} ${lastName}`) );
fullName$.subscribe(fullName
=> console.log('fullName', fullName)); setTimeout(() => { firstNameBS.next('Alex'); lastNameBS.next('Lee'); }, 1000);

下面是 Signal 的写法

export class AppComponent {   constructor() {     const firstName = signal('Derrick');     const lastName = signal('Yam');     const fullName = computed(() => firstName() + ' ' + lastName());      effect(() => console.log('fullName', fullName()))      setTimeout(() => {       firstName.set('Alex');       lastName.set('Lee');     }, 1000);   } }

是不是挺像的?写法上差不多,但实际运行的逻辑还是有一些不同哦。

下面是一个把 RxJS Observable 转换成 Signal 的例子

import { toSignal } from '@angular/core/rxjs-interop'; constructor() {   const number$ = new Observable(subscriber => {     let index = 0;     const intervalId = window.setInterval(() => subscriber.next(index++), 1000);     return () => {       window.clearInterval(intervalId);     }   });    const number = toSignal(number$);      effect(() => console.log(number())); // 会一直 log }

toSignal 会 subscribe number$ 然后一直接收新值,effect 可以监听每一次值得变化。

Optional RxJS 目前还只停留在 planning 中。Angular built-in 的 Router, HttpClient, ReactiveForms 依然是返回 RxJS Observable。

想深入理解 Signal 请看这篇 Angular 17+ 高级教程 – Signals

 

Optional Structural Directive Syntax Reference (结构型指令微语法)

下面是一个常见的结构型指令微语法

 <h1 *ngIf="user$ | async as user; else loading">{{ user.firstName }}</h1>  <ng-template #loading>loading...</ng-template>

这还是优化过的版本哦,没有优化更丑,请看

<ng-template [ngIf]="user$ | async" let-user="ngIf" [ngIfElse]="loading">   <h1>{{ user.firstName }}</h1> </ng-template> <ng-template #loading>loading...</ng-template>

里面涉及了很多知识:AsyncPipe, as syntax, else syntax, Template Variable, ng-template, ng-template as ng-container 等等。

下面是 v14 后,用 Control Flow 替代结构型指令的写法。

@if (user$ | async; as user) {   <h1>{{ user.firstName }}</h1> }  @else {   loading... }

是什么干净了很多?

换上 Signal 版本

@if (user(); as user) {   <h1>{{ user.firstName }}</h1> }  @else {   loading... }

export class AppComponent {   user = toSignal(new BehaviorSubject({ firstName: 'Derrick' }).pipe(delay(2000))); }

效果

Angular 17+ 高级教程 – 盘点 Angular v14 到 v17 的重大改变

Control Flow 可以替代 *ngIf, *ngFor, *ngSwitch 指令。

想深入理解 "结构型指令微语法" 请看这篇 Structural Directive (结构型指令) & Syntax Reference (微语法)

想深入理解 Control Flow 请看这篇 Component 组件 の Control Flow

 

其它小改动

以上这些都是 Angular v14 - v17 为降低学习门槛所做出的改动。

当然 v14 - v17 远远不只改动了这些,还加了许多新功能,这里我讲几个比较常用到的,有兴趣的可以点击链接查看:

  1. Typed Forms (v14)

  2. setInput (v14)

  3. Directive Composition API (v15)

  4. DestroyRef (v16)

  5. takeUntilDestroyed (v16)

  6. afterNextRender (v16)

  7. withComponentInputBinding (v16)

  8. input transform & required (v16)

 

我有必要升级改写法吗?

看到那么多改动,大家一定心里很焦虑,有种 AngularJS 被抛弃的感觉。

但其实呢...大家根本不用瞎焦虑。

这些改动都只是表层而已,底层 Ivy rendering engine 压根就没动过。

要知道,Angular 现在是在做减法,而不是加法。

我们跟着升级是很安全的,breaking changes 不多。

好,升级可以,那写法要改吗?

告诉你一个秘密,Angular Material 源码里:

  1. 一堆的 @Inject, @Input

  2. 一堆的 NgZone

  3. 一行 Signal 也没有

所以大家根本不用急,版本升级是必要的,有写新代码就用新的写法就可以了。

确实会有一些功能 (比如 Directive Composition API) 只有新写法支持,但是这种情况很少的,

要记得,他们底层 Ivy rendering engine 压根就没动过 (不然你以为 Angular 团队真的突飞猛进??)

所以,我非常鼓励大家升级 Angular 版本,至于写法嘛...不急

我自己体会了几个月,有一些新写法会比之前好,但有一些只是半径八两,我建议大家可以先学起来,有机会就写一写体会一下,总之不着急。

 

Next Big Thing の Wiz

v18, v19 估计 Signal 就要完结了。

Angular 下一个 Big Thing 是结合 Wiz,相关资讯:Angular and Wiz Are Better Together

科普一下,Wiz 是 Google 内部没有开源的框架。

Google 有很多 Web Application (比如:Google Ads, Analytics, Search Console, Tag Manager, Cloud 等等)

另外还有一些 Website (比如:Youtube, Search, Google Photos 等等)

它们可以分成两大派系,一个注重速度,一个注重交互。

目前重交互的通常使用 Angular,重速度的则使用 Wiz。

Wiz 的作者是大名鼎鼎的 Malte Ubl,现任 Vercel CTO,没错,就是那个 React 的 Next.js。

所以你大概可以遇见 Angular 之后会往 Next.js 的方向走,加强 SSR 和 SSG。

v17 推出的 Control Flow 除了有 @if, @for, @switch 之外还有一个叫 @defer,这个是全新的概念,它的灵感就是来源之 Wiz。

所以大家不需要太焦虑,跟着 Angular 慢慢走就可以了,记住,现在这群人的特色:"爱 follow 市场,爱新用户,爱借其它团队的力" 再加上 "有 planning"。

 

 

目录

上一篇 Angular 17+ 高级教程 – 学以致用

下一篇 TODO

想查看目录,请移步 Angular 17+ 高级教程 – 目录