從入門到放棄:Web Components

誰在搜索Web Components?

搜索Web Components的一般是不使用Web Components的,就像你和我,可是因爲閒着沒事和熱愛學習,又或者應付一下前端面試,不得不瞭解下。javascript

不使用Web Components是有不少客觀緣由的,例如你和Web Components之間大概有n個前端框架,這些框架是你面試工做必備的,不單你要有基於其它們的大型應用的實戰,並且還要有理解其源碼原理的能力。html

因此Web Components很天然成爲你的短板之一。前端

爲何Web Components

我的以爲這些年前端一直圍繞着一個問題:組件化,好比前端三國演義(React,Vue,Angular)的發展及其火熱程度足以說明,可是有一個問題一直沒解決,那就是組件複用問題,說白就是怎麼防止重複造輪子問題,儘管我不認爲這是問題,可是W3C認爲這是問題,因此咱們不得不來學習Web Componentsjava

W3C的解決方法就是,經過制定規範和標準,讓全部瀏覽器提供一系列平臺API來支持自定義HTML標籤,這樣你基於這些API所編寫的組件就能夠運行在全部支持的瀏覽器中,從而達到組件複用。git

Web Components的內容

若是你被W3C或者網上其它言論洗腦,你會相信Web Components就是將來,什麼三國演義都會俱往矣,因此你須要知道怎麼樣去編寫Web Componentsgithub

首先Web Components基於四個規範:自定義元素影子DOMES模塊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的雛形了,接下來咱們來分析一下每一行的代碼,及其所對應的規範和標準。數組

1. 自定義元素定義:
class HelloWorld extends HTMLElement {...}

只要繼承HTMLElement類,你即可以編寫自定義標籤/元素,裏面的構造函數和生命週期函數暫時都不要管。

2. HTML模版
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也能夠保證樣式不會被污染或泄漏,有點模塊化封裝的意思。

3. 全局註冊組件
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!');
    }
}
1. constructor()

元素建立但還沒附加到document時執行,一般用來初始化狀態,事件監聽,建立影子DOM。

2. connectedCallback()

元素被插入到DOM時執行,一般用來獲取數據,設置默認屬性。

3. disconnectedCallback()

元素從DOM移除時執行,一般用來作清理工做,例如取消事件監聽和定時器。

4. attributeChangedCallback(name, oldValue, newValue)

元素關注的屬性變化時執行,若是監聽屬性變化呢?

static get observedAttributes() {
    return ['my-attr'];
}

只要my-attr屬性變化,就會觸發attributeChangedCallback

5. adoptedCallback()

自定義元素被移動到新的document時執行。

如今咱們幾乎知道全部關於Web Components的知識,讓咱們看一下怎麼用它作一個稍微複雜的TODO應用。

TODO應用

簡單作一下邏輯劃分,咱們須要兩個自定義組件:

  • to-do-app 元素

接受一個數組做爲屬性,能夠添加/刪除/標記to-do。

  • to-to-item 元素

設置描述信息,索引屬性,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),我都建議你們保持觀望,暫時放棄。

相關文章
相關標籤/搜索