Web Components 上手指南

现在的前端开发基本离不开 React、Vue 这两个框架的支撑,而这两个框架下面又衍生出了许多定义组件库:

这些组件库的出现,让我们可以直接使用已经封装好的组件,而且在开源社区的帮助下,出现了很多的模板项目( vue-element-adminAnt Design Pro ),能让我们快速的开始一个项目。

虽然 React、Vue 为我们的组件开发提供了便利,但是这两者在组件的开发思路上,一个是自创的 JSX 语法,一个是特有的单文件模板的语法,两者的目标都是想提供一种组件的封装方法。毕竟都有其原创的东西在里面,和我们刚开始接触的 Web 基础的 HTML、CSS、JS 的方式还是有些出入的。今天介绍的就是,通过 HTML、CSS、JS 的方式来实现自定义的组件,也是目前浏览器原生提供的方案:Web Components。

什么是 Web Components?

Web Components 本身不是一个单独的规范,而是由一组DOM API 和 HTML 规范所组成,用于创建可复用的自定义名字的 HTML 标签,并且可以直接在你的 Web 应用中使用。

代码的复用一直都是我们追求的目标,在 JS 中可复用的代码我们可以封装成一个函数,但是对于复杂的HTML(包括相关的样式及交互逻辑),我们一直都没有比较好的办法来进行复用。要么借助后端的模板引擎,要么借助已有框架对 DOM API 的二次封装,而 Web Components 的出现就是为了补足浏览器在这方面的能力。

流行趋势

  • Twitter:嵌入式推文
  • YouTube:该站点是使用 Web Components 构建的
  • Electronic Arts:该站点也是使用 Web Components 构建的
  • Adobe Spectrum:该站点是一个基于 Web Components 的 UI 框架产品

另外还有维基百科、可口可乐、麦当劳、IBM 和 通用电气 等也使用基于Web 组件的技术和框架。

技术优点

  • 原生支持
    原生支持意味着可以不需要任何框架即可完成开发,同时也意味着这将有更好的用户体验,更低的网络请求,以及更稳定的迭代前景。并且我们一直都有在使用这项技术,比如 input, video,select 等等,其实他们都是标准的原生组件,只是如今我们自己也可以使用这项技术去创造这些组件。
  • 无排他性
    这是原生支持的一个延伸表现,作为浏览器原生支持也就意味着它可以在任何环境中使用,例如在 React,Angular,Vue 中使用他们。同时也意味着对 Web Components 的支持无需大刀阔斧的颠覆现有逻辑体系,你可以从局部开始进行改造。
  • 无依赖性
    这一点同样是原生支持的一个延伸表现,通过提供连接特定 Web 组件的选项,而无需将框架的依赖项导入到项目中,您就可以拥有优于流行框架的优势。
    让我们想象一个场景:如果您喜欢用 React 创建的小部件,并且想将其包含在您的项目中,则必须首先包含整个 React 库,然后才能导入您喜欢的小部件。相反,如果您选择使用 Web 组件创建的小部件,您可以立即将其插入到项目中——这一切都归功于这项技术的原生特性。

如何使用 Web Components?

Web Components 中包含的几个规范,都已在 W3C 和 HTML 标准中进行了规范化,主要由三部分组成:

  • Custom elements(自定义元素):一组 JavaScript API,用来创建自定义的 HTML标签,并允许标签创建或销毁时进行一些操作;
  • Shadow DOM(影子DOM):一组 JavaScript API,用于将创建的 DOM Tree 插入到现有的元素中,且 DOM Tree 不能被外部修改,不用担心元素被其他地方影响;
  • HTML templates(HTML模板):通过 <template><slot> 直接在 HTML 文件中编写模板,然后通过 DOM API 获取。

shadow DOM

Web 组件的一个重要方面是封装——能够将标记结构、样式和行为隐藏起来并与页面上的其他代码分开,这样不同的部分就不会发生冲突,并且代码可以保持整洁。Shadow DOM API 是其中的关键部分,它提供了一种将隐藏的分离 DOM 附加到元素的方法。

性能优势-组件隔离(Shadow Dom)

