自定义元素

自定义元素是 Web Components standard 的一部分,它在 2011 被提议,且在最近稳定前出台了的两个不同规范。最终定稿感觉是一个简单原生的组件化框架替代品而不是框架作者的工具。它为定义组件提供了一个漂亮的高阶 API, 但它缺少不需用垫片的功能(需要兼容插件来支持)。

如果您还不熟悉自定义元素,请在继续之前查看本文。

自定义元素 API

自定义元素 API 是基于 ES6 类的。元素可以由原生的 HTML 元素或者自定义元素继承而来,并且它们可以用新的属性和方法扩展。他们也可以重写一系列的方法-定义在规范中-可以作为他们生命周期的钩子。

  1. class MyEelement extends HTMLElement {
  2. // these are standard hooks, called on certain events
  3. constructor() { ... }
  4. connectedCallback () { ... }
  5. disconnectedCallback () { ... }
  6. adoptedCallback () { ... }
  7. attributeChangedCallback (attrName, oldVal, newVal) { ... }
  8. // these are custom methods and properties
  9. get myProp () { ... }
  10. set myProp () { ... }
  11. myMethod () { ... }
  12. }
  13. // this registers the Custom Element
  14. customElements.define('my-element', MyElement)

在定义之后,这些元素可以在 HTML 或者 JavaScript 代码中以名称实例化。

<my-element></my-element>

基于类的 API 非常简洁,但是在我看来,它缺少灵活性。作为框架作者,我更加喜欢弃用的 v0 API,它是基于老旧的经典原型方法的。

  1. const MyElementProto = Object.create(HTMLElement.prototype)
  2. // native hooks
  3. MyElementProto.attachedCallback = ...
  4. MyElementProto.detachedCallback = ...
  5. // custom properties and methods
  6. MyElementProto.myMethod = ...
  7. document.registerElement('my-element', { prototype: MyElementProto })

它大概不够优雅,但是它可以把 ES6 和 ES6 规范之前的代码很好地整合在一起。从另一方面说,把 ES6 规范之前的代码和类代码混合在一起使用会是相当复杂的。

比如,我想要有能力控制组件继承哪个 HTML 接口。ES6 类使用静态的 extends 关键字来继承,并且它们要求开发者输入 MyClass extends ChosenHTMLInterface

这非常不适用于我目前的使用情况,因为 NX 基于中间件函数而不是类。在 NX 中,可以用 element 配置属性来设置接口,接口接受一个有效的 HTML元素名称比如 - button

  1. nx.component({element: 'button'})
  2. .register('my-button')

为了达到这一目标,我不得不使用基于原型的系统来模仿 ES6 类。长话短说,操作起来比人所能想的要让人蛋疼,并且它需要不需垫片的 ES6 Reflect.construct 和性能杀手 Object.setPrototypeOf 函数。

  1. function MyElement() {
  2. return Reflect.construct(HTMLELEMENT, [], MyElement)
  3. }
  4. const myProto = MyElement.prototype
  5. Object.setPrototypeOf(myProto, HTMLElement.prototype)
  6. Object.setPrototypeOf(MyElement, HTMLElement)
  7. myProto.connectedCallback = ...
  8. myProto.disconnectedCallback = ...
  9. customElements.define('my-element', MyElement)

这只是我发现在使用 ES6 类的很困难的情况之一。对于日常应用,我觉得他们是非常好的,但是当我想要很装逼地充分利用这门语言的功能的时候,我更倾向于使用原型继承。

生命周期

自定义元素拥有五个生命周期钩子,会在特定事件触发的时候同步调用。

  • constructor 会在元素的实例化的过程被调用
  • connectedCallback 会在元素被挂载到 DOM 的时候调用
  • disconnectedCallback 会在元素被从 DOM 中移除的时候调用
  • adoptedCallback 会在当使用 importNode 或者 cloneNode 把元素挂载到一个新的文档之中的时候调用
  • attributeChangedCallback 会在当被监听的元素属性发生变化的时候被调用

constructorconnectedCallback 非常适合创建组件状态和逻辑,而 attributeChangedCallback 可以被用来以 HTML 属性来显示组件的属性,反之亦然。disconnectedCallback 用来在组件销毁后清理内存。

整合在一起,这些涵盖了一系列很好的功能,但是我仍然忽略了 beforeDisconnectedchildrenChanged 回调。beforeDisconnected 钩子适用于简单的离开动画,然而除了封装或者大幅修改 DOM 是无法实现它的。

childrenChanged 钩子对于桥接状态和视图是非常重要的。看下以下示例:

  1. nx.component()
  2. .use((elem, state) => state.name = 'World')
  3. .register('my-element')
  1. <my-component>
  2. <p>Hello: ${name}<p>
  3. </my-component>

这是一个简单的模板片段,把 name 属性值从状态插入到视图中。当用户决定置换 p 元素为其它元素时,框架会接收到改变的通知。它不得不清理老的 p 元素内容,然后把插值插入到新内容中。childrenChanged 可能不会公开为开发者钩子,但是知道何时组件内容发生改变是一个框架必备的功能。

如我所述,自定义元素缺少一个 childrenChanged 回调,但是可以使用老旧的 MutationObserver API 来实现。MutationObservers 也为老浏览器提供了 connectedCallbackdisconnectedCallbackattributeChangedCallback 钩子的替代品。

  1. // create an observer instance
  2. const observer = new MutationObserver(onMutations)
  3. function onMutations (mutations) {
  4. for (let mutation of mutations) {
  5. // handle mutation.addedNodes, mutation.removedNodes, mutation.attributeName and mutation.oldValue here
  6. }
  7. }
  8. // listen for attribute and child mutations on `MyComponentInstance` and all of its ancestors
  9. observer.observe(MyComponentInstance, {
  10. attributes: true,
  11. childList: true,
  12. subtree: true
  13. })

除了自定义元素的简洁 API,这将会产生一些自定义元素必要性的问题。

下一章节,我将会阐述 MutationObservers 和 自定义元素的一些关键区别以及使用的场景。