独自行走的犀牛 發表於 2023-4-6 21:20:00

Angular 20+ 高阶教程 – Component 组件 の Angular Components vs Custom Elements

<h2>前言</h2>
<p>在上一篇&nbsp;Angular Components vs Web Components&nbsp;中,我们整体对比了 Angular Components 和 Web Components 的区别。</p>
<p>这一篇我们将针对 Custom Elements 的部分继续对比学习。</p>
<p>同样地,请先看我以前写的 DOM – Web Components&nbsp;の Custom Elements。</p>
<p>&nbsp;</p>
<h2>Attribute、Property、Custom Event</h2>
<p>对于一个封装好的 Custom Element,若外部想与其交互,有两种方式:</p>
<ol>
<li>
<p>修改 property 或 attribute (外面影响里面 -- in)</p>
</li>
<li>
<p>监听 custom event (里面影响外面 -- out)</p>
</li>
</ol>
<p>Angular Components 也是如此,只是在 Component 内部,Angular 替我们封装了许多繁琐的实现代码。</p>
<p>&nbsp;</p>
<h2>Cookie Acknowledge Component Step by Step</h2>
<p>我们来做一个 cookie acknowledge 组件。</p>
<p>最终长这样:</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230406223053922-2126814529.gif"></p>
<h3>使用 Cookie Acknowledge Component</h3>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">websiteName</span><span style="color: rgba(0, 0, 255, 1)">="兴杰 Blog"</span><span style="color: rgba(255, 0, 0, 1)"> (acknowledge)</span><span style="color: rgba(0, 0, 255, 1)">="alert($event)"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span></pre>
</div>
<p>"websiteName" 是组件的 attribute / property。它是组件中 <code>&lt;p&gt;</code> Welcome to "兴杰 Blog" 的一部分。</p>
<p>下面这句是 Angular 的 binding syntax</p>
<div class="cnblogs_code">
<pre>(acknowledge)="alert($event)"</pre>
</div>
<p>后面的章节会详细介绍,这里只需要知道它相等于&nbsp;addEventListener 就可以了,类似于:</p>
<pre class="language-javascript highlighter-hljs"><code>document.querySelector('app-cookie-acknowledge')!.addEventListener('acknowledge', (event: string) =&gt; alert(event))</code></pre>
<p>注:Angular 没有强制使用 CustomEvent 作为 event,dispatch 的 event 可以是任何类型,比如一个 string 也可以。</p>
<h3>Create&nbsp;Cookie Acknowledge Component</h3>
<div class="cnblogs_code">
<pre>ng g c cookie-acknowledge</pre>
</div>
<h4>input and output</h4>
<p>cookie-acknowledge.ts</p>
<pre class="language-javascript highlighter-hljs"><code>import { Component, input, output } from '@angular/core';

