在当下,前端三巨头vue react ng都是提倡组件化开发的,在原生领域,web-components也逐渐成为标准。近段时间大热的omi就是基于web-components实现的
web-components主要由3部分组成
从字面意思可以知道这是自定义元素的意思。区别于原生html元素,我们可以自己定义它的行为。按照是否从原生html元素继承,可分下面两类
custom-elements 比较赞的一点是具有以下的生命周期
static get observedAttributes(){return ['需要监听的属性']}
使用,表示哪些属性变化才会触发这个生命周期。对于动态attributes进行渲染,这个非常好用
一个Autonomous custom elements web-components通常使用方法如下
class App extends HTMLElement {
static get observedAttributes() {
return ['text'];
}
constructor() {
super();
// 在constructor中初始化
// 创建一个shadow元素,会css隔离的,一些原生html元素例如video等也是基于shadowdom实现的
const shadow = this.attachShadow({mode: 'open'});
const div = document.createElement('div');
// web-components内的样式,外部不影响
const style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);
}
connectedCallback() {}
disconnectedCallback() {}
adoptedCallback() {}
attributeChangedCallback(name, oldValue, newValue) {}
}
customElements.define('my-app', App);
如果是扩展原生元素的web-components则是类似
class CustomP extends HTMLParagraphElement {
...
}
customElements.define('custom-p', CustomP,{extend:'p'});
shadom-dom操作和平常的dong操作差不多,对this.attachShadow({mode: 'open'});
。shadow-dom最大的好处就是实现了dom隔离。例如css只会对内部的shadow-dom有效,并不影响外部的元素。这应该是css最完美的解决方案了,目前很多组件化css解决方案css modules、各种css in js都不太优雅
// this是custom-element
const shadow = this.attachShadow({mode: 'open'});
const div = document.createElement('div');
const style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);
类似于vue的概念,用来实现html复用和插槽效果
<template id="my-paragraph">
<style>
p {
color: white;
background-color: #666;
padding: 5px;
}
</style>
<p>My paragraph</p>
</template>
// mdn例子
customElements.define('my-paragraph',
class extends HTMLElement {
constructor() {
super();
let template = document.getElementById('my-paragraph');
let templateContent = template.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(templateContent.cloneNode(true));
}
})
web-components的使用非常方便,有几种方法 1、直接html中使用自定义标签
<custom-element></custom-element>
2、通过js引入
const CustomElement = customElements.get('custom-element');
const customElement = new CustomElement();
// or
document.createElement('custom-elemen')
// append进dom
实际开发结合polymer体验更佳
最后写了个web-compoennts todolist
代码如下
// TodoList.js
class TodoList extends HTMLElement {
constructor() {
super();
this.shadowdom = this.attachShadow({ mode: "open" });
this.handleRemove = this.handleRemove.bind(this);
}
get data() {
const dataAttribute = this.getAttribute("data");
if (dataAttribute) {
return Array.isArray(dataAttribute)
? dataAttribute
: JSON.parse(dataAttribute);
} else {
return [];
}
}
set data(val) {
this.setAttribute("data", JSON.stringify(val));
this.render();
}
handleRemove(e) {
this.remove(e.detail.index);
}
connectedCallback() {
this.render();
this.shadowdom.addEventListener("sub", this.handleRemove);
}
disconnectedCallback() {
this.shadowdom.removeEventListener("sub", this.handleRemove);
}
//渲染内容
render() {
// 简便起见,每次渲染前先清空shadowdom的内容
let last = null;
while ((last = this.shadowdom.lastChild)) {
this.shadowdom.removeChild(last);
}
this.data.forEach((item, index) => {
const todoiterm = new (customElements.get("todo-iterm"))();
todoiterm.innerHTML = `<span slot='text'>${item}</span>`;
todoiterm.setAttribute("data-index", index);
this.shadowdom.appendChild(todoiterm);
});
}
addIterm(text) {
this.data = [...this.data, text];
}
remove(deleteIndex) {
this.data = this.data.filter((item, index) => index != deleteIndex);
}
}
customElements.define("todo-list", TodoList);
// TodoIterm.js
class TodoIterm extends HTMLElement {
constructor() {
super();
const template = document.getElementById("list-item");
const templateContent = template.content;
const shadowdom = this.attachShadow({ mode: "open" });
shadowdom.appendChild(templateContent.cloneNode(true));
shadowdom.getElementById("sub").onclick = e => {
const event = new CustomEvent("sub", {
bubbles: true,
detail: { index: this.dataset.index},
});
this.dispatchEvent(event)
};
}
}
customElements.define("todo-iterm", TodoIterm);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>web-components</title>
<script src="./TodoList.js"></script>
<script src="./TodoIterm.js"></script>
</head>
<body>
<template id="list-item">
<style>
* {
color: red;
}
</style>
<li><slot name="text">nothing write</slot><button id="sub">-</button></li>
</template>
<!-- <todo-list></todo-list> -->
<div>
<input id='input'/>
<button id='add'>+</button>
</div>
<script>
// 加载web compoennts
const List = customElements.get('todo-list');
const todoList = new List()
document.body.appendChild(todoList)
document.getElementById('add').onclick = function(){
const value = document.getElementById('input').value
todoList.addIterm(value)
}
</script>
</body>
</html>
一些需要注意的地方: 1、通过html传递属性值,由于是通过attributes传入,所以都是字符串 2、组件之间的通信传递需要通过自定义事件