【Web Components】Web Components 的最新開發方法

Web Components

MDN 上對 Web Components 這個名詞的解釋是:

Web Components是一套不同的技術,允許您創建可重用定製元素(它們的功能封裝在您的代碼之外)並且在您的web應用中使用它們。

當然,你也可以從W3C上學習組件規範

Web Components[以下簡稱"WC"],使用自定義元素(custom elements)來代替div,故能使用div的得房就能使用它。因此,使用WC,只需要在HTML中引入js文件即可。它不併不像目前主流的組件框架,需要外部支撐。例如,如果你要使用React組件,那你大概率的情況下要使用ReactJS。

開始前的準備

文章中的組件、自定義標籤、自定義組件其實描述的是同一個東西

Ploymer

WC 在整個前端的語境下更偏向於提供符合 DOM 標準的規範,而 Polymer 則是在這種規範之上的一種框架封裝,雖然Ploymer不是必須的,但是使用 Polymer 可以帶來更便利的組件化開發體驗。

ShadyDOM與ShadyCSS

  • ShadyDOM:是 Shadow DOM 的 polyfill 的官方名稱。 ▪ 它通過劫持 HTMLElement 的原型方法來實現一些 Shadow DOM 節點擁有的功能,實際上它的原理是把節點添加到了真實(light) DOM 節點之上
  • ShadyCSS:也是 polyfill 的名稱,它提供了一些 Shadow DOM 節點內樣式的封裝,使得可以在真實 DOM 中模擬 scoped style 的效果。它的原理是通過解析和重寫 style 節點內部的樣式規則來實現的

開始創建一個自定義組件

WC一般具有以下幾個部分:

  • Shadow Dom
  • Custom Elements
  • HTML Imports
  • HTML Templates

你可以在CanIUse上查到上述4部分的目前的支持情況。

OK , 現在我們新建一個項目目錄my-app,如下:

![](media/15575787033865/15575806427046.jpg)

然後,在命令行中開啓http服務(請先確保你已安裝該nodeJS服務npm install -g http-server):

$ http-server

第一個Web Components

//index.html
<!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>My-App</title>
    <link rel="import" href="elements/my-app.html" id="my-app">
</head>
<body>
    <my-app></my-app>
</body>
</html>

//my-app.html
<template>
    <style>
        p {
            color: #ccc;
        }
    </style>
    <p>這是一個自定義元素!</p>
</template>

<script>
    class MyAppElement extends HTMLElement {
        constructor() {
            super();
            const shadowRoot = this.attachShadow({mode:'open'});
            const myApp = document.getElementById('my-app').import;
            const templateNode = myApp.getElementsByTagName('template')[0].content.cloneNode(true);
            shadowRoot.appendChild(templateNode);
        }
        connectedCallback() {
            console.log('my-app element is connected');
        }
    }
    customElements.define('my-app', MyAppElement);
</script>

注意,這裏我們使用HTML imports在index.html中導入了my-app.html,這僅能在Chrome中實現。作爲ES6模塊的一部分,在將來可能會被廢棄。因爲,Ploymer正在開發相關的實現。

現在我們可以看到在頁面上顯示:

![](media/15575787033865/15575813447083.jpg)

當然,除此之外我們也可以使用ES6 import實現:

ES6 import

現在的目錄結構是這樣:在elements目錄下新建一個my-app.js和my-date.js。
在這裏插入圖片描述

//index.html
<!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>My-App</title>
    <script type="module" src="/elements/my-app.js"></script>
</head>
<body>
    <my-app></my-app>
</body>
</html>

//my-app.js
import './my-date.js'

class MyAppElement extends HTMLElement {
    constructor() {
        super();
        const shadowRoot = this.attachShadow({mode:'open'});
        shadowRoot.innerHTML = this.template();
    }
    connectedCallback() {
        console.log('my-app element is connected');
    }
    template() {
        return `
            <style>
                p {
                color: #f00;
                }
            </style>

            <p>This is a custom element!</p>
            <my-date></my-date>
        `;
    }
}
customElements.define('my-app', MyAppElement);

//my-date.js
class MyDateElement extends HTMLElement {
    constructor() {
        super();
        this.now = new Date();

        const shadowRoot = this.attachShadow({mode:'open'});
        shadowRoot.innerHTML = this.template();
    }
    connectedCallback() {
        console.log('my-date element is connected');
    }
    template() {
        return `
            <p>現在是<time datetime="${this.now.toISOString()}">${this.now.toLocaleString()}</p>
        `;
    }
}
customElements.define('my-date', MyDateElement);

現在我們可以看到頁面:

在這裏插入圖片描述

但是,使用es6 import也會存在缺點:

  • HTML標籤和JS沒有分開。我們看到,HTML標籤是寫在template()方法中。而前面一種方案是將HTML標籤寫在<template>標籤中。

  • 無法在不同時刻加載某些文件。我們看到,import在第一時間把需要的JS文件都加載進來了。

