前言
我在 <初識 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 的學習門檻。那要怎樣降低呢?
簡單丫,把一堆概念去掉,不就變得簡單了嗎。
-
去除 NgModule
所謂的去除其實是 optional 的意思,好好的功能怎麼可能刪掉嘛,只是不逼着你學,不逼你用而已。
NgModule 適合用來批量管理組件,但如果組件少的話,就會變成 1 個 NgModule 只管理 1 個組件,這就很多此一舉啊,一個有什麼好管理的?
-
去除 Decorator
Decorator 簡直是亂七八雜的東西,草案了這麼久,後來又大改。雖然現在是定案了,但生態也沒起來 (esbuild 就不支持 Decorator)
-
去除 Zone.js
Zone.js 本來是不錯的,但很遺憾,最終沒能進入 ECMA。那 monkey patching 的東西誰還敢用呢?
-
去除 RxJS
RxJS 是很好用,但是要學啊。必須改成 optional。
-
去除 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 Injection 和 NodeInjector。
input, output 函數
下面是組件 input, output Decorator 的寫法
export class TestComponent { @Input({ required: true, transform: numberAttribute }) age!: number; @Output('timeout') timeoutEventEmitter = new EventEmitter(); }
下面是 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。
Structural Directive Syntax Reference (結構型指令微語法)
目錄
想查看目錄,請移步 Angular 17+ 高級教程 – 目錄