原文: Angular Elements: how does this magic work under the hood?

现在,Angular Elements 这个项目已经在社区引起一定程度的讨论。这是显而易见的,因为 Angular Elements 提供了很多开箱即用的、十分强大的功能:

  • 通过使用原生的 HTML 语法来使用 Angular Elements —— 这意味着不再需要了解 Angular 的相关知识
  • 它是自启动的,并且一切都可以按预期那样运作
  • 它符合 Web Components 规范,这意味着它可以在任何地方使用
  • 虽然你没有使用 Angular 开发整个网站,但你仍然可以从 Angular Framework 这个庞大的体系中收益

@angular/elements这个包提供可将 Angular 组件转化为原生 Web Components 的功能,它基于浏览器的 Custom Elements API 实现。Angular Elements 提供一种更简洁、对开发者更友善、更快乐地开发动态组件的方式 —— 在幕后它基于同样的机制(指创建动态组件),但隐藏了许多样板代码。

关于如何通过 @angular/elements 创建一个 Custom Element,已经有大量的文章进行阐述,所以在这篇文章将深入一点,对它在 Angular 中的具体工作原理进行剖析。这也是我们开始研究 Angular Elements 的一系列文章的原因,我们将在其中详细解释 Angular 如何在 Angular Elements 的帮助下实现 Custom Elements API。

Custom Elements(自定义元素)

要了解更多关于 Custom Elements 的知识,可以通过 developers.google 中的这篇文章进行学习,文章详细介绍了与 Custom Elements API 相关的内容。

这里针对 Custom Elements,我们使用一句话来概括:

使用 Custom Elements,web 开发者可以创建一个新的 HTML 标签、增加已有的 HTML 标签以及继承其他开发者所开发的组件。

原生 Custom Elements

让我们来看看下面的例子,我们想要创建一个拥有 name 属性的 app-hello HTML 标签。可以通过 Custom Elements API 来完成这件事。在文章的后续章节,我们将演示如何使用 Angular 组件的 @Input 装饰器与 这个 name 属性保持同步。但是现在,我们不需要使用 Angular Elements 或者 ShadowDom 或者使用任何关于 Angular 的东西来创建一个 Custom Element,我们仅使用原生的 Custom Components API。

首先,这是我们的 HTML 标记:

<hello-elem name="Custom Elements"></hello-elem>
复制代码

要实现一个 Custom Element,我们需要分别实现如下在标准中定义的 hooks:

callback summary
constructor 如果需要的话,可在其中初始化 state 或者 shadowRoot,在这篇文章中,我们不需要
connectedCallback 在元素被添加到 DOM 中时会被调用,我们将在这个 hook 中初始化我们的 DOM 结构和事件监听器
disconnectedCallback 在元素从 DOM 中被移除时被调用,我们将在这个 hook 中清除我们的 DOM 结构和事件监听器
attributeChangedCallback 在元素属性变化时被调用,我们将在这个 hook 中更新我们内部的 dom 元素或者基于属性改变后的状态

如下是我们关于 Hello Custom Element 的实现代码:

class AppHello extends HTMLElement {constructor() {super();}// 这里定义了那些需要被观察的属性,当这些属性改变时,attributeChangedCallback 这个 hook 会被触发static get observedAttributes() {return ['name']; }// getter to do a attribute -> property reflectionget name() {return this.getAttribute('name');}// setter to do a property -> attribute reflection// 通过 setter 来完成类属性到元素属性的映射操作set name(val) {this.setAttribute('name', val);}connectedCallback() {this.div = document.createElement('div');this.text = document.createTextNode(this.name || '');this.div.appendChild(this.text);this.appendChild(this.div);}disconnectedCallback() {this.removeChild(this.div);}attributeChangedCallback(attrName, oldVal, newVal) {if (attrName === 'name' && this.text) {this.text.textContent = newVal;}}
}customElements.define('hello-elem', AppHello);
复制代码

