你了解原生web组件吗

读完此文你将系统了解原生web组件组成以及它们之间的相互作用,并且对日常开发中遇到的问题会有更深层次的理解。

Web Components主要解决代码复用及组件封装问题,其包括三个主要部分:

  • Custom elements(自定义元素):允许定义custom elements及其行为,用户可在界面中按需使用它们。
  • Shadow DOM(影子DOM):用于将封装的“影子”DOM树附加到元素上(与主文档DOM分开渲染),并控制其关联的功能。通过这种方式,可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
  • HTML templates(HTML模板)<template><slot> 元素可以用来编写不在渲染页面中显示的标记模板,它们可作为自定义元素结构的基础被多次重用。

下面将详细叙述这三部分的使用及其相互作用。

Custom Elements(自定义元素)

顾名思义,可以自己定义的元素标签,类似于在vue,react中我们为了方便组件的复用,而把某个常用功能单独写成一个组件形式,后续可以直接调用该组件。不同的是,custom elements是一个封装好的html标签,他可以基于现有标签进行功能扩展,也可以单独定义标签直接调用,并且结合shadow DOM来使用可以做到减少组件中深层嵌套的标签,使DOM更加简洁。

自定义元素的定义api:Window.customElements.define()

语法:

customElements.define('custom-element-name', customElementsClass, [extends]);
  • custom-element-name:必填,所创建的自定义元素名称,需符合 DOMString 标准的字符串。注意!custom elements 的名称不能是单个单词,且其中必须要有短横线(-)。
  • customElementsClass:必填,constructor,通常是用于定义元素行为的
  • extends:可选参数,一个包含 extends 属性的配置对象。它指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。

customElements 是Window对象上的一个只读属性,接口返回一个 CustomElementRegistry 对象的引用,可用于注册新的 custom elements。

举个例子:下面注册了一个通用<my-footer>标签

  • 注册custom elements
// 为元素创建一个class
class myFooter extends HTMLElement {
    constructor() {
        // 必须先调用super方法,类的构造函数constructor总是先调用super()来建立正确的原型链继承关系。
        self = super();

        const imgAttr = this.getAttribute('img')
        const textAttr = this.getAttribute('text')

        const style = document.createElement('style');
        style.textContent = ` div {text-align:center;margin: 0 auto; } `;

        const imgNode = document.createElement('img')
        imgNode.setAttribute('src', imgAttr)

        const textNode = document.createElement('div')
        textNode.innerHTML = textAttr

        const rootNode = document.createElement('div')
        rootNode.appendChild(imgNode)
        rootNode.appendChild(textNode)
        rootNode.appendChild(style);

        this.appendChild(rootNode)
    }
}
// 定义自定义元素标签
customElements.define('my-footer', myFooter);
  • 调用自定义标签
<body>
    <my-footer img="./img/default.png" text="default footer"></my-footer>
</body>
  • 效果


    image.png

Custom Elements的分类

自定义元素分为两类:

  • Autonomous custom elements 自主定义元素
  • Customized built-in elements 自定义内置元素

Autonomous custom elements 自主定义元素

如同上文定义的一个<my-footer>标签一样,custom elements不继承其他内置的HTML元素,你可以直接把它们写成HTML标签的形式在页面上使用,如<my-footer></my-footer>,或者通过document.createElement("my-footer")创建元素。Autonomous custom elements 的类定义总是继承自HTMLElement.

Customized built-in elements 自定义内置元素

Customized built-in elements 继承自基本的HTML元素。在创建时,必须指定所需扩展的元素,使用时,需要先写出基本的元素标签,并通过 is 属性指定custom elements的名称。例如<p is="word-count">, 或者 document.createElement("p", { is: "word-count" })

// 自定义内置元素
class moneyFormat extends HTMLParagraphElement {
        constructor() {
            self = super();
            const money = this.textContent
            this.textContent = `${money / 10000}万`
        }
    }
customElements.define('money-format', moneyFormat, { extends: 'p'});

// 你可以正常使用<p>标签,也可以通过is属性来指定一个custom elements的名称
<p>100000</p>                    // 在页面上显示出来的就是100000
<p is="money-format">100000</p>  // 在页面上显示出来的就是10万

// 通过动态创建自定义内置元素
cosnt moneyFormat = document.createElement('p', { is: 'money-format' })