Shadow DOM 为自定义的组件提供了包括 CSS、事件的有效隔离,不再担心不同的组件之间的样式、事件污染了。这相当于为自定义组件提供了一个天然有效的保护伞。
Shadow DOM 实际上是一个独立的子 DOM Tree,通过有限的接口和外部发生作用。我们都知道页面中的 DOM 节点数越多,运行时性能将会越差,这是因为 DOM 节点的相互作用会时常在触发重绘(Repaint)和重排(reflow)时会关联计算大量 Frame 关系。


  • Shadow host:Shadow DOM 附加到的常规 DOM 节点。
  • Shadow tree:Shadow DOM 内部的 DOM 树。
  • Shadow boundary:Shadow DOM 结束的地方,普通 DOM 开始的地方。
  • Shadow root:Shadow tree的根节点。

你可以以与非影子节点完全相同的方式影响影子 DOM 中的节点——例如,附加子项或设置属性、使用 element.style.foo 为单个节点设置样式,或在 <style> 内为整个影子 DOM 树添加样式元素。不同之处在于 shadow DOM 内部的任何代码都不会影响其外部的任何内容,从而可以方便地进行封装。

请注意,shadow DOM 无论如何都不是什么新鲜事物——浏览器已经使用它来封装元素的内部结构很长时间了。以一个 <video> 元素为例,它暴露了默认的浏览器控件。你在 DOM 中看到的只是 <video> 元素,但它的 shadow DOM 中包含一系列按钮和其他控件。shadow DOM 规范已经允许你实际操作你自己的自定义元素的 shadow DOM。

代码演示:

<!DOCTYPE html>
<html>
    <body>
        <div class="shadow-host"></div>
        <!-- js部分-->
        <script>
            const shadowHost = document.querySelector('.shadow-host');
            const shadowRoot = shadowHost.attachShadow({mode: 'closed'})
        </script>
    </body>
</html>

在浏览器中打开此文档,可以看到div.shadow-host元素下多出了一个#shadow-root(closed)
这里的closed来源于创建 Shadow DOM 时的 {mode: 'closed'} 选项, mode 可选值如下:

closed: shadowRoot元素不可以由外部js访问到, Element.shadowRoot为 null
open: shadowRoot元素可以由外部js访问到,即通过 Element.shadowRoot属性可以访问到此节点

此时 shadow-root 仍然是一个空节点,需要往其中添加一些节点

<!DOCTYPE html>
<html>
    <body>
        <div class="shadow-host"></div>
        <!-- js部分-->
        <script>
            const shadowHost = document.querySelector('.shadow-host');
            const shadowRoot = shadowHost.attachShadow({mode: 'closed'})
            const shadowDiv = document.createElement('div');
            shadowDiv.classList.add('shadow-div');
            shadowDiv.innerText = 'i am shadow div'
            shadowRoot.appendChild(shadowDiv)
        </script>
    </body>
</html>

注意:当给一个普通元素附加Shadow DOM后,其原本所包含的所有普通DOM元素都会失效,一个普通元素下不会同时存在Shadow DOM 和 普通 DOM。虽然仍然可以使用js获取到这部分普通元素,但是浏览器不会将其渲染到页面中。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div class="shadow-host"><span class="origin-span">我是普通DOM元素</span></div>
<button style="margin-top: 50px;">添加Shadow DOM</button>
<!-- js部分-->
<script>
    const btn = document.querySelector('button');
    btn.onclick = attachShadow;
    function attachShadow() {
        const shadowHost = document.querySelector('.shadow-host');
        const shadowRoot = shadowHost.attachShadow({ mode: 'closed' });
        const shadowDiv = document.createElement('div');
        shadowDiv.classList.add('shadow-div');
        shadowDiv.innerText = '我是 shadow div';
        shadowRoot.appendChild(shadowDiv);
        console.log('已附加Shadow DOM')
        // 获取原本存在的普通DOM元素
        console.log('shadowRoot下的普通DOM > origin-span: ', document.querySelector('.origin-span'))
    }
</script>
</body>
</html>

Shadow DOM的隔离特性

上面说到,Shadow DOM 内部的元素始终不会影响到它外部的元素(除了 :focus-within)。接下来就来探究Shadow DOM中的元素与外部普通DOM元素究竟有何关联。

focus-within

先说一下focus-within:是一个CSS 伪类 ,表示一个元素获得焦点,或,该元素的后代元素获得焦点。也就是说即便是Shadow DOM,当其获得焦点时,也会使得外部的父级DOM元素上的focus-within生效。下面用代码验证一下:

<!doctype html>
<html lang="en">
<html>
<head>
    <meta http-equiv="X-UA-Compatiple" content="IE=edge">
    <title>test</title>
    <style>
        .shadow-host-parent:focus-within {
            padding: 10px;
            background-color: cyan;
        }
        .shadow-host {
            padding: 5px;
            border: 1px dashed black;
        }
        .shadow-host:focus-within {
            background-color: cornflowerblue;
        }
    </style>
</head>
<body>
<div class="shadow-host-parent">
    <div class="shadow-host"></div>
</div>
<!-- js部分-->
<script>
    attachShadow();
    function attachShadow() {
        const shadowHost = document.querySelector('.shadow-host');
        const shadowRoot = shadowHost.attachShadow({ mode: 'closed' });
        const shadowInput = document.createElement('input');
        shadowInput.value = 'input'
        shadowRoot.appendChild(shadowInput);
    }
</script>
</body>
</html>
样式隔离

外部CSS(包含style和外链资源)中被应用在Shadow Host元素的样式中(包括继承来的样式)能被继承的属性都会被Shadow DOM中的元素继承,通过选择器无法直接对Shadow DOM的元素应用样式。
Shadow DOM中style或者引入的CSS不会对外部元素造成影响

由于Shadow DOM天然具有良好的样式隔离特性,所以Shadow DOM也是作为了微前端框架qiankun中实现样式隔离的一种方案。

例1:外部无法通过类选择器改变 Shadow DOM 内元素样式
可以看到,同样拥有pure-text这个 className,但是却只对外部DOM生效。

例2:Shadow Host 元素上的样式能够被继承

上面例子稍微改一下,我给shadow-host元素添加一个 color样式


例3:Shadow DOM内部应用样式无法改变影响外部元素

在Shadom内引入css文件:

shadow.css

.btn-primary {
    background-color: #1890ff;
    padding: 5px 10px;
    color: #fff;
}

html:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<div class="shadow-host-parent">
    <div class="shadow-host"></div>
</div>
<div class="shadow-host"></div>
<div style="margin-top:20px"><span class="btn-primary">click Me!</span></div>
<script>
    attachShadow();
    function attachShadow() {
        const shadowHost = document.querySelector('.shadow-host');
        const shadowRoot = shadowHost.attachShadow({ mode: 'closed' });
        // 引入shadow.css
        shadowRoot.innerHTML = `
            <head><link rel="stylesheet" href="./shadow.css" type="text/css"></link></head>
            <span class="btn-primary">我是dow DOM 内的 Button, click Me!</span>`;
    }
</script>
</body>
</html>

结果如下,只对 Shadow DOM的元素生效:


浏览器提供了一个方法: customElements.define() , 来进行自定义标签的定义。该方法接受三个参数:

  • 自定义元素的名称,一个 DOMString 标准的字符串,为了防止自定义元素的冲突,必须是一个带短横线连接的名称(e.g. custom-tag)。
  • 定义自定义元素的一些行为,类似于 React、Vue 中的生命周期。
  • 扩展参数(可选),该参数类型为一个对象,且需要包含 extends 属性,用于指定创建的元素继承自哪一个内置元素(e.g. { extends: 'p' })。

下面通过一些例子,演示其用法,完整代码放到了 JS Bin 上。

两种类型的自定义元素

  • Autonomous custom elements
    独立的——它们不继承自标准的 HTML 元素。你可以通过将它们字面上写为 HTML 元素来在页面上使用它们。例如 <popup-info> 或 document.createElement("popup-info")。
  • Customized built-in elements
    继承自基本的 HTML 元素。要创建其中之一,你必须指定它们扩展的元素,并且通过设置基本元素的 is 属性指定自定义元素的名称来使用它们。例如 <p is="word-count"> 或 document.createElement("p", { is: "word-count" })

创建一个新的 HTML 标签

先看看如何创建一个全新的自定义元素。

class HelloUser extends HTMLElement {
  constructor() {
    // 必须调用 super 方法
    super();

    // 创建一个 div 标签
    const $box = document.createElement("p");
    let userName = "User Name";
    if (this.hasAttribute("name")) {
      // 如果存在 name 属性,读取 name 属性的值
      userName = this.getAttribute("name");
    }
    // 设置 div 标签的文本内容
    $box.innerText = `Hello ${userName}`;

    // 创建一个 shadow 节点,创建的其他元素应附着在该节点上
    const shadow = this.attachShadow({ mode: "open" });
    shadow.appendChild($box);
  }
}