export class CookieAcknowledge {
readonly websiteName = input.required&lt;string&gt;();
readonly acknowledgeEmitter = output&lt;string&gt;({ alias: 'acknowledge' });
}</code></pre>
<p><code>input</code> 和 <code>output</code> 是 Angular built-in 的特殊函数。</p>
<p>为什么说它们特殊?</p>
<p>因为它们不仅仅用于 runtime。</p>
<p>在 compile 阶段,Angular compiler 会识别出它们并进行特殊编译。</p>
<p>具体怎样编译,我以后有机会再讲解,这里我们只需要知道 <code>input</code> 和 <code>output</code> 的含义就可以了。</p>
<h4>input</h4>
<p>input 代表说,这个属性 "websiteName" 会被用作 element 的 attribute / property。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629181346252-197412043.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629181348850-2065253087.png"></p>
<p>websiteName 属性值是透过 html element attribute 传进来的。</p>
<p>input.required 则表示 element 一定要附上这个 attribute,不然 IDE 会直接报错。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629182211624-1710890515.png"></p>
<h4>output</h4>
<p>有 input 自然就有 output。</p>
<p>input 用于 attribute,output 则用来表示 dispatch event。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202506/641294-20250629181749936-1811392115.png"></p>
<p>alias 是配置别名,如果没有配置的话,属性名 "acknowledgeEmitter" 会被用作 event name。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250708185548484-784714449.png"></p>
<p>cookie-acknowledge.html</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">h1</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>Cookie Acknowledge<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">h1</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>Welcome to {{ websiteName() }}, to allow we track you, please press acknowledge button.<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</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)">="acknowledgeEmitter.emit('Yes, track me!')"</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>Acknowledge<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></pre>
</div>
<p><code>{{ websiteName() }}</code> 是 binding syntax -- 意思是把 websiteName 属性值写入 <code>&lt;p&gt;</code> 中。(另外提一点: input 函数返回的是 InputSignal (Signal 的一种),因此它是 getter 函数,取值需要以调用的方式)</p>
<p><code>(click)</code> 也是 binding syntax -- 意思是 <code>addEventListener('click')</code>。</p>
<p><code>acknowledgeEmitter.emit('Yes, track me!')</code> 是 click event 的 callback,其行为是 dispatch custom event "acknowledge"。</p>
<p>Custom event 通常是 dispatch CustomEvent 对象,传值是透过 <code>event.detail</code> 属性;但 Angular 没有这个限制,我们可以 dispatch 任何类型。</p>
<p>当然,如果我们想 follow custom event 的规范,dispatch CustomEvent 对象也是可以。</p>
<h3>小结</h3>
<p>Angular Components 和 Custom Elements 一样,都是透过 component attribute / property 和 listen event dispatch 来与组件交互。</p>
<p>Angular 用 <code>input</code> 和 <code>output</code> 函数声明对外 (HTML) 开放的属性和可监听的事件,并透过 OutputEmitterRef 对象 dispatch event。</p>
<p>Angular dispatch event 没有强制要求使用 CustomEvent 对象,我们可以 dispatch 任何类型作为 event。</p>
<p>&nbsp;</p>
<h2>input 函数</h2>
<p>element attribute 和 element property 是两个不同的东西。</p>
<h3>attribute &amp; property</h3>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">input </span><span style="color: rgba(255, 0, 0, 1)">readonly</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></pre>
</div>
<p>上面这个是 attribute</p>
<ol>
<li>
<p>它可以有,也可以没有 (没有就是这样 <code>&lt;input&gt;</code>)。</p>
</li>
<li>
<p>它的 value 类型一定是 string。(<code>&lt;input readonly="false"&gt;</code>,value 是 string "false")</p>
</li>
<li>如果只有 attribute key,没有 attribute value (<code>&lt;input readonly&gt;</code>),那它的 value 是 empty string。</li>
</ol>
<pre class="language-javascript highlighter-hljs"><code>const input = document.createElement('input');
console.log(input.readOnly); // false</code></pre>
<p>上面这个是 property</p>
<ol>
<li>
<p>它一定有</p>
</li>
<li>
<p>它的 value 类型是 boolean</p>
</li>
<li>当有 attribute 时 (不管 value 是什么),readOnly 就是 true;反之 readOnly 就是 false。</li>
</ol>
<p>attribute 和 property 是相互关联的:</p>
<ol>
<li>
<p>当有 "readonly" attribute 时,"readOnly" property 就是 true;反之就是 false。</p>
</li>
<li>
<p>当你改变其中一个,另一个也要自动改变</p>
<pre class="language-javascript highlighter-hljs"><code>const input = document.createElement('input');
console.log(input.readOnly);               // by default property 是 false
input.setAttribute('readonly', '');          // set attribute
console.log(input.readOnly);               // property 变成了 true
input.readOnly = false;                      // set property
console.log(input.hasAttribute('readonly')); // attribute 变没了</code></pre>
</li>
</ol>
<h3>input の required</h3>
<p>Angular <code>input</code> 函数的主要职责就是替我们维护组件 attribute 和 property 的映射关系。</p>
<p>上面我们已经看了一个例子</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly websiteName = input.required&lt;string&gt;();
}</code></pre>
<p>这表示 CookieAcknowledge 组件有一个 property "websiteName",相应地,它也会有一个 "websiteName" attribute。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">websiteName</span><span style="color: rgba(0, 0, 255, 1)">="兴杰 Blog"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span></pre>
</div>
<p><code>input.required</code> 的 required 则表示这个 attribute 是必填的。</p>
<p>如果我们忘记填,IDE 会直接报错。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250708212618313-1305238956.png"></p>
<p>如果没有 required 则是这样</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly websiteName: InputSignal&lt;string | undefined&gt; = input&lt;string&gt;();
}</code></pre>
<p>attribute 可以不需要填,当没有 attribute 时,"websiteName" property getter value 会是 undefined。</p>
<p>注:<code>input</code> 返回的是 InputSignal,它继承自 readonly Signal,因此是一个 getter 函数。</p>
<p>如果我们不希望它是 undefined,可以提供一个 default value;当没有 attribute 时,"websiteName" property getter value 就会是 default value。</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly websiteName: InputSignal&lt;string&gt; = input&lt;string&gt;('default value'); // 这样就不会 undefined 了
}</code></pre>
<h3>input の alias</h3>
<p>alias 是一个 <code>input</code> options,它可以让 attribute name 和 property name 不相同,但却可以正确映射。</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly websiteName = input.required&lt;string&gt;({ alias: 'name' });
}</code></pre>
<p>property name 是 "websiteName",但配置了 alias "name",因此 attribute name 是 "name" 而不是 "websiteName"。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">name</span><span style="color: rgba(0, 0, 255, 1)">="兴杰 Blog"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span></pre>
</div>
<h3>input の transform</h3>
<p>transform 也是一个 <code>input</code> options,它的作用是在 set property value 时拦截,并对 value 进行转换 (transform),常用于类型转换。</p>
<p>上面我们说了,attribute value 的类型一定是 string,而 property value 的类型则可以是任何类型,比如说 boolean。</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly required = input.required&lt;boolean&gt;();
}</code></pre>
<p>有一个 "required" property,类型是 boolean。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">required </span><span style="color: rgba(0, 0, 255, 1)">/&gt;</span>         <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">Error: Type 'string' is not assignable to type 'boolean'.</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>