爲了解決這個問題,我們需要採取以下方法:

  1. 動態加載JS
//動態加載JS
const scriptElement = document.createElement('script');
scriptElement.src = '/elements/my-date.js';
document.head.appendChild(scriptElement);

  1. import()

使用具有函數性的import()函數進行動態導入,返回Promise。

目前適用於Chrome 63+和Safari 11+

參見:

解釋

下面,使用一個代碼示例,解釋代碼中不同部分的含義。請根據代碼註釋進行理解。

  • index.html
<!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>Document</title>
    <script src="app.js"></script>
</head>
<body>
    <my-element></my-element>
</body>
</html>

  • app.js
//生命週期函數的順序:constructor -> attributeChangedCallback -> connectedCallback
//attributeChangedCallback要在connectedCallback之前執行的原因:
//1.web組件上的屬性主要用來初始化配置。這意味着當組件被插入DOM時,這些配置需要可以被訪問了
customElements.define('my-element',class extends HTMLElement{
    static get observedAttributes() {
        return ['disabled'];
    }
    //構造函數一般是用來初始化狀態和設置Shadow DOM。
    //當時一個元素被創建時將會調用構造函數,如document.createElement
    constructor() {
        super();
        //使用Shadow DOM,自定義元素的HTML和CSS完全封裝在組件內。這意味着元素將以單個的HTML標籤出現在文檔的DOM樹種。其內部的結構將會放在#shadow-root。
        let style = document.createElement('style');

        const shadowRoot = this.attachShadow({mode: 'open'});// 定義一個Shadow root,mode:'open'意味着可以再開發者工具找到它並與之交互;mode:closed則相反
        //Shadow DOM還提供了局部作用域的CSS
        //所有的CSS都只應用於組件本身
        //元素將只繼承最小數量從組件外部定義的CSS,甚至可以不從外部繼承任何CSS
        //在實際插入DOM前,它是不可見也不可解析的。這意味着定義在內部的任何資源都無法獲取,任何內部定義的CSS和JavaScript只有當它被插入DOM中時,纔會被執行。
        //組件的所有樣式都被定義在style標籤內,如果你想使用一個常規的<link rel="stylesheet">標籤,你也可以獲取外部樣式
        
        shadowRoot.innerHTML = `     
            <div id="container" class="disabled">
                <h1>web Components</h1>
            </div>
        `;
        shadowRoot.appendChild(style);
        //當Shadow root被創建之後,你可以使用document對象的所有DOM方法,
        this.container = this.shadowRoot.querySelector('#container');//查找元素
        
        //還可以使用:host選擇器對組件本身進行樣式設置
        //從外部定義在組件本身的樣式優先於使用:host在Shadow DOM中定義的樣式
        style.textContent = '.disabled {opacity: 0.4;}' +
            'h1 { text-decoration: underline; }' +
            ':host-context(h1) { font-style: italic; }' +
            ':host-context(h1):after { content: " - no links in headers!" }' +
            ':host-context(article, aside) { color: gray; }' +
            ':host(.footer) { color : red; }' +
            ':host { background: rgba(0,0,0,0.1); padding: 2px 5px; }'+
            ':host {--background-color: #1387ff;}'+
            '#container {background-color: var(--background-color);}';

    }
    //當這個元素被插入DOM樹的時候將會觸發這個方法
    //在connectedCallback之後進行元素的設置
    //這是唯一可以確定所有的屬性和子元素都已經可用的辦法,如document.body.appendChild
    connectedCallback() {


    }
    //當元素從DOM中移除的時候將會調用它
    //在用戶關閉瀏覽器或者瀏覽器tab的時候,不會調用這個方法
    disconnectCallback() {

    }
    //當元素通過調用document.adoptNode(element)被採用到文檔時將會被調用
    adoptedCallback() {

    }
    //每當將屬性添加到observedAttributes的數組中時,就會調用這個函數
    //這個方法調用時兩個參數分別爲舊值和新值
    //這個方法只有當被保存在observedAttributes數組的屬性改變時,纔會調用,其他屬性改變則不會
    //web組件上的屬性主要用來初始化配置,狀態
    //當組件被插入DOM時,這些配置需要可以被訪問了
    attributeChangedCallback(attr,oldVal,newVal) {
        if(attr === 'disabled') {
            if(this.disabled) {  
              this.container.classList.add('disabled');      
            }      
            else {
              this.container.classList.remove('disabled')      
            }
        }
    }
})


//調用customElements.get(‘my-element’)來獲取這個元素構造函數的引用
const el = customElements.get('my-element');
//使用new element()來代替document.createElement()去實例一個元素
const myElement = new el();


//使用customElement.whenDefined升級元素時,可以調用回調,並會返回一個promise

customElements.whenDefined('my-element')
.then(() => {
  // my-element is now defined
})

在這裏插入圖片描述

  • 在瀏覽器中開啓shadow dom
    在這裏插入圖片描述

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章