// 定义一个名为 <hello-user /> 的元素
customElements.define("hello-user", HelloUser);
<hello-user name="hello-user"></hello-user>

这时候页面上就会生成一个 <p> 标签,其文本内容为:Hello hello-user。这种形式的自定义元素被称为: Autonomous custom elements,是一个独立的元素,可以在 HTML 中直接使用。

扩展已有的 HTML 标签

我们除了可以定义一个全新的 HTML 标签,还可以对已有的 HTML 标签进行扩展,例如,我们需要封装一个与 <ul> 标签能力类似的组件,就可以使用如下方式:

class SkillList extends HTMLUListElement {
  constructor() {
    // 必须调用 super 方法
    super();

    if (
      this.hasAttribute("skills") &&
      this.getAttribute("skills").includes(',')
    ) {
      // 读取 skills 属性的值
      const skills = this.getAttribute("skills").split(',');
      skills.forEach(skill => {
        const item = document.createElement("li");
        item.innerText = skill;
        this.appendChild(item);
      })
    }
  }
}

// 对 <ul> 标签进行扩展
customElements.define("skill-list", SkillList, { extends: "ul" });
<ul is="skill-list" skills="js,css,html"></ul>

对已有的标签进行扩展,需要用到 customElements.define 方法的第三个参数,且第二参数的类,也需要继承需要扩展标签的对应的类。使用的时候,只需要在标签加上 is 属性,属性值为第一个参数定义的名称。

生命周期

自定义元素的生命周期比较简单,一共只提供了四个回调方法:

  • connectedCallback:当自定义元素被插入到页面的 DOM 文档时调用。
  • disconnectedCallback:当自定义元素从 DOM 文档中被删除时调用。
  • adoptedCallback:当自定义元素被移动时调用。
  • attributeChangedCallback: 当自定义元素增加、删除、修改自身属性时调用。

下面演示一下使用方法:

// Create a class for the element
class Square extends HTMLElement {
  // Specify observed attributes so that
  // attributeChangedCallback will work
  static get observedAttributes() {
    return ['c', 'l'];
  }

  constructor() {
    // Always call super first in constructor
    super();

    const shadow = this.attachShadow({mode: 'open'});

    const div = document.createElement('div');
    const style = document.createElement('style');
    shadow.appendChild(style);
    shadow.appendChild(div);
  }

  connectedCallback() {
    console.log('Custom square element added to page.');
    updateStyle(this);
  }

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

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

  attributeChangedCallback(name, oldValue, newValue) {
    console.log('Custom square element attributes changed.');
    updateStyle(this);
  }
}

customElements.define('custom-square', Square);

function updateStyle(elem) {
  const shadow = elem.shadowRoot;
  shadow.querySelector('style').textContent = `
    div {
      width: ${elem.getAttribute('l')}px;
      height: ${elem.getAttribute('l')}px;
      background-color: ${elem.getAttribute('c')};
    }
  `;
}

const add = document.querySelector('.add');
const update = document.querySelector('.update');
const remove = document.querySelector('.remove');
let square;

update.disabled = true;
remove.disabled = true;