<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">required</span><span style="color: rgba(0, 0, 255, 1)">="true"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span><span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">Error: Type 'string' is not assignable to type 'boolean'.</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>

<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">required</span><span style="color: rgba(0, 0, 255, 1)">="false"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span> <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">Error: Type 'string' is not assignable to type 'boolean'.</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>

<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">required</span><span style="color: rgba(0, 0, 255, 1)">=""</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>      <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">Error: Type 'string' is not assignable to type 'boolean'.</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span></pre>
</div>
<p>无论我们怎样设置 "required" attribute 它都会报错,因为 attribute 是 string,但 property 却是 boolean。</p>
<p>有两种方法可以解决:</p>
<ol>
<li>
<p>binding syntax (模板语法)</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="true"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="false"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span></pre>
</div>
<p><code></code> 是 binding syntax,它的作用是 set value to property。</p>
<p>注意:不是 set value to attribute 哦,是 set value to property。</p>
<p>另外,它的 value 是 JavaScript 表达式 (Angular 限缩版的),而不是普通的 string。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="'兴杰 Blog'"</span><span style="color: rgba(255, 0, 0, 1)"> </span><span style="color: rgba(0, 0, 255, 1)">="true"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span> </pre>
</div>
<p>注意看,<code></code>的 value "'兴杰 Blog'" 有多一层 single quote,因为它是 JavaScript 表达式。</p>
<p>binding syntax 博大精深,以后会单独写一篇来教,这里点到为止。</p>
</li>
<li>
<p>transform</p>
<pre class="language-javascript highlighter-hljs"><code>import { booleanAttribute, Component, input } from '@angular/core';

export class CookieAcknowledge {
readonly required = input.required({ transform: booleanAttribute });
}</code></pre>
<p><code>booleanAttribute</code> 是一个把 value 转换成 boolean 的函数,它是 Angular built-in 的,源码在 coercion.ts</p>
<img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250708234107742-956285653.png">
<p>如果 value 是 boolean 那直接返回。</p>
<p>如果 value 是 null or undefined or 'false' 就返回 false&nbsp;</p>
<p>其它情况则返回 true</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">required </span><span style="color: rgba(0, 0, 255, 1)">/&gt;</span>         <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">true</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">required</span><span style="color: rgba(0, 0, 255, 1)">=""</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>      <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">true</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">required</span><span style="color: rgba(0, 0, 255, 1)">="true"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span><span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">true</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)">required</span><span style="color: rgba(0, 0, 255, 1)">="false"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span> <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">false</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span></pre>
</div>
<p>string value 除了 'false' 以外,其它的都会 transform 成 true。</p>
<p>搭配 binding syntax,它会是这样</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="null"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>      <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">false</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="undefined"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span> <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">false</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="false"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>   <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">false</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="'false'"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span>   <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">false</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>

