Angular 17+ 高階教程 – 盤點 Angular v14 到 v17 的重大改變

兴杰發表於2024-04-18

前言

我在 <初識 Angular> 文章裡有提到 Angular 目前的斷層問題。

大部分的 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 寫法

效果

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 InjectionNodeInjector

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>

效果

模板會自動 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)));
}

效果

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+ 高階教程 – 目錄

相關文章