Angular 20+ 高阶教程 – Component 组件 の Angular Components vs Web Components
前言
我在《初识 Angular》一文中有提到,Angular 团队是一群不爱创新、喜欢 follow 标准的人。
也因此,要想深入理解 Angular Components,我们就得要先搞懂古老的 Web Components 和 MVVM。
因为 Angular Components 正是 follow 这两个概念发展出来的。
MVVM 与 Web Components
关于 MVVM,可以阅读这篇。
简单说,MVVM 的中心思想就是要求程序员不要像 jQuery 年代那样直接操作 DOM,而是透过 MVVM 框架提供的接口,间接地去操作 DOM。
那为什么不要直接操作 DOM 呢?
-
操作 DOM 的代码通常比较繁琐,也不容易阅读理解。
你想要代码有高可读性,本来就需要把代码封装成声明式,而 MVVM 框架正是替你做了这一切。
- Angular 是一个 MVVM 框架,它控制了很多底层的东西 (如 DOM 渲染)。你如果绕过它、直接操作底层,那一不小心就会和它 "打架"。
因此 Best Practice 一直都是:"尽量" 不要 "直接" 操作 DOM。
"尽量" 的意思是:不是说完全不行,只是要控制,不能过多。
不要 "直接" 操作,那还可以 "间接" 操作嘛 -- 透过 Angular 提供的接口,间接操作 DOM 是可以的。
关于 Web Components,请务必先读完《DOM – Web Components》这篇文章,因为接下来我会用到里面的例子继续展开。
用 Angular Components 重写 Counter Component
在 DOM – Web Components 文章的结尾,我写了一个 Counter Component,我们现在用 Angular 把它重写一遍。
最终效果长这样

Step by Step
依据 Get Started 的指示搭建一个测试环境 (这里我就不使用 inline style 和 inline template 了,我不习惯 inline)。
ng new my-app --style=scss --skip-tests --routing=false --ssr=false --zoneless
创建 Counter Component
cd my-app/src/app
ng g c counter
进入 counter.ts, 它目前长这样

Counter 是一个类,我们要用它来描述下面这个 UI 组件。(Thinking in Angular Way)

以面向对象的方式来看,中间的 number 可以用一个属性 (property) 来表示。
点击左右 button,中间的 number 会累加累减,这个可以用 minus plus 方法 (method) 来表示。
好,添加 number 属性和 minus plus 方法到 Counter 里。
export class Counter {
// 一个属性代表中间的 number
protected number = 0;
// 一个累减方法代表左边的 minus button
protected minus() {
this.number--; // 累减 number
}
// 一个累加方法代表右边的 plus button
protected plus() {
this.number++; // 累加 number
}
}
注:你可能会好奇,为什么属性和方法前面要加上 protected?
这纯粹是我个人的代码风格而已。
至于 Angular 的代码风格,我会在后面的章节 《Coding Style Guide 编码风格》另外讲解,这里大家先不用在意。
class 搞定了,接下来我们进入 counter.html
先给它一个初始 HTML (俗称 view)
<button>-</button> <span>1</span> <button>+</button>
接着让它和 Counter 对象 (俗称 view model) 关联起来 (俗称 binding)。
<span>{{ number }}</span>
{{ number }} 是其中一种 Angular 关联语法 (binding syntax),它的意思是把 counter.number 写入到 <span> 里。
类似这样
const counter = new Counter(); // 实例化组件,得到 view model
span.textContent = counter.number.toString(); // 做 binding 渲染
接着是左右两个 button 的点击事件,它们要关联到 Counter 对象的 minus 和 plus 方法。
<button (click)="minus()">-</button> <span>{{ number }}</span> <button (click)="plus()">+</button>
注:这里我们暂不深入这些 binding syntax (看懂表面意思就够了),先继续往下。
接着加点 styles,让它美观。
添加 class 到 HTML 作为 CSS selector
<button class="minus" (click)="minus()">-</button> <span class="number">{{ number }}</span> <button class="plus" (click)="plus()">+</button>
进入 counter.scss
:host { display: flex; gap: 16px; .number { width: 128px; height: 64px; border: 1px solid gray; font-size: 36px; display: grid; place-items: center; } .minus, .plus { width: 64px; height: 64px; } }
至此,Counter Component 的部分就算完成了。
接着是如何使用它,我们进入 app.html 写上
<app-counter />
这个 <app-counter /> element 对应的是 Counter 组件 metadata 里的 css selector