<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="0"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span><span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">true</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="1"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span><span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">true</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="[]"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span> <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">true</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="{}"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span> <span style="color: rgba(0, 128, 0, 1)">&lt;!--</span><span style="color: rgba(0, 128, 0, 1)">true</span><span style="color: rgba(0, 128, 0, 1)">--&gt;</span></pre>
</div>
<p>首先,transform 是 for property value 而不是 attribute value,所以采用 binding syntax direct set value to property 也依然会 transform。</p>
<p>除了头 4 个 null, undefined, false, 'false' 会 transform 成 false 以外,其它的 value 一律都 transform 成 true。</p>
</li>
</ol>
<p>除了 <code>booleanAttribute</code>,Angular 还有一个 built-in 的 transform 函数 -- <code>numberAttribute</code>。</p>
<p>顾名思义,它是用来 transform to number 的,源码在 coercion.ts</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250709004639100-491182371.png"></p>
<p>主要是透过 <code>parseFloat</code> 和 <code>Number</code> 做转化,转换失败会 fallback to <code>NaN</code>。</p>
<h3>undefined as no set attribute</h3>
<pre class="language-javascript highlighter-hljs"><code>function createComponent(websiteName = 'default value') {}</code></pre>
<p>这是一个函数,它有一个 optional parameter with default value。</p>
<pre class="language-javascript highlighter-hljs"><code>createComponent();
createComponent(undefined);</code></pre>
<p>调用函数时,可以不传参数,也可以传 undefined,这两个方式是等价的,websiteName 最终都是 'default value'。</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly websiteName = input('default value');
}</code></pre>
<p>"websiteName" property 是 optional with default value。</p>
<p>我们可以不设置 "websiteName" attribute,像这样</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(0, 0, 255, 1)">/&gt;</span> </pre>
</div>
<p>但却不能透过 binding syntax 直接设置 property value to undefined</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202507/641294-20250709013145802-1479516188.png"></p>
<p>这意味着,我们无法 "动态" 的决定是否要设置 attribute 给它。</p>
<p>解决方案是配置 transform</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly websiteName = input('default value', {
    transform: (value : string | undefined | null) =&gt; value == null ? 'default value' : value
});
}</code></pre>
<p>如果传入的是 null or undefined 就 transform to 'default value'。</p>
<p>虽然代码有点繁琐,不优雅,但勉强能用。</p>
<h3>input の compilation</h3>
<p><code>input</code>除了是一个函数,它在 compile 阶段还是一个识别符。</p>
<p>因此,下面这个写法会报错</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly websiteName: InputSignal&lt;string&gt;;

