搜索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),我都建議你們保持觀望,暫時放棄。