搜索Web Components的一般是不使用Web Components的,就像你和我,可是因为闲着没事和热爱学习,又或者应付一下前端面试,不得不了解下。javascript
不使用Web Components是有不少客观缘由的,例如你和Web Components之间大概有n个前端框架,这些框架是你面试工做必备的,不单你要有基于其它们的大型应用的实战,并且还要有理解其源码原理的能力。html
因此Web Components很天然成为你的短板之一。前端
我的以为这些年前端一直围绕着一个问题:组件化,好比前端三国演义(React,Vue,Angular)的发展及其火热程度足以说明,可是有一个问题一直没解决,那就是组件复用问题,说白就是怎么防止重复造轮子问题,尽管我不认为这是问题,可是W3C认为这是问题,因此咱们不得不来学习Web Components。java
W3C的解决方法就是,经过制定规范和标准,让全部浏览器提供一系列平台API来支持自定义HTML标签,这样你基于这些API所编写的组件就能够运行在全部支持的浏览器中,从而达到组件复用。git
若是你被W3C或者网上其它言论洗脑,你会相信Web Components就是将来,什么三国演义都会俱往矣,因此你须要知道怎么样去编写Web Components。github
首先Web Components基于四个规范:自定义元素,影子DOM,ES模块,HTML模版,我劝你仍是别点进去,规范就像懒婆娘的裹脚,又臭又长,一个简单的hello world或todo才是浅尝辄止的咱们所须要的。web
hello-world.js面试
const template = document.createElement('template'); template.innerHTML = ` <style> h2 { background-color: blue; } </style> <h2>Hello: <span>World</span></h2> `; class HelloWorld extends HTMLElement { constructor() { super(); this._shadowRoot = this.attachShadow({ mode: 'open' }); this._shadowRoot.appendChild(template.content.cloneNode(true)); this.$headline = this._shadowRoot.querySelector('h2'); this.$span = this._shadowRoot.querySelector('span'); } connectedCallback() { if(!this.hasAttribute('color')) { this.setAttribute('color', 'orange'); } if(!this.hasAttribute('text')) { this.setAttribute('text', ''); } this._render(); } static get observedAttributes() { return ['color', 'text']; } attributeChangedCallback(name, oldVal, newVal) { switch(name) { case 'color': this._color = newVal; break; case 'text': this._text = newVal; break; }; this._render(); } _render() { this.$headline.style.color = this._color; this.$span.innerHTML = this._text; } } window.customElements.define('hello-world', HelloWorld);
hello-world.htmlapi
<!DOCTYPE html> <html> <head> <title>Hello World Web Components</title> </head> <body> <hello-world></hello-world> <script src="./hello-world.js"></script> </body> </html>
能够看出写一个组件仍是算简单的,其实如今你的脑海里大体有个Web Components的雏形了,接下来咱们来分析一下每一行的代码,及其所对应的规范和标准。数组
class HelloWorld extends HTMLElement {...}
只要继承HTMLElement类,你即可以编写自定义标签/元素,里面的构造函数和生命周期函数暂时都不要管。
const template = document.createElement('template'); template.innerHTML = ...
HTML<template>标签里面包含了具体样式和DOM,
影子DOM
this._shadowRoot = this.attachShadow({ mode: 'open' }); this._shadowRoot.appendChild(template.content.cloneNode(true));
HelloWorld类和模版目前仍是没有任何关联,影子DOM的第一个做用就是粘合HelloWorld类和模版,而后做为一个子DOM树被添加。同时影子DOM也能够保证样式不会被污染或泄漏,有点模块化封装的意思。
window.customElements.define('hello-world', HelloWorld);
组件注册以后,经过引用这个js文件,你即可以使用这个Web Components了。
至此,建立一个简单Web Components的流程,咱们都大体了解了,可是想要应用到大型复杂的项目仍是须要更多的API支持。
class MyElement extends HTMLElement { constructor() { // always call super() first super(); console.log('constructed!'); } connectedCallback() { console.log('connected!'); } disconnectedCallback() { console.log('disconnected!'); } attributeChangedCallback(name, oldVal, newVal) { console.log(`Attribute: ${name} changed!`); } adoptedCallback() { console.log('adopted!'); } }
元素建立但还没附加到document时执行,一般用来初始化状态,事件监听,建立影子DOM。
元素被插入到DOM时执行,一般用来获取数据,设置默认属性。
元素从DOM移除时执行,一般用来作清理工做,例如取消事件监听和定时器。
元素关注的属性变化时执行,若是监听属性变化呢?
static get observedAttributes() { return ['my-attr']; }
只要my-attr属性变化,就会触发attributeChangedCallback
自定义元素被移动到新的document时执行。
如今咱们几乎知道全部关于Web Components的知识,让咱们看一下怎么用它作一个稍微复杂的TODO应用。
简单作一下逻辑划分,咱们须要两个自定义组件:
接受一个数组做为属性,能够添加/删除/标记to-do。
设置描述信息,索引属性,checked属性
to-do-app.js
const template = document.createElement("template"); template.innerHTML = ` <style> :host { display: block; font-family: sans-serif; text-align: center; } button { border: none; cursor: pointer; } ul { list-style: none; padding: 0; } </style> <h1>To do App</h1> <input type="text" placeholder="添加新的TODO"></input> <button>添加</button> <ul id="todos"></ul> `; class TodoApp extends HTMLElement { constructor() { super(); this._shadowRoot = this.attachShadow({ mode: "open" }); this._shadowRoot.appendChild(template.content.cloneNode(true)); this.$todoList = this._shadowRoot.querySelector("ul"); thisl.todos = []; } } window.customElements.define("to-do-app", TodoApp);
咱们经过setter和getter实现添加一个新属性:
set todos(value) { this._todos = value; this._renderTodoList(); } get todos() { return this._todos; }
当传递给这个属性值时渲染to-do列表:
_renderTodoList() { this.$todoList.innerHTML = ""; this._todos.forEach((todo, index) => { let $todoItem = document.createElement("div"); $todoItem.innerHTML = todo.text; this.$todoList.appendChild($todoItem); }); }
咱们须要对输入框和按钮添加事件:
constructor() { super(); ... this.$input = this._shadowRoot.querySelector("input"); this.$submitButton = this._shadowRoot.querySelector("button"); this.$submitButton.addEventListener("click", this._addTodo.bind(this)); }
添加一个TOOD:
_addTodo() { if(this.$input.value.length > 0){ this._todos.push({ text: this.$input.value, checked: false }) this._renderTodoList(); this.$input.value = ''; } }
如今咱们能够TODO app能够添加todo了。
为了实现删除和标记,咱们须要建立一个to-do-item.js
to-do-item.js
const template = document.createElement('template'); template.innerHTML = ` <style> :host { display: block; font-family: sans-serif; } .completed { text-decoration: line-through; } button { border: none; cursor: pointer; } </style> <li class="item"> <input type="checkbox"> <label></label> <button>❌</button> </li> `; class TodoItem extends HTMLElement { constructor() { super(); this._shadowRoot = this.attachShadow({ 'mode': 'open' }); this._shadowRoot.appendChild(template.content.cloneNode(true)); this.$item = this._shadowRoot.querySelector('.item'); this.$removeButton = this._shadowRoot.querySelector('button'); this.$text = this._shadowRoot.querySelector('label'); this.$checkbox = this._shadowRoot.querySelector('input'); this.$removeButton.addEventListener('click', (e) => { this.dispatchEvent(new CustomEvent('onRemove', { detail: this.index })); }); this.$checkbox.addEventListener('click', (e) => { this.dispatchEvent(new CustomEvent('onToggle', { detail: this.index })); }); } connectedCallback() { // We set a default attribute here; if our end user hasn't provided one, // our element will display a "placeholder" text instead. if(!this.hasAttribute('text')) { this.setAttribute('text', 'placeholder'); } this._renderTodoItem(); } _renderTodoItem() { if (this.hasAttribute('checked')) { this.$item.classList.add('completed'); this.$checkbox.setAttribute('checked', ''); } else { this.$item.classList.remove('completed'); this.$checkbox.removeAttribute('checked'); } this.$text.innerHTML = this._text; } static get observedAttributes() { return ['text']; } attributeChangedCallback(name, oldValue, newValue) { this._text = newValue; } } window.customElements.define('to-do-item', TodoItem);
在_renderTodolist中开始渲染咱们的to-do-item,当让使用以前要import,这就咱们以前没说的ES模块规范。
_renderTodoList() { this.$todoList.innerHTML = ''; this._todos.forEach((todo, index) => { let $todoItem = document.createElement('to-do-item'); $todoItem.setAttribute('text', todo.text); this.$todoList.appendChild($todoItem); }); }
组件经过事件通知父组件(删除按钮和勾选框):
this.$removeButton.addEventListener('click', (e) => { this.dispatchEvent(new CustomEvent('onRemove', { detail: this.index })); }); this.$checkbox.addEventListener('click', (e) => { this.dispatchEvent(new CustomEvent('onToggle', { detail: this.index })); }); });
父组件监听:
$todoItem.addEventListener('onRemove', this._removeTodo.bind(this)); $todoItem.addEventListener('onToggle', this._toggleTodo.bind(this));
组件监听属性变化:
static get observedAttributes() { return ["text", "checked", "index"]; } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "text": this._text = newValue; break; case "checked": this._checked = this.hasAttribute("checked"); break; case "index": this._index = parseInt(newValue); break; } }
如今咱们todo app都已经编写完成
to-do-app.js
import "./components/to-do-item"; const template = document.createElement("template"); template.innerHTML = ` <style> :host { display: block; font-family: sans-serif; text-align: center; } button { border: none; cursor: pointer; } ul { list-style: none; padding: 0; } </style> <h3>Raw web components</h3> <br> <h1>To do</h1> <input type="text" placeholder="Add a new to do"></input> <button>✅</button> <ul id="todos"></ul> `; class TodoApp extends HTMLElement { constructor() { super(); this._shadowRoot = this.attachShadow({ mode: "open" }); this._shadowRoot.appendChild(template.content.cloneNode(true)); this.$todoList = this._shadowRoot.querySelector("ul"); this.$input = this._shadowRoot.querySelector("input"); this.todos = []; this.$submitButton = this._shadowRoot.querySelector("button"); this.$submitButton.addEventListener("click", this._addTodo.bind(this)); } _removeTodo(e) { this._todos.splice(e.detail, 1); this._renderTodoList(); } _toggleTodo(e) { const todo = this._todos[e.detail]; this._todos[e.detail] = Object.assign({}, todo, { checked: !todo.checked }); this._renderTodoList(); } _addTodo() { if (this.$input.value.length > 0) { this._todos.push({ text: this.$input.value, checked: false }); this._renderTodoList(); this.$input.value = ""; } } _renderTodoList() { this.$todoList.innerHTML = ""; this._todos.forEach((todo, index) => { let $todoItem = document.createElement("to-do-item"); $todoItem.setAttribute("text", todo.text); if (todo.checked) { $todoItem.setAttribute("checked", ""); } $todoItem.setAttribute("index", index); $todoItem.addEventListener("onRemove", this._removeTodo.bind(this)); $todoItem.addEventListener("onToggle", this._toggleTodo.bind(this)); this.$todoList.appendChild($todoItem); }); } set todos(value) { this._todos = value; this._renderTodoList(); } get todos() { return this._todos; } } window.customElements.define("to-do-app", TodoApp);
to-do-item.js
const template = document.createElement("template"); template.innerHTML = ` <style> :host { display: block; font-family: sans-serif; } .completed { text-decoration: line-through; } button { border: none; cursor: pointer; } </style> <li class="item"> <input type="checkbox"> <label></label> <button>❌</button> </li> `; class TodoItem extends HTMLElement { constructor() { super(); this._shadowRoot = this.attachShadow({ mode: "open" }); this._shadowRoot.appendChild(template.content.cloneNode(true)); this.$item = this._shadowRoot.querySelector(".item"); this.$removeButton = this._shadowRoot.querySelector("button"); this.$text = this._shadowRoot.querySelector("label"); this.$checkbox = this._shadowRoot.querySelector("input"); this.$removeButton.addEventListener("click", e => { this.dispatchEvent(new CustomEvent("onRemove", { detail: this.index })); }); this.$checkbox.addEventListener("click", e => { this.dispatchEvent(new CustomEvent("onToggle", { detail: this.index })); }); } connectedCallback() { if (!this.hasAttribute("text")) { this.setAttribute("text", "placeholder"); } this._renderTodoItem(); } static get observedAttributes() { return ["text", "checked", "index"]; } attributeChangedCallback(name, oldValue, newValue) { switch (name) { case "text": this._text = newValue; break; case "checked": this._checked = this.hasAttribute("checked"); break; case "index": this._index = parseInt(newValue); break; } } _renderTodoItem() { if (this.hasAttribute("checked")) { this.$item.classList.add("completed"); this.$checkbox.setAttribute("checked", ""); } else { this.$item.classList.remove("completed"); this.$checkbox.removeAttribute("checked"); } this.$text.innerHTML = this._text; } set index(val) { this.setAttribute("index", val); } get index() { return this._index; } get checked() { return this.hasAttribute("checked"); } set checked(val) { if (val) { this.setAttribute("checked", ""); } else { this.removeAttribute("checked"); } } } window.customElements.define("to-do-item", TodoItem);
index.html
<!DOCTYPE html> <html> <head> <title>Web Components</title> </head> <body> <to-do-app></to-do-app> <script src="to-do-app.js"></script> </body> </html>
不知道时候会用到Web Components,就像我在文中开篇所讲,你和Web Components中间隔着那些框架,并且Web Components也没有解决我目前的任何问题,还有存在浏览器兼容问题(尽管能够用polyfill),我都建议你们保持观望,暂时放弃。