这里是可运行实例的链接。这样我们就实现了第一版的 Custom Element,回顾一下,这个 app-hellp 标签包含一个文本节点,并且这个节点将会渲染通过 app-hello 标签 name 属性传递进来的任何内容,这一切仅仅基于原生 javascript。

将 Angular 组件导出为 Custom Element

既然我们已经了解了关于实现一个 HTML Custom Element 所涉及的内容,让我们来使用 Angular实现一个相同功能的组件,之后再使它成为一个可用的 Custom Element。

首先,让我们从一个简单的 Angular 组件开始:

import { Component, Input } from '@angular/core';@Component({selector: 'app-hello',template: `<div>{{name}}</div>`
})
export class HelloComponent  {@Input() name: string;
}
复制代码

正如你所见,它和上面的例子在功能上一模一样。

现在,要将这个组件包装为一个 Custom Element,我们需要创建一个 wrapper class 并实现所有 Custom Elements 中定义的 hooks:

class HelloComponentClass extends HTMLElement {constructor() {super();}static get observedAttributes() {}connectedCallback() {}disconnectedCallback() {}attributeChangedCallback(attrName, oldVal, newVal) {}
}
复制代码

下一步,我们要做的是桥接 HelloComponentHelloComponentClass。它们之间的桥会将 Angular Component 和 Custom Element 连接起来,如图所示:

要完成这座桥,让我们来依次实现 Custom Elements API 中所要求的每个方法,并在这个方法中编写关于绑定 Angular 的代码:

callback summary angular part
constructor 初始化内部状态 进行一些准备工作
connectedCallback 初始化视图、事件监听器 加载 Angular 组件
disconnectedCallback 清除视图、事件监听器 注销 Angular 组件
attributeChangedCallback 处理属性变化 处理 @Input 变化

1. constructor()

我们需要在 connectedCallback() 方法中初始化 HelloComponent,但是在这之前,我们需要在 constructor 方法中进行一些准备工作。

顺便,关于如何动态构造 Angular 组件可以通过阅读Dynamic Components in Angular这篇文章进行了解。它其中阐述的运作机制和我们这里使用的一模一样。

所以,要让我们的 Angular 动态组件能够正常工作(需要 componentFactory 能够被编译),我们需要将 HelloComponent 添加到 NgModuleentryComponents 属性(它是一个列表)中去:

@NgModule({imports: [BrowserModule],declarations: [HelloComponent],entryComponents: [HelloComponent]
})
export class CustomElementsModule {ngDoBootstrap() {}
}
复制代码

基本上,调用 prepare() 方法会完成两件事:

  • 它会基于组件的定义初始化一个 factoryComponent 工厂方法
  • 它会基于 Angular 组件的 inputs 初始化 observedAttributes,以便我们在 attributeChangedCallback() 中完成我们需要做的事
class AngularCustomElementBridge {prepare(injector, component) {this.componentFactory = injector.get(ComponentFactoryResolver).resolveComponentFactory(component);// 我们使用 templateName 来处理 @Input('aliasName') 这种情形this.observedAttributes = componentFactory.inputs.map(input => input.templateName); }
}
复制代码

2. connectedCallback()

在这个回调函数中,我们将看到:

