Angular 20+ 高阶教程 – Component 组件 の Angular Components vs Web Components
<h2>前言</h2><p>我在《初识 Angular》一文中有提到,Angular 团队是一群不爱创新、喜欢 follow 标准的人。</p>
<p>也因此,要想深入理解 Angular Components,我们就得要先搞懂古老的 Web Components 和 MVVM。</p>
<p>因为 Angular Components 正是 follow 这两个概念发展出来的。</p>
<p> </p>
<h2>MVVM 与 Web Components</h2>
<p>关于 MVVM,可以阅读这篇。</p>
<p>简单说,MVVM 的中心思想就是要求程序员不要像 jQuery 年代那样直接操作 DOM,而是透过 MVVM 框架提供的接口,间接地去操作 DOM。</p>
<p>那为什么不要直接操作 DOM 呢?</p>
<ol>
<li>
<p>操作 DOM 的代码通常比较繁琐,也不容易阅读理解。</p>
<p>你想要代码有高可读性,本来就需要把代码封装成声明式,而 MVVM 框架正是替你做了这一切。</p>
</li>
<li>Angular 是一个 MVVM 框架,它控制了很多底层的东西 (如 DOM 渲染)。你如果绕过它、直接操作底层,那一不小心就会和它 "打架"。
<p>因此 Best Practice 一直都是:"尽量" 不要 "直接" 操作 DOM。</p>
<p>"尽量" 的意思是:不是说完全不行,只是要控制,不能过多。</p>
<p>不要 "直接" 操作,那还可以 "间接" 操作嘛 -- 透过 Angular 提供的接口,间接操作 DOM 是可以的。</p>
</li>
</ol>
<p>关于 Web Components,请务必先读完《DOM – Web Components》这篇文章,因为接下来我会用到里面的例子继续展开。</p>
<p> </p>
<h2>用 Angular Components 重写 Counter Component</h2>
<p>在 DOM – Web Components 文章的结尾,我写了一个 Counter Component,我们现在用 Angular 把它重写一遍。</p>
<p>最终效果长这样</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230404183326025-1340632211.gif"></p>
<h3>Step by Step</h3>
<p>依据 Get Started 的指示搭建一个测试环境 (这里我就不使用 inline style 和 inline template 了,我不习惯 inline)。</p>
<div class="cnblogs_code">
<pre>ng new my-app --style=scss --skip-tests --routing=<span style="color: rgba(0, 0, 255, 1)">false</span> --ssr=<span style="color: rgba(0, 0, 255, 1)">false</span> --zoneless</pre>
</div>
<p>创建 Counter Component</p>
<div class="cnblogs_code">
<pre>cd my-app/src/<span style="color: rgba(0, 0, 0, 1)">app
ng g c counter</span></pre>
</div>
<p>进入 counter.ts, 它目前长这样</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629110405942-2075692234.png"></p>
<p>Counter 是一个类,我们要用它来描述下面这个 UI 组件。<strong>(Thinking in Angular Way)</strong></p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230404183500407-1119040844.png"></p>
<p>以面向对象的方式来看,中间的 number 可以用一个属性 (property) 来表示。</p>
<p>点击左右 button,中间的 number 会累加累减,这个可以用 minus plus 方法 (method) 来表示。</p>
<p>好,添加 number 属性和 minus plus 方法到 Counter 里。</p>
<pre class="language-javascript highlighter-hljs"><code>export class Counter {
// 一个属性代表中间的 number
protected number = 0;
// 一个累减方法代表左边的 minus button
protected minus() {
this.number--; // 累减 number
}
// 一个累加方法代表右边的 plus button
protected plus() {
this.number++; // 累加 number
}
}</code></pre>
<p>注:你可能会好奇,为什么属性和方法前面要加上 <code>protected</code>?</p>
<p>这纯粹是我个人的代码风格而已。</p>
<p>至于 Angular 的代码风格,我会在后面的章节 《Coding Style Guide 编码风格》另外讲解,这里大家先不用在意。</p>
<p>class 搞定了,接下来我们进入 counter.html</p>
<p>先给它一个初始 HTML (俗称 view)</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>-<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span>1<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>+<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p>接着让它和 Counter 对象 (俗称 view model) 关联起来 (俗称 binding)。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span>{{ number }}<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p><code>{{ number }}</code> 是其中一种 Angular 关联语法 (binding syntax),它的意思是把 <code>counter.number</code> 写入到 <code><span></code> 里。</p>
<p>类似这样</p>
<pre class="language-javascript highlighter-hljs"><code>const counter = new Counter(); // 实例化组件,得到 view model
span.textContent = counter.number.toString(); // 做 binding 渲染</code></pre>
<p>接着是左右两个 <code>button</code> 的点击事件,它们要关联到 Counter 对象的 <code>minus</code> 和 <code>plus</code> 方法。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">(click)</span><span style="color: rgba(0, 0, 255, 1)">="minus()"</span><span style="color: rgba(0, 0, 255, 1)">></span>-<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span>{{ number }}<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">(click)</span><span style="color: rgba(0, 0, 255, 1)">="plus()"</span><span style="color: rgba(0, 0, 255, 1)">></span>+<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p>注:这里我们暂不深入这些 binding syntax (看懂表面意思就够了),先继续往下。</p>
<p>接着加点 styles,让它美观。</p>
<p>添加 class 到 HTML 作为 CSS selector</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="minus" </span><span style="color: rgba(255, 0, 0, 1)">(click)</span><span style="color: rgba(0, 0, 255, 1)">="minus()"</span><span style="color: rgba(0, 0, 255, 1)">></span>-<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">span </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="number"</span><span style="color: rgba(0, 0, 255, 1)">></span>{{ number }}<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="plus"</span><span style="color: rgba(255, 0, 0, 1)"> (click)</span><span style="color: rgba(0, 0, 255, 1)">="plus()"</span><span style="color: rgba(0, 0, 255, 1)">></span>+<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p>进入 counter.scss</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(128, 0, 0, 1)">:host </span>{<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> flex</span>;<span style="color: rgba(255, 0, 0, 1)">
gap</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;<span style="color: rgba(255, 0, 0, 1)">
.number {
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 128px</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> 64px</span>;<span style="color: rgba(255, 0, 0, 1)">
border</span>:<span style="color: rgba(0, 0, 255, 1)"> 1px solid gray</span>;<span style="color: rgba(255, 0, 0, 1)">
font-size</span>:<span style="color: rgba(0, 0, 255, 1)"> 36px</span>;<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> grid</span>;<span style="color: rgba(255, 0, 0, 1)">
place-items</span>:<span style="color: rgba(0, 0, 255, 1)"> center</span>;
}<span style="color: rgba(128, 0, 0, 1)">
.minus, .plus </span>{<span style="color: rgba(255, 0, 0, 1)">
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 64px</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> 64px</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}</span></pre>
</div>
<p>至此,Counter Component 的部分就算完成了。</p>
<p>接着是如何使用它,我们进入 app.html 写上</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">app-counter </span><span style="color: rgba(0, 0, 255, 1)">/></span></pre>
</div>
<p>这个 <code><app-counter /></code> element 对应的是 Counter 组件 metadata 里的 css selector </p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629113341183-304994384.png"></p>
<p>@Component decorator 负责定义这个组件的 "元信息" (metadata),比如这个组件的 .html 和 .scss file 的路径等等。</p>
<p>此外,我们还需要进入 app.ts</p>
<pre class="language-javascript highlighter-hljs"><code>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: ,
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App {}</code></pre>
<p>这里有一个小知识点:</p>
<p>透过 CSS selector 来配对组件的使用,是 Angular 跟随 Web Components 的做法。</p>
<p>但相比其它框架 (如 React、Svelte),这种写法相对比较繁琐。</p>
<p>也因此,在未来 (maybe v21, v22),Angular 团队计划推出 selectorless 的写法,这个等它正式推出了以后,我会再回来讲解。</p>
<p>最后跑起来</p>
<div class="cnblogs_code">
<pre>ng serve --open</pre>
</div>
<p>效果</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230404183326025-1340632211.gif"></p>
<h3>Signal-based 写法</h3>
<p>讲点题外话。</p>
<p>虽然上面的代码一切工作正常,但从 Angular v20 开始,它已经不是主流写法了。</p>
<p>现在流行的是 Signal-based 写法</p>
<pre class="language-javascript highlighter-hljs"><code>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);
}
}</code></pre>
<p>其实也没有太大的区别,主要是把 property 的值改成 Signals 而已。</p>
<p>还有 counter.html</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)"><!--</span><span style="color: rgba(0, 128, 0, 1)"> <span class="number">{{ number }}</span> </span><span style="color: rgba(0, 128, 0, 1)">--></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">span </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="number"</span><span style="color: rgba(0, 0, 255, 1)">></span>{{ number() }}<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">span</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p>Signal 是 getter,因此这里也要改成 method call (调用) 形式。</p>
<h4>why Signal-based?</h4>
<p>你可能会好奇,既然两种写法都可以正常运作,为什么还要跟风使用 Signal-based 写法呢?</p>
<p>而且 Signal-based 写法一点都不优雅,可读性甚至还下降了🤔。</p>
<p>这其实跟 Angular 的 Change Detection 机制有关,后面的章节会详细讲解。</p>
<p>我们现在只需要知道:虽然目前看起来两种写法都能正常工作,但未来可能就不一定了。</p>
<p>总之,Best Practice 是:只要组件的属性有用于 Template (count.html) binding syntax,那就应该优先使用 Signals。</p>
<p> </p>
<h2>Angular Components vs Web Components</h2>
<p>透过对比,我们可以看出 Angular 团队在设计 Angular Components 时的思路 -- 他们如何看待 Web Components 的缺陷、如何保留 Web Components 的设计理念、又如何完善 Web Components。</p>
<p>最终保留了什么、丢弃了什么、增加了什么?搞清楚这些对学习和使用 Angular 非常重要,正所谓 Thinking in Angular Way 就是这样来的。</p>
<p>我们先回顾 Web Components 的整体流程</p>
<p>1. 定义 class Counter </p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250707163355907-260662102.png"></p>
<p>2. 做 Shadow DOM 隔离 CSS styles</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250707163448332-1047910535.png"></p>
<p>3. ajax 获取 template 和 style (如果你不介意直接写 raw HTML 和 CSS 在 TS 的话,则可以省略掉这一步)</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250707163710829-1115203346.png"></p>
<p>4. 搞事件监听和渲染 (DOM 操作)</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250707163845211-1203349171.png"></p>
<p>5. define element</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250707163917885-775996928.png"></p>
<p>上面 5 个 steps,Angular 全都实现了。</p>
<p>只不过大部分实现代码都被隐藏了起来,我们主要写的是声明代码。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629125342981-1494515297.png"></p>
<p>短短的几行代码,Angular 就 "声明" 了以下 4 个 steps</p>
<ol>
<li>
<p>定义 class Counter</p>
<p>@Component 表示这个 class 是一个 Component</p>
</li>
<li>
<p>做 Shadow DOM 隔离 CSS styles</p>
<p>by default 所有 Angular Component 都是 CSS styles 隔离的。</p>
<p>不过它并不是透过 Shadow DOM 实现的,这部分我们留以后再详细讲解。</p>
</li>
<li>
<p>ajax 获取 template 和 style</p>
<p>Angular 会在 compile 阶段去链接 .html 和 .scss file</p>
<p>@Component.templateUrl 和 styleUrl 负责声明 files 的路径</p>
</li>
<li>
<p>define component</p>
<p>@component.selector 声明了匹配组件的 css selector</p>
</li>
</ol>
<p>上面这几个步骤,我们完全不需要写实现代码,只要写好声明代码,剩下的交给 Angular 就行了。</p>
<p>还有最后一个 step 是:搞事件监听和渲染 (DOM 操作)</p>
<p>这一步,Angular 使用 MVVM 的方式来 "声明"。</p>
<p>view model</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629131213064-589545013.png"></p>
<p>view and binding</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629131222821-1527787184.png"></p>
<p>同样的,我们不需要写实现代码 (操作 DOM API),只需要 "声明" 就可以了。</p>
<p>从这些对比中,我们可以体会到:Angular 的设计理念就是尽可能地将 "实现代码" 转换为 "声明代码"。</p>
<p>这个动机很好理解,一个项目,代码分两种:</p>
<ol>
<li>
<p>实现代码 (命令式)</p>
<p>实现代码就是让逻辑跑起来的代码。它们的特色就是繁琐、啰嗦、可读性差、难修改、难扩展。</p>
</li>
<li>
<p>声明代码 (声明式)</p>
<p>声明代码的目的不是为了 "实现",而是为了 "表达"。</p>
<p>比如我们写的变量名、函数名、class、interface、metadata、configuration 等等,这些都是为了让我们的程序更可读、更好理解、更易扩展、更方便调修维护。</p>
</li>
</ol>
<p>一个好的框架,应该尽可能的替我们完成实现代码的部分,而我们只需要专注在 "声明" 就可以了。</p>
<p> </p>
<h2>Angular Components !== Web Components</h2>
<p>Angular Components 虽然很大程度上借鉴了 Web Components,但是 Angular 并不是用 Custom Elements + Shadow DOM 来实现 Web Components 的。 </p>
<p>Angular 有一个扩展项目叫 Angular elements,它的方向是 convert Angular Components to 正真的 Web Components,也就是 Custom Elements + Shadow Dom。</p>
<p>但目前这个项目有很多缺失的功能,而且没有得到足够的重视。希望未来不会被砍掉呗...🙄</p>
<p> </p>
<p> </p>
<h2>目录</h2>
<p>上一篇 Angular 20+ 高阶教程 – Dependency Injection 依赖注入</p>
<p>下一篇 Angular 20+ 高阶教程 – Component 组件 の Angular Component vs Custom Elements</p>
<p>想查看目录,请移步 Angular 20+ 高阶教程 – 目录</p>
<p>喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻</p>
<p> </p><br><br>
来源:https://www.cnblogs.com/keatkeat/p/16965777.html
頁:
[1]