前言

我在 <初识 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 的学习门槛。那要怎样降低呢?

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

  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 18+ 高级教程 – 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 呢?

答案是 Signal。

在 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 注释掉

使用 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。

如果要用 Zone.js 实现相同的效果,我们需要手动 ChangeDetectorRef.markForCheck。

export class AppComponent {
constructor(private changeDetectorRef: ChangeDetectorRef) {} value = 0; startTimer() {
window.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 18+ 高级教程 – 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 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 远远不只改动了这些,还加了许多新功能,这里我讲几个比较常用到的,有兴趣的可以点击链接查看:

  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 版本,至于写法嘛...不急

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

想了解新写法请看这篇 -- 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+ 高级教程 – 学以致用

下一篇 Angular 18+ 高级教程 – Coding Style Guide 编码风格

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

喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding

Angular 18+ 高级教程 – 盘点 Angular v14 到 v18 的重大改变的更多相关文章

  1. Angular CLI 使用教程指南参考

    Angular CLI 使用教程指南参考 Angular CLI 现在虽然可以正常使用但仍然处于测试阶段. Angular CLI 依赖 Node 4 和 NPM 3 或更高版本. 安装 要安装Ang ...

  2. angular.js高级程序设计书本开头配置环境出错,谁能给解答一下

    server.jsvar connect=require('connect');serveStatic=require('serve-static');var app=connect();app.us ...

  3. NgRx/Store 4 + Angular 5使用教程

    这篇文章将会示范如何使用NgRx/Store 4和Angular5.@ngrx/store是基于RxJS的状态管理库,其灵感来源于Redux.在NgRx中,状态是由一个包含action和reducer ...

  4. ReactiveX 学习笔记(18)使用 RxJS + Angular 调用 REST API

    JSON : Placeholder JSON : Placeholder (https://jsonplaceholder.typicode.com/) 是一个用于测试的 REST API 网站. ...

  5. angular源码分析:angular中的依赖注入式如何实现的

    一.准备 angular的源码一份,我这里使用的是v1.4.7.源码的获取,请参考我另一篇博文:angular源码分析:angular源代码的获取与编译环境安装 二.什么是依赖注入 据我所知,依赖注入 ...

  6. Siki_Unity_2-9_C#高级教程(未完)

    Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...

  7. [Angular] ngx-formly (AKA angular-formly for Angular latest version)

    In our dynamic forms lessons we obviously didn’t account for all the various edge cases you might co ...

  8. Pandas之:Pandas高级教程以铁达尼号真实数据为例

    Pandas之:Pandas高级教程以铁达尼号真实数据为例 目录 简介 读写文件 DF的选择 选择列数据 选择行数据 同时选择行和列 使用plots作图 使用现有的列创建新的列 进行统计 DF重组 简 ...

  9. angular源码分析:angular中脏活累活的承担者之$interpolate

    一.首先抛出两个问题 问题一:在angular中我们绑定数据最基本的方式是用两个大括号将$scope的变量包裹起来,那么如果想将大括号换成其他什么符号,比如换成[{与}],可不可以呢,如果可以在哪里配 ...

  10. angular源码分析:angular中入境检察官$sce

    一.ng-bing-html指令问题 需求:我需要将一个变量$scope.x = '<a href="http://www.cnblogs.com/web2-developer/&qu ...

随机推荐

  1. 【原创软件】第6期:极简SciHub论文下载器

    一.背景 因为科研需求下载英文论文,省得自己去找有效的scihub网址,特此写了一个基于c#和wpf的小软件. 二.使用方法 只需要输入doi即可,点击[打开浏览器下载论文]即可跳转浏览器进行下载.下 ...

  2. vue高频面试题

    来源:B站程序员来了 第一部分:vue基础 1,v-if和v-for的优先级谁更高?同时出现该如何优化性能? 在同级出现的时候,render函数会将v-for和v-if同时渲染在一个名为_l的函数,在 ...

  3. php8.3开启jit技术

    查看是否开启:$jitEnabled = ini_get('jit.enabled'); echo "JIT Enabled: " . ($jitEnabled == '1' ? ...

  4. oauth2协议

    什么是OAUTH2协议: 首先是几个概念问题: 资源:用户信息,在微信中存储 资源拥有者:用户 认证服务:微信负责认证用户的身份,也负责为客户端颁发令牌 客户端:携带令牌请求微信获取用户信息 仍以微信 ...

  5. 使用ollama本地部署gemma记录

    1.官网https://ollama.com/安装ollama 2.先配置一下环境变量 不然下载的东西会默认丢在C盘里 3.cmd执行ollama run gemma:2b (使用后推荐直接下7b,2 ...

  6. JWT浅了解

    JWT通过数字签名的方式(让我想起了软考),以json对象为载体,在不同的服务终端之间安全传输信息 是一种授权认证 生成token的原理:通过header的加密方式,对payload进行加密.然后把h ...

  7. 基于禅道数据库对bug进行不同维度统计

    工作中经常需要在周报.月报.年报对禅道bug数据进行不同维度统计导出,以下是我常用的统计sql 1.统计2022年每个月bug数(deleted='0'是查询未删除的bug) select DATE_ ...

  8. 中国特供阉割版4090D建议安装最新驱动,据说不然的话会报error:4090和4090D对比

    资料来源: https://www.bilibili.com/video/BV1oa4y127fG/?spm_id_from=333.999.0.0&vd_source=f1d0f27367a ...

  9. 同策略强化学习算法可以使用经验缓存池(experience buffer)吗 ??? 设计一个基于缓存池的改进reinforce算法,给出初步的尝试 ---------- (reinforce + experience buffer)

    本文使用代码地址: https://gitee.com/devilmaycry812839668/reinforce_with_-experience-buffer ================= ...

  10. 记录一次实验室显卡服务器崩溃事件(Ubuntu18.04 server系统,4块NVIDIA的特斯拉显卡)

    系统报错(显示屏上的错误): 系统中的日志文件中所有的log文件都没有记录这次崩溃事件. 不过根据屏幕上显示出的报错,大致估计为显卡的问题: 重启后查看显卡地址: 发现报错的显卡是  0号显卡.个人估 ...