这里的真正不同点在于元素定义类继承的是HTMLParagraphElement接口(不同的元素继承的接口都不一样,<p>继承的是HTMLParagraphElement<ul>继承的的是HTMLUListElement),而不是HTMLElement。所以它拥有<p>元素所有的特性,以及在此基础上我们定义的功能,这是与独立元素(standalone element)不同之处。这也是为什么我们称它为 customized built-in元素,而不是一个autonomous元素

生命周期回调函数

生命周期 调用时机
constructor 创建元素的实例时调用,用于初始化状态、设置事件侦听器或创建影子dom
connectedCallback 当 custom elements首次被插入文档DOM时
disconnectedCallback 当 custom elements从文档DOM中删除时
adoptedCallback 当 custom elements被移动到新的文档时(document.adoptNode方法修改元素ownerDocument时触发)
attributeChangedCallback(name, oldValue, newValue) 当 custom element增加、删除、修改自身属性时,attributeChangedCallback()回调函数会执行
static get observedAttributes() {return ['attribute']; } 如果需要在元素属性变化后,触发 attributeChangedCallback()回调函数,则需要监听这个属性

!注意:想要attributeChangedCallback生效,必须设置observedAttributes来返回该标签需要监听哪些属性的改变,两者需要结合使用。

下面示例展示了:定义自定义元素加入文档中、修改自定义元素的属性以及从文档中移除自定义属性的生命周期触发时机

<my-footer id="my-footer" img="./img/default.png" text="default footer"></my-footer>
<button id="change-attr-btn">change attribute</button>
<button id="remove-attr-btn">remove attribute</button>

定义自定义元素my-footer以及添加绑定事件触发自定义元素的生命周期函数

// 自定义元素生命周期展示
class myFooter extends HTMLElement {
     constructor() {
         self = super();
         const style = document.createElement('style');
         style.textContent = `
             div {text-align:center;margin: 0 auto; }
         `;
         document.body.appendChild(style);
         this.updateText()
     }
     // 监听自定义元素的属性:text,发生改变时会触发 attributeChangedCallback 函数
     static get observedAttributes() {
         return ['text'];
     }

     connectedCallback() { console.log('【connectedCallback】Custom element added to page.'); }

     disconnectedCallback() { console.log('【disconnectedCallback】Custom element removed from page.'); }

     adoptedCallback() { console.log('【adoptedCallback】Custom element moved to new page.'); }

     attributeChangedCallback(name, oldValue, newValue) {
         console.log('【attributeChangedCallback】', name, oldValue, newValue)
         console.log('【attributeChangedCallback】Custom element attributes changed.');
         this.updateText()
     }

     // 更新自定义元素内容
     updateText() {
         const img = this.getAttribute('img')
         const text = this.getAttribute('text')
         this.innerHTML = `<div><img src="${img}"/></div><div>${text}</div>`
     }
 }
 // 定义自定义元素:my-footer
 customElements.define('my-footer', myFooter);

 const doc = document
 const myFooterEle = doc.getElementById('my-footer')
 const changeAttrBtn = doc.getElementById('change-attr-btn')
 const removeAttrBtn = doc.getElementById('remove-attr-btn')

 changeAttrBtn.onclick = function () {
     myFooterEle.setAttribute('text', 'change footer')
 }
 removeAttrBtn.onclick = function () {
     doc.body.removeChild(myFooterEle)
 }

刷新页面:控制台输出

【attributeChangedCallback】 text null default footer
【attributeChangedCallback】Custom element attributes changed.
【connectedCallback】Custom element added to page.

点击changeAttrBtn按钮:

【attributeChangedCallback】 text default footer change footer
【attributeChangedCallback】Custom element attributes changed.

点击removeAttrBtn按钮:

【disconnectedCallback】Custom element removed from page.

css伪类

:defined

:defined表示任何已定义的元素,包括任何浏览器内置的标准元素以及已成功定义的自定义元素 (例如通过 CustomElementRegistry.define()方法定义的元素)。

/* 选择所有已定义的元素 */
:defined {
  font-style: italic;
}

/* 选择指定自定义元素的任何实例 */
my-footer:defined {
  display: block;
}

/* 在你有一个复杂的自定义元素需要一段时间才能加载到页面中时非常有用 —— 你可能想要隐藏元素的实例直到定义完成为止,这样你就不会在页面上出现一些难看的元素 */
my-footer:not(:defined) {
  display: none;
}