constructor() {
    // Error : Unsupported call to the input.required function. This function can only be called in the initializer of a class member.
    this.websiteName = input.required&lt;string&gt;();
}
}</code></pre>
<p>从 JavaScript 的角度看,这个写法完全没有问题。</p>
<p>但 IDE 却报错了,因为 Angular compiler 无法解析这么复杂的写法。</p>
<p>所以,我们只能这样定义 input</p>
<pre class="language-javascript highlighter-hljs"><code>export class CookieAcknowledge {
readonly websiteName = input.required&lt;string&gt;();
}</code></pre>
<p>&nbsp;</p>
<h2>output 函数</h2>
<p>todo 写 custom element + custom event 例子 (用上面例子,但不需要写完整)</p>
<p>然后用 output 实现</p>
<p>&nbsp;</p>
<p>接下来写 lifecycle custom element vs angular 的,可以尝试引入 @Attribute 那个 inject(token)</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2>@Attribute</h2>
<p>@Input 是用来拿 property 的,@Attribute 是用来拿 attribute 的。</p>
<p>看例子:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-item </span><span style="color: rgba(255, 0, 0, 1)">name</span><span style="color: rgba(0, 0, 255, 1)">="iPhone 14"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span></pre>
</div>
<p>有一个 Item 组件,它有一个 attrbute name,value 是 'iPhone 14'。</p>
<p>在 AppItem 组件 constructor 参数使用 @Attribute decorator</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">export class ItemComponent {
constructor(
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 1. 在 constructor 使用 @Attribute decorator 获取 name attribute</span>
    @Attribute('name'<span style="color: rgba(0, 0, 0, 1)">) name: string,
) {
    console.log(name); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 'iPhone 14'</span>
<span style="color: rgba(0, 0, 0, 1)">}
}</span></pre>
</div>
<p>这样就可以拿到 attribute value 了。</p>
<p>这里有 2 个点要注意:</p>
<ol>
<li>
<p>@Attribute 是 apply 在 constructor 参数,而不是像 @Input 那样 appy 在 property。</p>
</li>
<li>
<p>@Attribute 不可以和 @Input 撞,两者只能有一个存在。</p>
</li>
<li>
<p>@Attribute 没有 binding 概念,它一定是 static string value。</p>
</li>
</ol>
<p>@Attribute 相对 @Input 来说是非常冷门的,组件一般上很少会用&nbsp;@Attribute,指令 + 原生 DOM 才可能会用到&nbsp;@Attribute。(指令后面章节才会教)</p>
<p>&nbsp;</p>
<h2>@Input 和 @Output Decorator 正在被放弃</h2>
<p>Decorator 目前普遍不受待见,两大原因。&nbsp;</p>
<p>1. ECMA 把 Decorator 拆成了两个版本,而且第二个还没有定稿。</p>
<p>2. 函数式的天下,Decorator 自然也变成小众了。</p>
<p>所以,Angular 从 v14 开始就有了弃暗投明的想法。一步一步靠拢 react、vue、solid、svelte 等等前端技术。</p>
<p>当然所谓的靠拢只是在开发体验上,写法上不同而已,概念是靠拢不了的。</p>
<h3>metadata 写法</h3>
<p>metadata inputs 写法</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">@Component({
inputs: [
    { name: </span>'boolValue', alias: 'value', required: <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">, transform: booleanAttribute }
]
})</span></pre>
</div>
<p>取代了原本的 @Input,接口都一样,只是搬家而已。</p>
<p>我个人是觉得没有必要这么写啦,逻辑分开有时候也很乱,建议大家还是等 Signal-based Component 吧。</p>
<h3>Signal-based&nbsp;写法</h3>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">export class CardComponent {
title </span>= input&lt;<span style="color: rgba(0, 0, 255, 1)">string</span>&gt;(<span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<p>上面这个就是 Signal-based Component Input 的写法。title 是属性,input 是全局函数。</p>
<p>这个写法和 DI 的&nbsp;inject 函数非常相识。</p>
<p>Angular v17.1.0 正式推出了 Signal-based Input,想学可以看这篇 Signals&nbsp;# Signal-based Input。</p>
<p>&nbsp;</p>
<h2>Angular Component Lifecycle vs Custom Elements Lifecycle</h2>
<h3>Custom Elements Lifecycle</h3>
<p>Custom Elements 有 3 个 Lifecycle Hook.</p>
<p>1. connectedCallback&nbsp;当被 append to document</p>
<p>2.&nbsp;disconnectedCallback&nbsp;当被 remove from document</p>
<p>3.&nbsp;attributeChangedCallback&nbsp;当监听的 attributes add, remove, change value 的时候触发</p>
<h3>Angular Component Lifecycle</h3>
<p>Angular 有好多 Lifecycle Hook...我先介绍 5 个基本的,后面的章节还会介绍其它的。</p>
<p>首先,我们添加一些交互</p>
<p>一个 change attribute 和一个 remove element</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230407010542394-632652876.gif"></p>
<p>app.component.ts</p>
<div class="cnblogs_code"><img src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_c663fb6f-0faf-45ec-8ef8-893b720e479a" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 0, 0, 1)">export class AppComponent {
alert </span>=<span style="color: rgba(0, 0, 0, 1)"> alert;
websiteName </span>= '兴杰 Blog'<span style="color: rgba(0, 0, 0, 1)">;
showCookieAcknowledge </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
}</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>app.component.html</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_13a3a3c5-481b-4bbb-9151-b02695cb6855" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="container"</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="action"</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
    <span style="color: rgba(0, 0, 255, 1)">&lt;</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)">="websiteName = 'Derrick\'s Blog'"</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>Change Website Name<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
    <span style="color: rgba(0, 0, 255, 1)">&lt;</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)">="showCookieAcknowledge = false"</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>Delete Cookie Acknowledge<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>
