# web components

组件化是web发展的方向,前端三架马车(react、vue、angular)无一不是组件框架。

chrome因为自家浏览器的缘故,一直在推动浏览器的原生组件,即 Web Components API (opens new window)。 相比第三方框架,原生组件简单直接,符合直觉,不用加载任何外部模块,代码量小。 目前,它还在不断发展,但已经可用于生产环境,几乎现代浏览器都支持了,区别只是支持度的问题。像IE之类的古董就不用考虑了。

这里简单记录下怎么使用。

# 自定义元素

假设我们要自定义的组件名称为user-card,根据规范,自定义元素的名称必须包含连词线,用与区别原生的 HTML 元素。在DOM中这样使用:

<user-card></user-card>

怎么定义呢?

class UserCard extends HTMLElement {
  constructor() {
    super();
  }
}

window.customElements.define('user-card', UserCard);

# 自定义元素的内容

class UserCard extends HTMLElement {
  constructor() {
    super();

    var image = document.createElement('img');
    image.src = 'https://semantic-ui.com/images/avatar2/large/kristy.png';
    image.classList.add('image');

    var container = document.createElement('div');
    container.classList.add('container');

    var name = document.createElement('p');
    name.classList.add('name');
    name.innerText = 'User Name';

    container.append(name);
    this.append(image, container);
  }
}

上面代码最后一行,this.append()this表示自定义元素实例。

完成这一步以后,自定义元素内部的 DOM 结构就已经生成了。

# <template>标签

使用 JavaScript 写上一节的 DOM 结构很麻烦,Web Components API 提供了<template>标签,可以在它里面使用 HTML 定义 DOM

<template id="userCardTemplate">
  <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
  <div class="container">
    <p class="name">User Name</p>
  </div>
</template>

然后,改写自定义元素的类:

class UserCard extends HTMLElement {
  constructor() {
    super();

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    this.appendChild(content);
  }
}  

上面代码中,获取<template>节点以后,克隆了它的所有子元素,这是因为可能有多个自定义元素的实例,这个模板还要留给其他实例使用,所以不能直接移动它的子元素。

# 添加样式

自定义元素还没有样式,可以给它指定全局样式,比如下面这样。

user-card {
}

但是,组件的样式应该与代码封装在一起,只对自定义元素生效,不影响外部的全局样式。所以,可以把样式写在<template>里面。

<template id="userCardTemplate">
  <style>
    :host {
     display: flex;
     ...
   }
   ...
  </style>

  <img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
  <div class="container">
    <p class="name">User Name</p>
  </div>
</template>

上面代码中,<template>样式里面的:host伪类,指代自定义元素本身。

# 自定义参数

<user-card>内容现在是在<template>里面设定的,为了方便使用,把它改成参数。

<user-card
  image="https://semantic-ui.com/images/avatar2/large/kristy.png"
  name="User Name"
  email="yourmail@some-email.com"
></user-card>

<template>代码也相应改造。

<template id="userCardTemplate">
  <style>...</style>

  <img class="image">
  <div class="container">
    <p class="name"></p>
  </div>
</template>

最后,改一下类的代码,把参数加到自定义元素里面。

class UserCard extends HTMLElement {
  constructor() {
    super();

    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');
    this.appendChild(content);
  }
}
window.customElements.define('user-card', UserCard);  

# Shadow DOM

我们不希望用户能够看到<user-card>的内部代码,Web Component 允许内部代码隐藏起来,这叫做 Shadow DOM,即这部分 DOM 默认与外部 DOM 隔离,内部任何代码都无法影响外部。

自定义元素的this.attachShadow()方法开启 Shadow DOM,详见下面的代码。

class UserCard extends HTMLElement {
  constructor() {
    super();
    var shadow = this.attachShadow( { mode: 'closed' } ); //添加这一句

    //下面几句没有变化
    var templateElem = document.getElementById('userCardTemplate');
    var content = templateElem.content.cloneNode(true);
    content.querySelector('img').setAttribute('src', this.getAttribute('image'));
    content.querySelector('.container>.name').innerText = this.getAttribute('name');

    shadow.appendChild(content); //原来是this添加孩子,现在使用shadow
  }
}
window.customElements.define('user-card', UserCard);

上面代码中,this.attachShadow()方法的参数{ mode: 'closed' },表示 Shadow DOM 是封闭的,不允许外部访问。

完整代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>web component</title>
</head>
<body>
    <template id="userCardTemplate">
        <style>
            :host {
                display: flex;
                align-items: center;
                width: 450px;
                height: 180px;
                background-color: #d4d4d4;
                border: 1px solid #d5d5d5;
                box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
                border-radius: 3px;
                overflow: hidden;
                padding: 10px;
                box-sizing: border-box;
                font-family: 'Poppins', sans-serif;
            }
            .image {
                flex: 0 0 auto;
                width: 160px;
                height: 160px;
                vertical-align: middle;
                border-radius: 5px;
            }
            .container {
                box-sizing: border-box;
                padding: 20px;
                height: 160px;
            }
            .container > .name {
                font-size: 20px;
                font-weight: 600;
                line-height: 1;
                margin: 0;
                margin-bottom: 5px;
            }
            .container > .email {
                font-size: 12px;
                opacity: 0.75;
                line-height: 1;
                margin: 0;
                margin-bottom: 15px;
            }
            .container > .button {
                padding: 10px 25px;
                font-size: 12px;
                border-radius: 5px;
                text-transform: uppercase;
            }
        </style>

        <img class="image">
        <div class="container">
            <p class="name"></p>
            <p class="email"></p>
            <button class="button">Follow John</button>
        </div>
    </template>
    <user-card
            image="https://semantic-ui.com/images/avatar2/large/kristy.png"
            name="aabb"
            email="jw@some-email.com"
    ></user-card>
    <script>
        class UserCard extends HTMLElement {
            constructor() {
                super();
                var shadow = this.attachShadow( { mode: 'closed' } );

                var templateElem = document.getElementById('userCardTemplate');
                var content = templateElem.content.cloneNode(true);
                content.querySelector('img').setAttribute('src', this.getAttribute('image'));
                content.querySelector('.container>.name').innerText = this.getAttribute('name');
                content.querySelector('.container>.email').innerText = this.getAttribute('email');

                shadow.appendChild(content);
            }
        }
        window.customElements.define('user-card', UserCard);
    </script>
</body>
</html>

# 组件封装

假设我们的组件代码放在外部文件modules/userCard.js

const template = document.createElement('template');
template.innerHTML = `
  <style>
      ...
  </style>

  <img class="image">
  <div class="container">
      <p class="name"></p>
  </div>
`;

export default class UserCard extends HTMLElement {
    constructor () {
        super();
        ...
    }
}

再在主页面这样引用:

 <script type="module">
    import UserCard from './modules/userCard.js';
    window.customElements.define('user-card', UserCard);
</script>

另外,google 主推的 web components 框架从Polymer 已经转向 lit-element 了,就是市面上没有什么水花。多学点儿总是没错的。

参考阮一峰大神Web Components 入门实例教程 (opens new window)

可以使用这个组件库 (opens new window)