改进

  1. 自定义元素都要动态通过JS生成DOM,很繁琐,并且不直观,针对这个问题,可以使用HTML templates中的<template> 和 <slot> 元素
  2. 每个自定义标签下面都嵌套一堆DOM元素,如下,十分冗余,并没有真正减负;其次,没有达到真正HTML和CSS封装的目的,容易受主文档的影响,针对这个问题,可以使用Shadow DOM(影子DOM)
 <!-- 浏览器devtools中展示的elements情况 -->
<my-footer id="my-footer" img="./img/default.png" text="default footer">
    <div><img src="./img/default.png"></div>
    <div>default footer</div>
</my-footer>

兼容性

image.png
image.png
image.png
image.png

可以看到主流浏览器对customElements接口Custom Elements标签都兼容,IE不兼容,但可以通过polyfills去兼容(详情文末),并且Customized built-in elements自定义内置元素兼容性不佳,部分浏览器不兼容。

Shadow DOM(影子DOM)

Shadow DOM主要解决了 DOM 树的封装问题。Shadow DOM允许在文档(document)渲染时插入一棵DOM元素子树,但是这棵子树不在主DOM树中,它与文档的主 DOM 树分开渲染。

什么是Shadow DOM

Shadow DOM重要的特性就是封装性,它可以将DOM结构、css样式和行为隐藏起来,并与页面上的其他代码相隔开来,保证不同的部分不会混在一起,使代码更加干净整洁。Shadow DOM允许将隐藏的 DOM 树附加到常规的 DOM 树中(被附加的这个常规的树的节点叫shadow host),它以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样。你可以像操作常规DOM一样操作Shadow DOM,不同的是Shadow DOM内部的元素始终不会影响到它外部的元素,这为封装提供了便利。

Shadow DOM在哪里

Shadow DOM离我们其实并不遥远,平时我们在浏览器devtool工具里面看不到是因为我们没有开启显示shadow dom,打开方式:浏览器打开开发者调试工具-右上角“设置”图标-Preference-Elements-勾选“show user agent shadow DOM”


image.png

文档结构:

<input type="text" placeholder="please add your plan" />
<video controls="" style="width: 300px;height:200px;">
    <source src="./video.mov">
</video>

devtools中的elements表现:


image.png

可以看到,<input><video>标签下挂载着一个shadow-root,但在常规调试工具中,是看不到里面的DOM结构的,看到的是代码中所写的样子。这里shadow-root下的DOM就是Shadow DOM。Shadow DOM里面元素以及样式不会影响外部,这也是封装的意义所在。(也可以看到video里面的Shadow DOM都带了pseudo属性,这样我们就可以通过伪类::-webkit-media-controls去改变video的样式)

Shadow DOM与常规DOM的关系:

Document Tree
      |
  Shadow host
------------------ 边界 
      |
  Shadow root
      |
  Shadow Tree

image.png
  • Shadow host:一个常规 DOM节点,Shadow DOM 会被附加到这个节点上。
  • Shadow root:Shadow tree 的根节点。
  • Shadow boundary:Shadow DOM结束的地方,也是常规 DOM 开始的地方。
  • Shadow tree:Shadow DOM 内部的DOM树。

创建Shadow DOM

Element.attachShadow() 方法给指定的元素挂载一个Shadow DOM,并且返回对 ShadowRoot 的引用。
不是每一种类型的元素都可以附加到shadow root(影子根)下面。出于安全考虑,一些元素不能使用 shadow DOM(例如<a>),以及许多其他的元素。

基本语法:var shadowroot = element.attachShadow(shadowRootInit<Object>);

参数:
shadowRootInit(Object):

  • mode:指定 Shadow DOM 树封装模式的字符串
    • open: 可从外部访问shadow root元素的根节点,如Element.shadowRoot
    • closed: 不可以从外部获取 Shadow DOM,Element.shadowRoot返回null
  • delegatesFocus: 焦点委托
    • 一个布尔值, 当设置为 true 时, 指定减轻自定义元素的聚焦性能问题行为.
    • 当shadow DOM中不可聚焦的部分被点击时, 让第一个可聚焦的部分成为焦点, 并且shadow host(影子主机)将提供所有可用的 :focus 样式.

返回值:返回一个 ShadowRoot 对象或者 null。