<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span><span style="color: rgba(0, 0, 0, 1)">
@if (showCookieAcknowledge) {
    </span><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge </span><span style="color: rgba(255, 0, 0, 1)"></span><span style="color: rgba(0, 0, 255, 1)">="websiteName"</span><span style="color: rgba(255, 0, 0, 1)"> (acknowledge)</span><span style="color: rgba(0, 0, 255, 1)">=" alert($event)"</span><span style="color: rgba(0, 0, 255, 1)">&gt;&lt;/</span><span style="color: rgba(128, 0, 0, 1)">app-cookie-acknowledge</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span><span style="color: rgba(0, 0, 0, 1)">
}
</span><span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>不要在意 @if 和 ,后面章节会教,我们 focus Lifecycle Hook 就好了。</p>
<p>添加 5 个 Lifecycle Hook 到 Cookie Acknowledge Component</p>
<p>cookie-acknowledge.component.ts</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_9ce233e0-c373-49ba-9245-e0ce49f3eb8a" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 0, 0, 1)">export class CookieAcknowledgeComponent
implements OnInit, OnChanges, OnDestroy, AfterViewInit
{
@Input({ required: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)"> })
websiteName</span>!<span style="color: rgba(0, 0, 0, 1)">: string;

@Output(</span>'acknowledge'<span style="color: rgba(0, 0, 0, 1)">)
acknowledgeEmitter </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> EventEmitter&lt;string&gt;<span style="color: rgba(0, 0, 0, 1)">();

constructor() {
    console.log(</span>'constructor', '@Input value not ready yet'<span style="color: rgba(0, 0, 0, 1)">);
    console.log(
      </span>'constructor, this.websiteName === undefined'<span style="color: rgba(0, 0, 0, 1)">,
      </span><span style="color: rgba(0, 0, 255, 1)">this</span>.websiteName ===<span style="color: rgba(0, 0, 0, 1)"> undefined
    );
}

ngOnInit(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
    console.log(</span>'OnInit', '@Input value ready'<span style="color: rgba(0, 0, 0, 1)">);
    console.log(
      </span>'OnInit, this.websiteName !== undefined'<span style="color: rgba(0, 0, 0, 1)">,
      </span><span style="color: rgba(0, 0, 255, 1)">this</span>.websiteName !==<span style="color: rgba(0, 0, 0, 1)"> undefined
    );
}

ngOnChanges(changes: SimpleChanges): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
    </span><span style="color: rgba(0, 0, 255, 1)">if</span> ('websiteName' <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> changes) {
      const change </span>= changes['websiteName'<span style="color: rgba(0, 0, 0, 1)">];
      </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (change.firstChange) {
      console.log(
          </span>'OnChanges first change'<span style="color: rgba(0, 0, 0, 1)">,
          `prev: ${change.previousValue}, curr: ${change.currentValue}`
      );
      console.log(
          </span>'OnChanges first change, paragraph appended'<span style="color: rgba(0, 0, 0, 1)">,
          document.querySelector(</span>'.paragraph') !== <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">
      );
      console.log(
          </span>'OnChanges first change, paragraph data binding no complete yet'<span style="color: rgba(0, 0, 0, 1)">,
          document.querySelector(</span>'.paragraph')!.textContent === ''<span style="color: rgba(0, 0, 0, 1)">
      );
      } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
      console.log(
          </span>'OnChanges second change'<span style="color: rgba(0, 0, 0, 1)">,
          `previous value : ${change.previousValue}`
      );
      console.log(
          </span>'OnChanges second change'<span style="color: rgba(0, 0, 0, 1)">,
          `current value : ${change.currentValue}`
      );
      }
    }
}

ngAfterViewInit(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
    console.log(
      </span>'AfterViewInit, paragraph data binding completed'<span style="color: rgba(0, 0, 0, 1)">,
      document.querySelector(</span>'.paragraph')!.textContent !== ''<span style="color: rgba(0, 0, 0, 1)">
    );
}