@Component decorator 负责定义这个组件的 "元信息" (metadata),比如这个组件的 .html 和 .scss file 的路径等等。
此外,我们还需要进入 app.ts
import { Component } from '@angular/core';
import { Counter } from './counter/counter'; // 1. import class Counter
@Component({
selector: 'app-root',
// 2. 把 Counter 放入 App metadata 中
// 意思是,App Template (app.html) 里会使用到 Counter 组件 (<app-counter />)
imports: [Counter],
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App {}
这里有一个小知识点:
透过 CSS selector 来配对组件的使用,是 Angular 跟随 Web Components 的做法。
但相比其它框架 (如 React、Svelte),这种写法相对比较繁琐。
也因此,在未来 (maybe v21, v22),Angular 团队计划推出 selectorless 的写法,这个等它正式推出了以后,我会再回来讲解。
最后跑起来
ng serve --open
效果

Signal-based 写法
讲点题外话。
虽然上面的代码一切工作正常,但从 Angular v20 开始,它已经不是主流写法了。
现在流行的是 Signal-based 写法
export class Counter {
// protected number = 0;
protected readonly number = signal(0);
protected minus() {
// this.number--;
this.number.update(number => number - 1);
}
protected plus() {
// this.number++;
this.number.update(number => number + 1);
}
}
其实也没有太大的区别,主要是把 property 的值改成 Signals 而已。
还有 counter.html
<!-- <span class="number">{{ number }}</span> --> <span class="number">{{ number() }}</span>
Signal 是 getter,因此这里也要改成 method call (调用) 形式。
why Signal-based?
你可能会好奇,既然两种写法都可以正常运作,为什么还要跟风使用 Signal-based 写法呢?
而且 Signal-based 写法一点都不优雅,可读性甚至还下降了🤔。
这其实跟 Angular 的 Change Detection 机制有关,后面的章节会详细讲解。
我们现在只需要知道:虽然目前看起来两种写法都能正常工作,但未来可能就不一定了。
总之,Best Practice 是:只要组件的属性有用于 Template (count.html) binding syntax,那就应该优先使用 Signals。
Angular Components vs Web Components
透过对比,我们可以看出 Angular 团队在设计 Angular Components 时的思路 -- 他们如何看待 Web Components 的缺陷、如何保留 Web Components 的设计理念、又如何完善 Web Components。
最终保留了什么、丢弃了什么、增加了什么?搞清楚这些对学习和使用 Angular 非常重要,正所谓 Thinking in Angular Way 就是这样来的。
我们先回顾 Web Components 的整体流程
1. 定义 class Counter

2. 做 Shadow DOM 隔离 CSS styles

3. ajax 获取 template 和 style (如果你不介意直接写 raw HTML 和 CSS 在 TS 的话,则可以省略掉这一步)

4. 搞事件监听和渲染 (DOM 操作)

5. define element

上面 5 个 steps,Angular 全都实现了。
只不过大部分实现代码都被隐藏了起来,我们主要写的是声明代码。

短短的几行代码,Angular 就 "声明" 了以下 4 个 steps
-
定义 class Counter
@Component 表示这个 class 是一个 Component
-
做 Shadow DOM 隔离 CSS styles
by default 所有 Angular Component 都是 CSS styles 隔离的。
不过它并不是透过 Shadow DOM 实现的,这部分我们留以后再详细讲解。
-
ajax 获取 template 和 style
Angular 会在 compile 阶段去链接 .html 和 .scss file
@Component.templateUrl 和 styleUrl 负责声明 files 的路径
-
define component
@component.selector 声明了匹配组件的 css selector
上面这几个步骤,我们完全不需要写实现代码,只要写好声明代码,剩下的交给 Angular 就行了。
还有最后一个 step 是:搞事件监听和渲染 (DOM 操作)
这一步,Angular 使用 MVVM 的方式来 "声明"。
view model

view and binding

同样的,我们不需要写实现代码 (操作 DOM API),只需要 "声明" 就可以了。
从这些对比中,我们可以体会到:Angular 的设计理念就是尽可能地将 "实现代码" 转换为 "声明代码"。
这个动机很好理解,一个项目,代码分两种:
-
实现代码 (命令式)
实现代码就是让逻辑跑起来的代码。它们的特色就是繁琐、啰嗦、可读性差、难修改、难扩展。
-
声明代码 (声明式)
声明代码的目的不是为了 "实现",而是为了 "表达"。
比如我们写的变量名、函数名、class、interface、metadata、configuration 等等,这些都是为了让我们的程序更可读、更好理解、更易扩展、更方便调修维护。
一个好的框架,应该尽可能的替我们完成实现代码的部分,而我们只需要专注在 "声明" 就可以了。
Angular Components !== Web Components
Angular Components 虽然很大程度上借鉴了 Web Components,但是 Angular 并不是用 Custom Elements + Shadow DOM 来实现 Web Components 的。
Angular 有一个扩展项目叫 Angular elements,它的方向是 convert Angular Components to 正真的 Web Components,也就是 Custom Elements + Shadow Dom。
但目前这个项目有很多缺失的功能,而且没有得到足够的重视。希望未来不会被砍掉呗...🙄
目录
上一篇 Angular 20+ 高阶教程 – Dependency Injection 依赖注入
下一篇 Angular 20+ 高阶教程 – Component 组件 の Angular Component vs Custom Elements
想查看目录,请移步 Angular 20+ 高阶教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻

浙公网安备 33010602011771号