提示:也可以选择host.createShadowRoot()的方法创建Shadow Root挂载Shadow Tree

应用

结合custom elements,创建一个挂载Shadow DOM的自定义元素

class myFooter extends HTMLElement {
    constructor() {
        // 必须先调用super方法,类的构造函数constructor总是先调用super()来建立正确的原型链继承关系。
        self = super();
        // 自定义元素挂载一个Shadow DOM
        this._shadowRoot = this.attachShadow({ mode: 'open' })

        const style = document.createElement('style');
        style.textContent = `img,div {text-align:center;margin: 0 auto;display: block};`;

        const img = this.getAttribute('img')
        const text = this.getAttribute('text')
        const imgNode = document.createElement('img')
        imgNode.setAttribute('src', img)
        const textNode = document.createElement('div')
        textNode.textContent = text

        this._shadowRoot.appendChild(imgNode);
        this._shadowRoot.appendChild(textNode);
        this._shadowRoot.appendChild(style);
    }
}
customElements.define('my-footer', myFooter);

调用:

<my-footer id="my-footer" img="./img/default.png" text="default footer"></my-footer>

DOM结构:


image.png

可以看到<my-footer>下多了一个#shadow-root,展开#shadow-root才看到具体的shadow tree里面的结构,并且shadow dom中定义的样式不会影响主DOM中样式,当把调试工具中的勾选去掉“show user agent shadow DOM”后,在文档上就看不到<my-footer>内具体的DOM树了,这就跟<video>标签一样的表现形式了。

因为这里设置了mode:'open',所以可以在外面获取shadow root下的根节点。

console.log(document.getElementById('my-footer').shadowRoot)

// 控制台打印:
#shadow-root(open)
<img src="./img/default.png">
<div>default footer</div>

mode: 'closed'时,则外层无法获取Shadow DOM,<video>标签就是设置了closed属性

console.log(document.getElementById('my-footer').shadowRoot) // null

从组件的可扩展性与灵活些来说,建议设置open属性,方便使用者进行修改扩展。

  • mode="open": Ele.shadowRoot = #shadow-root(open)....
  • mode="closed": Ele.shadowRoot = null

Event.composed && Event.composedPath()

属性/接口 返回值 作用
Event.composed Boolean 若返回值为true,表明当事件到达 shadow DOM 的根节点(也就是 shadow DOM 中事件开始传播的第一个节点)时,事件可以从 shadow DOM 传递到一般 DOM。当然,事件要具有可传播性,即该事件的 bubbles 属性必须为 true。如果属性值为 false,那么事件将不会跨越 shadow DOM 的边界传播。
Event.composedPath() 一个 EventTarget对象数组,表示将在其上调用事件侦听器的对象。 返回事件路径,如果影子根节点被创建并且ShadowRoot.mode是关闭的,那么该路径不包括影子树中的节点。

对文档设置事件监听

document.querySelector('html').addEventListener('click', function (e) {
    console.log(e.bubbles)        
    console.log(e.composed);      
    console.log(e.composedPath());
}, false);