  • 初始化我们的 Angular 组件(就如创建动态组件那样)
  • 设置组件的初始 input 值
  • 在渲染组件时,触发脏检查机制
  • 最后,将 HostView 增加到 ApplicationRef

如下是实战代码:

class AngularCustomElementBridge {initComponent(element: HTMLElement) {// 首先我们需要 componentInjector 来初始化组件// 这里的 injector 是 Custom Element 外部的注入器实例,调用者可以在这个实例中注册// 他们自己的 providersconst componentInjector = Injector.create([], this.injector);this.componentRef = this.componentFactory.create(componentInjector, null, element);// 然后我们要检查是否需要初始化组件的 input 的值// 在本例中,在 Angular Element 被加载之前,user 可能已经设置了元素的属性// 这些值被保存在 initialInputValues 这个 map 结构中this.componentFactory.inputs.forEach(prop => this.componentRef.instance[prop.propName] = this.initialInputValues[prop.propName]);// 之后我们会触发脏检查,这样组件在事件循环的下一个周期会被渲染this.changeDetectorRef.detectChanges();this.applicationRef = this.injector.get(ApplicationRef);// 最后,我们使用 attachView 方法将组件的 HostView 添加到 applicationRef 中this.applicationRef.attachView(this.componentRef.hostView);}
}
复制代码

3. disconnectedCallback()

这个十分容易,我们仅需要在其中注销 componentRef 即可:

class AngularCustomElementBridge {destroy() {this.componentRef.destroy();}
}
复制代码

4. attributeChangedCallback()

当元素属性发生改变时,我们需要相应地更新 Angular 组件并触发脏检查:

class AngularCustomElementBridge {setInputValue(propName, value) {if (!this.componentRef) {this.initialInputValues[propName] = value;return;}if (this.componentRef[propName] === value) {return;}this.componentRef[propName] = value;this.changeDetectorRef.detectChanges();}
}
复制代码

5. Finally, we register the Custom Element

customElements.define('hello-elem', HelloComponentClass);
复制代码

这是一个可运行的例子链接。

总结

这就是根本思想。通过在 Angular 中使用动态组件,我们简单实现了 Angular Elements 所提供的基础功能,重要的是,没有使用 @angular/element 这个库。

当然,不要误解 —— Angular Elements 的功能十分强大。文章中所涉及的所有实现逻辑在 Angular Elements 都已被抽象化,使用这个库可以使我们的代码更优雅,可读性和维护性也更好,同时也更易于扩展。

以下是关于 Angular Elements 中一些模块的概要以及它们与这篇文章的关联性:

  • create-custom-element.ts:这个模块实现了我们在这篇文章中讨论的关于 Custom Element 的几个回调函数,同时它还会初始化一个 NgElementStrategy 策略类,这个类会作为连接 Angular Component 和 Custom Elements 的桥梁。当前,我们仅有一个策略 —— component-factory-strategy.ts —— 它的运作机制与本文例子中演示的大同小异。在将来,我们可能会有其他策略,并且我们还可以实现自定义策略。
  • component-factory-strategy.ts:这个模块使用一个 component 工厂函数来创建和销毁组件引用。同时它还会在 input 改变时触发脏检查。这个运作过程在上文的例子中也有被提及。

下次我们将阐述 Angular Elements 通过 Custom Events 输出事件。

【译】Angular Elements 及其运作原理相关推荐

  1. 主机主浏览服务器宣告的运作原理机制

    主机主浏览服务器宣告的运作原理机制.刚启动的电脑会在第1,第2,第4,第8 第12 分钟向主浏览器进行主机宣告,此后每间隔12分钟进行主机宣告! 问题一   请问如果第1分钟就已经注册成功了,那是不是 ...

  2. 【nodejs原理源码赏析(2)】KOA中间件的基本运作原理

    [摘要] KOA中间件的基本运作原理 示例代码托管在:http://www.github.com/dashnowords/blogs 在中间件系统的实现上,KOA中间件通过async/await来在不 ...

  3. 只是浅谈MS SQL Server的Page Splits运作原理

    一直以来,很多文章或书籍都会提到数据库在对数据做增删修都会因为数据异动导致Page Splits的产生. 一旦过度的分割就会提高所谓的逻辑片段,而要降低逻辑片段就得对数据表或索引做Rebuild或Re ...

  4. AI行为树的基础运作原理

    欢迎捉虫! 之前我研究了一下基于switch case语句的FSM状态机的使用,后来遇到了很多问题. 比如当角色的行为很多时,代码结构相当混乱(你需要考虑每一种状态之间的联系). 所以,当角色的行为愈 ...

  5. 【WPS-OFFICE-Word】 WPS中样式的运作原理?样式自动更新、自动改变如何处理?样式的管理方法?