ngOnDestroy(): </span><span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> {
    console.log(</span>'OnDestroy'<span style="color: rgba(0, 0, 0, 1)">, `element has been removed`);
    console.log(
      </span>'OnDestroy, query app-cookie-acknowledge === null'<span style="color: rgba(0, 0, 0, 1)">,
      document.querySelector(</span>'app-cookie-acknowledge') === <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">
    );
}
}</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>不需要看 code, 下面我们看 runtime console 就可以了。</p>
<h4>1. constructor</h4>
<p>组件是 class,第一个被 call 的自然是 constructor。在这个阶段 @Input 的是还没有输入值的,它是 undefined。</p>
<p>template 也还没有 append 到 document 里。</p>
<h4>2.&nbsp;ngOnChanges&nbsp;(first time)</h4>
<p>ngOnChanges 对应 Custom Elements 的&nbsp;attributeChangedCallback。每当 attribute 变化的时候就会 call。</p>
<p>websiteName 从开始的 undefined 变成 '兴杰 blog' 后就会触发 first time onchanges。</p>
<p>注:Custom Elements 的 attributeChangedCallback 是没有 first time call 的,它只有后续改变才会 call。</p>
<p>另外,template 在这个阶段已经 append to document 了,但如果有 binding data 的部分则还没有完成。</p>
<p>比如这句</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">p </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="paragraph"</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span>Welcome to {{ websiteName }}, to allow we track you, please press acknowledge button.<span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">p</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></pre>
</div>
<p>假如这个阶段我们 document.query .paragraph 会获得 element,但是 element.textContent 将会是 empty string。</p>
<h4>3. ngOnInit</h4>
<p>ngOnInit 对应 Custom Elements&nbsp;connectedCallback。在这个阶段 @Input 的 value 已经有值了。</p>
<p>通常我们会在这个阶段发 ajax 取 data 什么的。</p>
<p>这个阶段 binding data 依旧还没开始,paragraph.textContent 依然是 emtpty string。</p>
<h4>4.&nbsp;ngAfterViewInit</h4>
<p>这个阶段 binding data 就完成了。paragraph.textContent 已经有包括 websiteName '兴杰 blog' 在内的 text 了。</p>
<p>在这个阶段我们不应该再去修改 view model 了,如果修改它会报错的。</p>
<p><img alt="" data-src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230414134932970-992858144.png"></p>
<p>如果真的有需要修改的话,那么就用&nbsp;setTimeout 让它开启下一个循环。</p>
<h4>5. ngOnChanges(second time)</h4>
<p>当 @Input 值被修改后,又会触发&nbsp;ngOnChanges。记得,这个阶段 data binding 是还没有完成的哦。</p>
<p>如果想监听到 data binding 完成,可以使用&nbsp;ngAfterViewChecked,但这个比较冷门,我不想在这里展开,以后的章节会详解介绍。</p>
<h4>6.&nbsp;ngOnDestroy</h4>
<p>ngOnDestroy 对应 Custom Elements 的&nbsp;disconnectedCallback,这个阶段 element 已经从 document 移除了。</p>
<p>我们通常会在这里做一些释放资源的动作。</p>
<h4>console 结果</h4>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230407015150098-689302291.png"></p>
<p>&nbsp;</p>
<h2>Future (Signal-based Components)</h2>
<p>参考: Github – Sub-RFC 3: Signal-based Components</p>
<p>这篇提到的 @Input @Output 还有 Lifecycle Hook 写法,在未来(一年后)会有很大的变化。</p>
<p>因为 Angular 正在向 React 学习,希望透过改变开放体验来吸引一些新用户。&nbsp;</p>
<p>感受一下:&nbsp;</p>
<p>@Input</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230407100616855-1943334088.png"></p>
<p>Angular v17.1.0 正式推出了 Input Signal,想学可以看这篇&nbsp;Signals。</p>
<p>@Output</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230407100626838-858489814.png"></p>
<p>Lifecycle Hook</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202304/641294-20230407100638528-1554471898.png"></p>
<p>改变的方向是尽可能移除 Decorator 和增加函数式特性,同时减少面向对象特性。</p>
<p>虽然写法上区别很大,但是底层思路改变的不多,而且 Angular 依然会保留目前的写法很长一度时间(maybe 2 more years)。</p>
<p>所以短期内大家还是可以学习和安心使用的。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2>目录</h2>
<p>上一篇&nbsp;Angular 20+ 高阶教程 – Component 组件 の Angular Component vs Web Component</p>
<p>下一篇&nbsp;Angular 20+ 高阶教程 – Component 组件 の Angular Component vs Shadow DOM (CSS Isolation &amp; slot)</p>
<p>想查看目录,请移步&nbsp;Angular 20+ 高阶教程 – 目录</p>
<p>喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding&nbsp;😊💻</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/keatkeat/p/17294252.html
頁: [1]
查看完整版本: Angular 20+ 高阶教程 – Component 组件 の Angular Components vs Custom Elements