点击<my-footer>中的<img>标签,得到如下结论:

  1. 无论ShadowRoot.mode是open或closed,e.composed都返回true,因为click事件始终能跨越阴影边界传播。
  2. 不同在于e.composedPath():
    • ShadowRoot.mode=open时,e.composedPath()返回[img, document-fragment, my-footer#my-footer, body, html, document, Window],事件能到达Shadow DOM里面的元素
    • ShadowRoot.mode=closed时,e.composedPath()返回[my-footer#my-footer, body, html, document, Window],事件不能到达Shadow DOM中,监听器只会捕获到<my-footer> 元素本身

当一个事件从 Shadow DOM 冒泡时,它的target会被调整以保持 Shadow DOM 提供的封装。也就是说,事件被重新定位,使其看起来像是来自组件而不是影子 DOM 中的内部元素。有些事件甚至不会从 shadow DOM 传播出去。

以下为能跨越阴影边界的事件:

  • Focus Events: blur, focus, focusin, focusout
  • Mouse Events: click, dblclick, mousedown, mouseenter, mousemove, etc.
  • Wheel Events: wheel
  • Input Events: beforeinput, input
  • Keyboard Events: keydown, keyup
  • Composition Events: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop, etc.

有些情况下事件绑定不进行重定向而直接被干掉,以下事件会被阻塞到根节点且不会被原有 DOM 结构监听,被阻塞的事件:

  • abort
  • error
  • select
  • change
  • load
  • reset
  • resize
  • scroll

可以参考以下实例解释:

<body>
    <input id="normal-text" type="text" value="I'm normal text">
    <input-item text="姓名:"></input-item>
</body>
class inputItem extends HTMLElement {
    constructor() {
        self = super();
        this._shadowRoot = this.attachShadow({ mode: 'open' })

        const text = this.getAttribute('text')
        const textNode = document.createElement('div')
        textNode.textContent = text
        const inputNode = document.createElement('input')
        inputNode.value = 'I am shadow dom text'

        this._shadowRoot.appendChild(textNode);
        this._shadowRoot.appendChild(inputNode);
    }
}
customElements.define('input-item', inputItem);

document.addEventListener('change', function (e) {
    console.log('[change event target]', e.target);
    console.log('[change event bubbles]', e.bubbles)         
    console.log('[change event composed]', e.composed);       
    console.log('[change event composedPath]', e.composedPath());

});

document.addEventListener('click', function (e) {
    console.log('[click event target]', e.target)
    console.log('[click event bubbles]', e.bubbles)         
    console.log('[click event composed]', e.composed);       
    console.log('[click event composedPath]', e.composedPath()); 
});

点击正常input标签并修改value的值,事件冒泡到document,click和change事件能够成功被监听。可以看到change事件中composed返回false,devtool中开启“show Shadow-root”的话可以看到其实input标签下也是挂载了一个Shadow DOM,所以change事件不能从Shadow DOM中传递回一般的DOM,input是一个封装好的元素,保证了其封装性,change事件的target就是input元素本身。

[click event target] <input id="normal-text" type="text" value="I'm normal text">…</input>
[click event bubbles] true
[click event composed] true
[click event composedPath] (5) [input#normal-text, body, html, document, Window]

[change event target] <input id="normal-text" type="text" value="I'm normal text">…</input>
[change event bubbles] true
[change event composed] false
[change event composedPath] (5) [input#normal-text, body, html, document, Window]

点击Shadow DOM中的input并修改值,change事件冒泡到Shadow Root就会停止向上,所以绑定在document上的事件不会被触发,只有click事件能冒泡到document被触发,并且控制台上打印target的是宿主对象host,即<input-item>元素。这是因为影子节点上的事件必须重定向,否则这将破坏封装性。如果事件继续指向 #shadow-root 里面的元素,那么任何人都可以在 Shadow DOM 里破坏其内部结构,这就违背了其封装性的初衷。

[click event target] <input-item text="姓名:">…</input-item>
[click event bubbles] true
[click event composed] true
[click event composedPath] (7) [input, document-fragment, input-item, body, html, document, Window]

使用自定义事件

在影子树中的内部节点上触发的自定义DOM事件不会冒泡出影子边界,除非该事件是使用 composed: true 标志创建的

使用 new Event()

new Event(eventName<String>, eventInit<Object<{bubbles: false, cancelable: false, composed: fakse}>>)

eventInit(Object),可选

  • "bubbles",可选,Boolean类型,默认值为 false,表示该事件是否冒泡。
  • "cancelable",可选,Boolean类型,默认值为 false, 表示该事件能否被取消。
  • "composed",可选,Boolean类型,默认值为 false,指示事件是否会在影子DOM根节点之外触发侦听器。
class myFooter extends HTMLElement {
    constructor() {
        self = super();
        this._shadowRoot = this.attachShadow({ mode: 'open' })

        // 省略中间代码...
    this._shadowRoot.addEventListener('click', this._changeText.bind(this))
    }

    _changeText() {
        // 向一个指定的事件目标派发一个事件
        this._shadowRoot.dispatchEvent(new Event('text-change', { bubbles: true, composed: true }));
    }
}
customElements.define('my-footer', myFooter);

document.getElementById('my-footer').addEventListener('text-change', function(e) {
    // 当事件定义参数不是`composed: true`时不会触发该事件
    // 当事件定义参数`composed: true`时,输出:【text-change event】 Event {isTrusted: false, type: "text-change", target: my-footer#my-footer, currentTarget: my-footer#my-footer, eventPhase: 2, …}
    console.log('【text-change event】', e)
});

使用 new CustomEvent()

相比于new Event()new CustomEvent()可以自定义派发的数据。

new CustomEvent(eventName<String>, eventInit<Object<{bubbles: false, cancelable: false, detail: any}>>)

eventInit(Object)可选

  • "bubbles",可选,Boolean类型,默认值为 false,表示该事件是否冒泡。
  • "cancelable",可选,Boolean类型,默认值为 false, 表示该事件能否被取消。
  • "detail",可选,any类型,默认值为 null,当事件初始化时传递的数据
class myFooter extends HTMLElement {
    constructor() {
        self = super();
        this._shadowRoot = this.attachShadow({ mode: 'open' })

        // 省略中间代码...
        this._shadowRoot.addEventListener('click', this._changeText.bind(this))
    }

    _changeText() {
        // 这里不在this._shadowRoot上派发,而是在<my-footer>上派发,因为CustomEvent事件默认composed是false,所以监听事件不会触发
        this.dispatchEvent(new CustomEvent('text-change', { detail: { text: 'change text' } }));
    }
}
customElements.define('my-footer', myFooter);

document.getElementById('my-footer').addEventListener('text-change', function(e) {
    console.log('【text-change event】', e)
});

输出:

> CustomEvent {isTrusted: false, detail: null, type: "text-change", target: my-footer#my-footer, currentTarget: my-footer#my-footer, …}
    bubbles: false
    cancelBubble: false
    cancelable: false
    composed: false
    currentTarget: null
    defaultPrevented: false
    detail: {text: "change text"}   => 派发过来的数据
    eventPhase: 0
    isTrusted: false
    path: (5) [my-footer#my-footer, body, html, document, Window]
    returnValue: true
    srcElement: my-footer#my-footer
    target: my-footer#my-footer
    timeStamp: 1882.7999997138977
    type: "text-change"
    __proto__: CustomEvent

css伪类

只能在Shadow DOM内使用,在之外使用时,没有任何效果。

  • :host:选择包含其自定义元素内部的shadow DOM的根元素。
  • :host():选择包含使用这段 CSS 的 Shadow DOM 的影子宿主,只能选择host元素
  • :host-context():选择shadow DOM 中shadow host,这个伪类内可以写关于该shadow host的CSS规则。 在DOM 层级中,括号中的选择器参数必须和shadow host 的祖先相匹配。可以用于主体化定制,典型的使用方法是后代选择器表达式,例如只选择在<h1>内的自定义元素的实例。
/* 选择一个 shadow root host */
:host {
  font-size: 16px;
}

/* 选择阴影根元素,仅当它与选择器参数匹配 */
:host(#my-footer) {
    font-weight: bold;
}

/* 选择阴影根元素,仅当它与选择器参数匹配 */
:host(my-footer) {
    color: green;
}

/* host元素下shadow dom */
:host(my-footer) #my-footer-text{
    color: red;
}

/* 选择了一个 shadow root host, 当且仅当这个 shadow root host 是括号中选择器参数(h1)的后代 */
:host-context(h1) {
    color: blue;
}

如果你想使用者可以从外部修改自定义元素的样式,那么开发者可以在自定义元素内部预埋可供使用者覆盖的css的属性,这样可做到自定义样式。

 <!-- 在Shadow DOM内通过var预埋可覆盖样式属性 -->
    :host {
        font-size: 20px;
        background-color: var(--my-footer-bg-color, #fff);
    }

<!-- 在html文档中 -->
        /* 选择指定自定义元素的任何实例 */
        my-footer:defined {
            --my-footer-bg-color: #eee;
        }

兼容性

image.png
image.png

HTML templates(HTML模板)

<template><slot> 元素可定义可重用的HTML结构,减少使用js动态创建,也更加可视化与灵活性。

template

template模板中可以定义DOM结构,但是浏览器不会渲染,可以在custom elements中利用。

定义template内容

<body> 
    <template id="my-footer-template">
        
        <div><img  id="my-footer-img" src="./img/default.png"></div>
        <div id="my-footer-text">default footer</div>
    </template>

    <my-footer id="my-footer" img="./img/default.png" text="default footer"></my-footer>
</body>

创建custom elements:

class myFooter extends HTMLElement {
    constructor() {
        // 必须先调用super方法,类的构造函数constructor总是先调用super()来建立正确的原型链继承关系。
        self = super();
        this._shadowRoot = this.attachShadow({ mode: 'open' })
        const template = document.getElementById('my-footer-template')
        // 拷贝template的内容添加到shadow dom上
        this._shadowRoot.appendChild(template.content.cloneNode(true))

        this.$img = this._shadowRoot.querySelector('#my-footer-img')
        this.$text = this._shadowRoot.querySelector('#my-footer-text')
        const img = this.getAttribute('img')
        const text = this.getAttribute('text')
        this.$img.setAttribute('src', img)
        this.$text.textContent = text
    }
}
customElements.define('my-footer', myFooter);

在定义好的template中可以清晰地看到将要添加到Shadow DOM中的文档结构,就像真正写在页面中一样。将template中的内容拷贝到Shadow DOM上,用到了Node.cloneNode()接口。

Node.cloneNode()

Node.cloneNode()方法返回调用该方法的节点的一个副本.

语法:const dupNode = node.cloneNode(deep);

  • node: 将要被克隆的节点
  • dupNode: 克隆生成的副本节点
  • deep:可选,是否采用深度克隆,如果为true,则该节点的所有后代节点也都会被克隆;如果为false,则只克隆该节点本身
var dupNode = node.cloneNode(deep);
// 拷贝节点并不属于当前文档树的一部分,也就是说,它没有父节点,需要Node.appendChild()或其他类似的方法将拷贝的节点添加到文档中
Node.appendChild(dupNode)

slot插槽

我们在Vue中创建组件时经常会使用到<slot>标签,该标签主要能增加插入元素的灵活性。这里也是如此,slot由其name属性标识,并允许在模板中定义占位符,当在文档中通过定义属性slot=slotName使用时,可以在占位符中填充任何HTML标记片段。

定义template及slot

<body> 
    <template id="my-footer-template">
        
        <div><img  id="my-footer-img" src="./img/default.png"></div>
        <div id="my-footer-text">default footer</div>
        <div><slot name="footer-after"></slot></div>
    </template>

    <my-footer id="my-footer" img="./img/default.png" text="default footer">
        <span slot="footer-after">This is footer-after text.</span>
    </my-footer>
</body>

页面上渲染出来的结构为:


image.png

可以看出通过插槽的方式,可以定义占位符,后续复用自定义元素的时候,既可以有复用性的一面,又有自定义内容的一面,两者结合,完美。

兼容性

image.png
image.png

应用

看完上文,应该多多少少也了解了web组件的特性及作用,这里总结下web组件可以用来做什么:

  • 构建不依赖任何框架(如Vue,React,Angular等)的可重用组件库
  • 创建具有封装性的自定义组件标签,隔离HTML与CSS
  • 可用于挂载一个独立功能的Shadow DOM,减少dom深层嵌套及与主文档的相互影响

web组件构建框架

目前有一些框架专门用来构建web组件,可以更加方便我们去构建组件库。

  • Polymer library: (published by Google in 2013) The Polymer library provides a set of features for creating custom elements.
  • Lit: (The Polymer library is in maintenance mode. For new development, we recommend Lit) Lit is a simple library for building fast, lightweight web components.
    At Lit's core is a boilerplate-killing component base class that provides reactive state, scoped styles, and a declarative template system that's tiny, fast and expressive.
  • LitElement: LitElement is now part of the Litlibrary
  • Vue3.2+: defineCustomElement api.
  • ReactJS: Web Components
  • AngularJS: createCustomElement api.

兼容

可在下面文档中去引入polyfills文件去兼容web组件

最后

看完上文,大家应该对video标签为什么在不同浏览器展示的样式不同,以及为什么不同浏览器兼容性不一致这些问题都有了很好的理解。未来,可能会有更多浏览器内置的自定义元素出现,需要各自浏览器去兼容,这样我们就能用到更多通用的标签。

最近vue也发布了3.2.0版本,该版本全局api中就新增了defineCustomElement接口,并且google也在不断维护其web组件创建框架,各种浏览器也在不断兼容web组件特性,未来我觉得web组件不会被埋没,而是会让更多人了解。

但由于我们现在用的大多框架都在virtual DOM层面上操作DOM,而web组件是脱离框架的,这使得需要纯JS去操作DOM,这可能需要回到最初的虚拟DOM前的时代去创建,可能会带来开发上的繁琐,这也是一个值得思考与改进的问题。

更多阅读

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容