    一.WPS中样式的运作原理 文档中的每一个文字或者段落,它的格式取决于两点--样式以及自定义修改. 比如内容A基于样式1,样式1的字体格式是五号宋体.段落格式是1.5倍行距: 我们在样式1的基础上,从 ...

  6. Angular Elements,四步将Angular 组件转换为 web 组件

    Angular Elements,四步将Angular 组件转换为 web 组件 从Angular版本6开始,我们可以将Angular组件公开为Web组件,或者更确切地说:作为自定义元素,它是Web组 ...

  7. 物流供应链系统运作原理,物流供应链管理系统优化布局

    物流供应链系统管理优化对于物流行业企业而言有重要意义,以前的观点是生产促进产业增值,现在随着消费行为多样性.消费种类多样化,在商品通过物流配送中实现安全.稳定.可控的成功交付.管理过程中,也是属于实现 ...

  8. PHP session的运作原理解析

    今天主要介绍关于PHP session的运作原理解析,内容当中有详细资料跟代码实例相结合,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文主要介绍了 php语言,其他语言的操作可 ...

  9. UG模具设计:强复位的作用及运作原理

    UG模具设计:强复位的作用及运作原理 在后模滑块成型胶位底部有顶针.斜顶的情况下,合模时常因为滑块的复位先于顶针的复位,从而导致滑块与顶针或者斜顶相撞的恶性事件,为此,在模具上必须设置先复位机构以确保 ...

  10. 大脑是如何记忆的?大脑记忆工作的构成和运作原理

    我们每个人每天都会接收一些信息,同时我们还会忘掉一些东西,但我们很少去了解我们大脑为什么会这样处理?所以我们深入的探究一下大脑记忆是如何工作的?了解一下大脑记忆的构成和运作原理? 我们先看记忆是什么. ...

最新文章

  1. 独家 | 一文读懂机器学习中的贝叶斯统计学
  2. 这10个机器学习开源工具,你用过吗?
  3. gitlab合并分支后需要提交吗_阿里前端,如何基于 GitLab 进行「自动化」构建及发布...
  4. OpenCV 车道线提取
  5. python调用nacos账号密码,Python脚本,使用私钥(如果可用)或用户名密码
  6. spring AspectJ的Execution详解
  7. java中的字符,字符串,数字之间的转换(亲测)
  8. 机器学习-算法背后的理论与优化(part6)--正则化的优缺点
  9. C#LeetCode刷题之#747-至少是其他数字两倍的最大数( Largest Number At Least Twice of Others)
  10. python文件的路径_如何从目录中给出python文件的路径
  11. asp 文本转时间_ASP.NET Core界面开发,DevExpress v19.2增强富文本编辑器功能
  12. [2018.10.17 T1] 斜率
  13. Python学习笔记大总结(我把我所有python笔记做了总结和学习顺序,点击即可进入相关博客。希望可以便于你们阅读)
  14. linux如何共享网络打印机,Ubunt如何安装网络打印机的详细图文步骤
  15. axios安装与基本方法
  16. 【】每日360题,2019.11.02日08点财会类考试习题答案
  17. 计算机上显示找不到无线网络连接,电脑怎么找不到无线网络? 笔记本找不到无线网络如何解决?...
  18. 为什么程序员做外包会被瞧不起
  19. android 自定义locale,关于android:设置Locale.setDefault(locale)后,如何获取手机语言?...
  20. 一元享移动怎么样_中国移动终于认怂?29元享100G流量还不限速,网友:后悔携号转网了...

热门文章

  1. 设计模式(4)----抽象工厂模式
  2. jquery 获取节点各种方法
  3. mysql导出成execl
  4. 【转】如何理解NPV与IRR的区别??
  5. [傅里叶变换及其应用学习笔记] 七. 傅里叶正(反)变换复习
  6. MapX从数据库读取数据形成新图层【转载】
  7. python 给字符串加颜色
  8. STM32串口通信(使用C8T6)
  9. 手机通过笔记本电脑上网
  10. 读写执行Druapl7 Note-5: 利用FTP安装module或theme时出错(FIXED)