# 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
了,就是市面上没有什么水花。多学点儿总是没错的。
可以使用这个组件库 (opens new window)