Angular 19+ 高级教程 – 盘点 Angular v14 到 v19 的重大改变
前言
我在 <初识 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 到 v18 也没有推出什么新功能。
v14 到 v18 那么多改变,都离不开 "爱 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 19+ 高级教程 – 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 的功能
下面是 v17 后,用 input 和 output 函数替代 Decorator 的写法。
export class TestComponent { age = input.required({ transform: numberAttribute }); timeoutEventEmitter = output({ alias: 'timeout' }) }
v17 的写法显然没有以前整齐了 (无法一眼分辨哪些 property 是 input, output),
但没办法,为了去除 Decorator...只能牺牲整齐度了。
另外一个重点,input 函数不仅仅替代了 Decorator,它还引入了 Signal 概念。
input 函数的返回类型是 Signal 对象。
想深入理解 Signal 请看这篇 Angular 18+ 高级教程 – 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>; }
下面是 v17 后,用 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 呢?
答案是 "潜规则" + markForCheck 方法
在 app.config.ts 用 provideExperimentalZonelessChangeDetection 取代原本的 provideZoneChangeDetection (Zone.js)。
import { provideExperimentalZonelessChangeDetection, type ApplicationConfig } from '@angular/core'; export const appConfig: ApplicationConfig = { providers: [ // provideZoneChangeDetection({ eventCoalescing: true }) // 这是 Zone.js,注释掉它 provideExperimentalZonelessChangeDetection() ], };
接着在 angular.json 也把 zone.js 注释掉
App 组件
export class AppComponent { protected index = 0; }
App Template
<button (click)="index = index + 1">increase</button> <p>index number: {{ index }}</p>
效果
可以看到,即便没有 Zone.js,Angular 依然能感知到 index 属性的变更。
但是,假如我们换一个方式就不行了。
export class AppComponent { protected index = 0; constructor() { window.setInterval(() => this.index += 1, 200); } }
每 200ms index 就加 1
效果
可以看到,index number 并没有在每 200ms 后渲染。
而是在点击 increase 时一次过跳到后面的号数。
为什么呢🤔?
因为没了 Zone.js,Angular 无法再监听到 setInterval,而 index 的变更本来就监听不到,所以自然就无法变更渲染了。
那为什么点击事件可以呢?
这是因为这个点击事件是透过 template syntax binding 进去的,Angular 在间中动了手脚。
假如我们绕开 Angular,它就无法变更渲染了。
export class AppComponent { protected index = 0; private readonly buttonRef = viewChild.required<string, ElementRef<HTMLElement>>('button', { read: ElementRef }); constructor() { afterNextRender(() => this.buttonRef().nativeElement.addEventListener('click', () => this.index += 1)) } }
<button #button>increase</button> <p>index number: {{ index }}</p>
效果
怎么破?
答案是 markForCheck。
constructor() { const cdr = inject(ChangeDetectorRef); afterNextRender(() => this.buttonRef().nativeElement.addEventListener('click', () => { this.index += 1; cdr.markForCheck(); })); }
简单说,Angular detect 不到 change,那我们就主动通知它咯...是不是很简单呢🙄
当然 markForCheck 很容易一不小心就忘记放,所以 Angular 的 Best Practice 是使用 RxJS BehaviourSubject + AsyncPipe (注:在不使用 Signal 的情况下是这样,使用 Signal 的话下一 part 会讲解)
这样虽然多了很多代码,而且还依赖 RxJS,但是就不用担心忘记放 markForCheck 了,Angular 是不是好棒棒呢🙄
想深入理解 Change Detection 请看这篇 Angular 18+ 高级教程 – Change Detection。
Optional RxJS
上一 part 我们展示了如何利用 RxJS BehaviourSubject + AsyncPipe 来通知 Angular view model 变更,尽而淘汰 Zone.js。
Angular 团队在像 Solid.js 取经之后,迷途知返的推出了 Signal。
它可以取代 RxJS 在上述例子中扮演的角色,我们来看一看。
export class AppComponent { protected readonly index = signal(0); private readonly buttonRef = viewChild.required<string, ElementRef<HTMLElement>>('button', { read: ElementRef }); constructor() { const { buttonRef, index } = this; afterNextRender(() => buttonRef().nativeElement.addEventListener('click', () => index.set(index() + 1) )); } }
<button #button>increase</button> <p>index number: {{ index() }}</p>
有了 Signal,我们就可以在不使用 BehaviourSubject,AsyncPipe,markForCheck 的前提下,依然告知 Angular view model 变更。
注1:目前 Signal 还处于半成品,它还无法完全替代 RxJS 在 Angular Framework 里的每一个使用场景,但 Angular 团队正在往这个方向前进。
注2:Signal 是用来取代 Angular Framework 里的 RxJS,但不是你项目里所有的 RxJS。有些项目是因为使用 Angular 而被迫使用 RxJS,这部分的 RxJS 可以 optional,但也有一些项目是本来就适合使用 RxJS,这些则保留。
注3:Signal 可能会进入 T39,往后 Observable 也可能进入 T39,这大致就是 Angular 会前往的方向。
想深入理解 Signal 请看这篇 Angular 18+ 高级教程 – 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 等等。
下面是 v17 后,用 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 - v18 为降低学习门槛所做出的改动。
当然 v14 - v18 远远不只改动了这些,还加了许多新功能,这里我讲几个比较常用到的,有兴趣的可以点击链接查看:
-
Typed Forms (v14)
-
setInput (v14)
-
DestroyRef (v16)
-
takeUntilDestroyed (v16)
-
afterNextRender (v16)
v19 的改动可以看这篇 – Angular 19 正式发布 の 新功能介绍。
我有必要升级改写法吗?
看到那么多改动,大家一定心里很焦虑,有种 AngularJS 被抛弃的感觉。
但其实呢...大家根本不用瞎焦虑。
这些改动都只是表层而已,底层 Ivy rendering engine 压根就没动过。
要知道,Angular 现在是在做减法,而不是加法。
我们跟着升级是很安全的,breaking changes 不多。
好,升级可以,那写法要改吗?
告诉你一个秘密,Angular Material 源码里:
-
一堆的 @Inject, @Input
-
一堆的 NgZone
-
一行 Signal 也没有
(更新:v18 一行都没有,但 v19 开始,已经有一些 Signal 了)
所以大家根本不用急,版本升级是必要的,有写新代码就用新的写法就可以了。
确实会有一些功能 (比如 Directive Composition API) 只有新写法支持,但是这种情况很少的,
要记得,他们底层 Ivy rendering engine 压根就没动过 (不然你以为 Angular 团队真的突飞猛进??)
所以,我非常鼓励大家升级 Angular 版本,至于写法嘛...不急
我自己体会了几个月,有一些新写法会比之前好,但有一些只是半径八两,我建议大家可以先学起来,有机会就写一写体会一下,总之不着急。
想了解新写法请看这篇 -- Coding Style Guide 编码风格
Next Big Thing の Wiz
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 18+ 高级教程 – Coding Style Guide 编码风格
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