function random(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

add.onclick = function() {
  // Create a custom square element
  square = document.createElement('custom-square');
  square.setAttribute('l', '100');
  square.setAttribute('c', 'red');
  document.body.appendChild(square);

  update.disabled = false;
  remove.disabled = false;
  add.disabled = true;
};

update.onclick = function() {
  // Randomly update square's attributes
  square.setAttribute('l', random(50, 200));
  square.setAttribute('c', `rgb(${random(0, 255)}, ${random(0, 255)}, ${random(0, 255)})`);
};

remove.onclick = function() {
  // Remove the square
  document.body.removeChild(square);

  update.disabled = true;
  remove.disabled = true;
  add.disabled = false;
};


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Life cycle callbacks test</title>
    <style>
      custom-square {
        margin: 20px;
      }
    </style>
    <script defer src="main.js"></script>
  </head>
  <body>
    <h1>Life cycle callbacks test</h1>

    <div>
      <button class="add">Add custom-square to DOM</button>
      <button class="update">Update attributes</button>
      <button class="remove">Remove custom-square from DOM</button>
    </div>

  </body>
</html>

在元素被创建后,等待 5s,然后将自定义元素移动到 iframe 文档中,这时候能看到控制台会同时出现 删除元素移动元素 的 log。

HTML templates(HTML模板)

前面的案例中,有个很明显的缺陷,那就是操作 DOM 还是得使用 DOM API,相比起 Vue 得模板和 React 的 JSX 效率明显更低,为了解决这个问题,在 HTML 规范中引入了 <tempate><slot> 标签。

使用模板

模板简单来说就是一个普通的 HTML 标签,可以理解成一个 div,只是这个元素内的所以内容不会展示到界面上。

<template id="helloUserTpl">
    <p class="name">User Name</p>
    <a target="blank" class="url">##</a>
</template>

在 JS 中,我们可以直接通过 DOM API 获取到该模板的实例,获取到实例后,一般不能直接对模板内的元素进行修改,要调用 tpl.content.cloneNode 进行一次拷贝,因为页面上的模板并不是一次性的,可能其他的组件也要引用。

// 通过 ID 获取标签
const tplElem = document.getElementById('helloUserTpl');
const content = tplElem.content.cloneNode(true);

我们在获取到拷贝的模板后,就能对模板进行一些操作,然后再插入到 Shadow DOM 中。

<hello-user name="Shenfq" url="http://www.baidu.com" />

<script>
    // 通过 ID 获取标签
    const tplElem = document.getElementById('helloUserTpl');
    const content = tplElem.content.cloneNode(true);

    class HelloUser extends HTMLElement {
        constructor() {
            // 必须调用 super 方法
            super();

            // 通过 ID 获取标签
            const tplElem = document.getElementById('helloUserTpl');
            const content = tplElem.content.cloneNode(true);

            if (this.hasAttribute('name')) {
                const $name = content.querySelector('.name');
                $name.innerText = this.getAttribute('name');
            }
            if (this.hasAttribute('url')) {
                const $url = content.querySelector('.url');
                $url.innerText = this.getAttribute('url');
                $url.setAttribute('href', this.getAttribute('url'));
            }
            // 创建一个 shadow 节点,创建的其他元素应附着在该节点上
            const shadow = this.attachShadow({ mode: "closed" });
            shadow.appendChild(content);
        }
    }

    // 定义一个名为 <hello-user /> 的元素
    customElements.define("hello-user", HelloUser);
</script>

添加样式

<template> 标签中可以直接插入 <style> 标签在,模板内部定义样式。

<template id="helloUserTpl">
    <style>
        :host {
            display: flex;
            flex-direction: column;
            width: 200px;
            padding: 20px;
            background-color: #D4D4D4;
            border-radius: 3px;
        }

        .name {
            font-size: 20px;
            font-weight: 600;
            line-height: 1;
            margin: 0;
            margin-bottom: 5px;
        }

        .email {
            font-size: 12px;
            line-height: 1;
            margin: 0;
            margin-bottom: 15px;
        }
    </style>
    <p class="name">User Name</p>
    <a target="blank" class="url">##</a>
</template>

其中 :host 伪类用来定义 shadow-root的样式,也就是包裹这个模板的标签的样式。

占位元素

占位元素就是在模板中的某个位置先占据一个位置,然后在元素插入到界面上的时候,在指定这个位置应该显示什么。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<template id="helloUserTpl">
    <p class="name">User Name</p>
    <a target="blank" class="url">##</a>
    <!--占位符-->
    <slot name="desc"></slot>
</template>

<hello-user name="asdasdasd" url="http://www.baidu.com">
    <p slot="desc">你好,我是slot</p>
</hello-user>

<script>
    // 通过 ID 获取标签
    const tplElem = document.getElementById('helloUserTpl');
    const content = tplElem.content.cloneNode(true);

    class HelloUser extends HTMLElement {
        constructor() {
            // 必须调用 super 方法
            super();

            // 通过 ID 获取标签
            const tplElem = document.getElementById('helloUserTpl');
            const content = tplElem.content.cloneNode(true);

            if (this.hasAttribute('name')) {
                const $name = content.querySelector('.name');
                $name.innerText = this.getAttribute('name');
            }
            if (this.hasAttribute('url')) {
                const $url = content.querySelector('.url');
                $url.innerText = this.getAttribute('url');
                $url.setAttribute('href', this.getAttribute('url'));
            }
            // 创建一个 shadow 节点,创建的其他元素应附着在该节点上
            const shadow = this.attachShadow({ mode: "closed" });
            shadow.appendChild(content);
        }
    }

    // 定义一个名为 <hello-user /> 的元素
    customElements.define("hello-user", HelloUser);
</script>
</body>
</html>

Shadow Host 的 CSS 选择器

:host 伪类选择器
选取内部使用该部分 CSS 的 Shadow host 元素,其实也就是自定义标签元素。用法如下:

:host {
    display: block;
    margin: 20px;
    width: 200px;
    height: 200px;
    border: 3px solid #000;
}

注意::host 选择器只在 Shadow DOM 中使用才有效果。



另外,可以使用 :host 子选择器 的形式来给 Shadow Host 的子元素设置样式,比如:

:host()伪类函数
:host() 的作用是获取给定选择器的 Shadow Host。比如下面的代码:

<my-card class="my-card"></my-card>
<my-card></my-card>

<script>
    class MyCard extends HTMLElement {
        constructor () {
            super();
            this.shadow = this.attachShadow({mode: "open"});
            let styleEle = document.createElement("style");
            styleEle.textContent = `
                :host(.my-card){
                    display: block;
                    margin: 20px;
                    width: 200px;
                    height: 200px;
                    border: 3px solid #000;
                }
                :host .card-header{
                    border: 2px solid red;
                    padding:10px;
                    background-color: yellow;
                    font-size: 16px;
                    font-weight: bold;
                }
            `;
            this.shadow.appendChild(styleEle);


            let headerEle = document.createElement("div");
            headerEle.className = "card-header";
            headerEle.innerText = "My Card";
            this.shadow.appendChild(headerEle);
        }
    }

    window.customElements.define("my-card", MyCard);

</script>

:host(.my-card) 只会选择类名为 my-card 的自定义元素, 且它后面也可以跟子选择器来选择自己跟节点下的子元素。

需要注意的是::host() 的参数是必传的,否则选择器函数失效

:host-context()伪类函数
用来选择特定祖先内部的自定义元素,祖先元素选择器通过参数传入。比如以下代码:

<div id="container">
    <my-card></my-card>
</div>
<my-card></my-card>
<script>
    class MyCard extends HTMLElement {
        constructor () {
            super();
            this.shadow = this.attachShadow({mode: "open"});
            let styleEle = document.createElement("style");
            styleEle.textContent = `
                :host-context(#container){
                    display: block;
                    margin: 20px;
                    width: 200px;
                    height: 200px;
                    border: 3px solid #000;
                }
                :host .card-header{
                    border: 2px solid red;
                    padding:10px;
                    background-color: yellow;
                    font-size: 16px;
                    font-weight: bold;
                }
            `;
            this.shadow.appendChild(styleEle);


            let headerEle = document.createElement("div");
            headerEle.className = "card-header";
            headerEle.innerText = "My Card";
            this.shadow.appendChild(headerEle);
        }
    }

    window.customElements.define("my-card", MyCard);

</script>

:host-context(#container) 只会对 id 为 container 元素下的自定义元素生效

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

推荐阅读更多精彩内容

  • 组件的概念 组件,是数据和方法的一个封装,其定义了一个可重用的软件元素的功能,展示和使用,通常表现为一个或一组可重...
    zx_lau阅读 2,429评论 0 3
  • Web Components 首先来了解下 Web Components 的基本概念, Web Component...
    涅槃快乐是金阅读 719评论 0 3
  • 前言:这周完成了两场技术分享会,下周还有一场,就完成了这阶段的一个重大任务。分享会是关于 TS 的,我这两场分享会...
    CondorHero阅读 969评论 0 2
  • 前言 不知不觉,2019年即将接近尾声,现有前端三大框架也各自建立着自己的生态、自己的使用群体。从angular1...
    Kaku_fe阅读 2,751评论 0 19
  • Web Components是W3C制定的一种规范,可用于构建独立的Web应用组件,主要包含以下4个模块:模板元素...
    何幻阅读 2,503评论 0 12