跟 Web Components 打個啵

What are Web Components

  • Web Components 是 W3C 定義的新標準,目前還在草案階段。

Why are they important

  • 前端組件化

    • bootstrap

    // 初始化
  $('#myModal').modal({
    keyboard: false
  });

  // 顯示
  $('#myModal').modal('show');

  // 關閉事件
  $('#myModal').on('hidden.bs.modal', function (e) {
    // do something...
  });
  • atom

// 初始化組件
var dialog = new Dialog(
     trigger: '#trigger-btn',
   title: '我是自定義的標題',
   content: 'hello world',
   buttons: ['submit', 'cancel']
});

// 顯示
dialog.show();

// 關閉事件
dialog.after('hide', function() {
     // do something...
});
      
  • 統一標準、減少輪子

  • 簡化代碼,提高可維護性

    ![gmail](http://www.html5rocks.com/zh/tutorials/webcomponents/customelements/gmail.png)
    
<hangout-module>
  <hangout-chat from="Paul, Addy">
    <hangout-discussion>
      <hangout-message from="Paul" profile="profile.png"
           datetime="2013-07-17T12:02">
        <p>Feelin' this Web Components thing.</p>
        <p>Heard of it?</p>
      </hangout-message>
    </hangout-discussion>
  </hangout-chat>
  <hangout-chat>...</hangout-chat>
</hangout-module>

關鍵技術

  • HTML Imports

  • HTML Templates

  • Custom Elements

  • Shadow DOM

雖然大部分瀏覽器還不支持 Web Components ,但是有個叫做 webcomponents.js 的兼容庫,可以讓 Web Components 在不支持它的瀏覽器上運行起來。只要你在項目中引入這個庫,就可以在其他瀏覽器中將 Web Components 用起來。

HTML Imports

通過<link>標籤來引入 HTML 文件,使得我們可以用不同的物理文件來組織代碼。

<link rel="import" href="http://example.com/component.html" >

注意:受瀏覽器同源策略限制,跨域資源的 import 需要服務器端開啓 CORS。
Access-Control-Allow-Origin: example.com

通過import引入的 HTML 文件是一個包含了 html, css, javascript 的獨立 component。

<template>
    <style>
        .coloured {
            color: red;
        }
    </style>
    <p>My favorite colour is: <strong class="coloured">Red</strong></p>
</template>
<script>
    (function() {
        var element = Object.create(HTMLElement.prototype);
        var template = document.currentScript.ownerDocument.querySelector('template').content;
        element.createdCallback = function() {
            var shadowRoot = this.createShadowRoot();
            var clone = document.importNode(template, true);
            shadowRoot.appendChild(clone);
        };
        document.registerElement('favorite-colour', {
            prototype: element
        });
    }());
</script>

HTML Templates

關於 HTML 模板的作用不用多講,用過 mustache、handlbars 模板引擎就對 HTML 模板再熟悉不過了。但原來的模板要麼是放在script 元素內,要麼是放在 textarea 元素內,HTML 模板元素終於給了模板一個名正言順的名分: <template>

原來的模板形式:

  • script 元素

<script type="text/template">
    <div>
        this is your template content.
    </div>
</script>
  • textarea 元素

<textarea style="display:none;">
    <div>
        this is your template content.
    </div>
</textarea>

現在的模板形式:

  • template 元素

<template>
    <div>
        this is your template content.
    </div>
</template>

主要有四個特性:

  1. 惰性:在使用前不會被渲染;

  2. 無副作用:在使用前,模板內部的各種腳本不會運行、圖像不會加載等;

  3. 內容不可見:模板的內容不存在於文檔中,使用選擇器無法獲取;

  4. 可被放置於任意位置:即使是 HTML 解析器不允許出現的位置,例如作爲 <select> 的子元素。

Custom Elements

自定義元素允許開發者定義新的 HTML 元素類型。帶來以下特性:

  1. 定義新元素

  2. 元素繼承

  3. 擴展原生 DOM 元素的 API

定義新元素

使用 document.registerElement() 創建一個自定義元素:

var Helloworld = document.registerElement('hello-world', {
  prototype: Object.create(HTMLElement.prototype)
});

document.body.appendChild(new Helloworld());

標籤名必須包含連字符 ' - '

  • 合法的標籤名:<hello-world><my-hello-world>

  • 不合法的標籤名:<hello_world><HelloWorld>

元素繼承

如果 <button> 元素不能滿足你的需求,可以繼承它創建一個新元素,來擴展 <button> 元素:

var MyButton = document.registerElement('my-button', {
  prototype: Object.create(HTMLButtonElement.prototype)
});

擴展原生 API

var MyButtonProto = Object.create(HTMLButtonElement.prototype);

MyButtonProto.sayhello = function() {
  alert('hello');
};

var MyButton = document.registerElement('my-button', {
  prototype: MyButtonProto
});


var myButton = new MyButton();
document.body.appendChild(myButton);

myButton.sayhello(); // alert: "hello"

實例化

使用 new 操作符:

var myButton = new MyButton();
myButton.innerHTML = 'click me!';
document.body.appendChild(myButton);

或,直接在頁面插入元素:

<my-button>click me!</my-button>

生命週期

元素可以定義特殊的方法,來注入其生存週期內的關鍵時間點。生命週期的回調函數名稱和時間點對應關係如下:

  • createdCallback: 創建元素實例時

  • attachedCallback: 向文檔插入實例時

  • detachedCallback: 從文檔移除實例時

  • attributeChangedCallback(attrName, oldVal, newVal): 添加,移除,或修改一個屬性時

var MyButtonProto = Object.create(HTMLButtonElement.prototype);

MyButtonProto.createdCallback = function() {
  this.innerHTML = 'Click Me!';
};

MyButtonProto.attachedCallback = function() {
  this.addEventListener('click', function(e) {
    alert('hello world');
  });
};

var MyButton = document.registerElement('my-button', {
  prototype: MyButtonProto
});

var myButton = new MyButton();
document.body.appendChild(myButton);

Shadow DOM

Shadow DOM 是一個 HTML 的新規範,其允許開發者封裝自己的 HTML 標籤、CSS 樣式和 JavaScript 代碼。Shadow DOM 使得開發人員可以創建類似 <input type="range"> 這樣自定義的一級標籤。

web 開發經典問題:封裝。如何保護組件的樣式不被外部 css 樣式侵入,如何保護組件的 dom 結構不被頁面的其他 javascript 腳本修改。大家都用過 Bootstrap,如果要使用其中的某些組件,例如 modal,通常會把組件的 DOM 結構複製過來。

<div class="modal fade">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
        <h4 class="modal-title">Modal title</h4>
      </div>
      <div class="modal-body">
        <p>One fine body&hellip;</p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

這樣一坨複製過來的代碼,大多數時候並沒有仔細瞭解,任何時候一個不小心都有可能覆蓋了其中的一個 class 樣式,這裏面可能潛在很多小 bug。Shadow Dom 可以很好的解決組件封裝問題。

一個例子說明,什麼是 Shadow DOM ?

瀏覽器渲染 <input type="range"> 標籤,顯示結果如下:

<input type="range">

看起來似乎很簡單,只有一個 input 標籤而已。但實際上是這樣的: 

顯示 shadow dom 需要開啓 Chrome 開發者工具的 'Show user agent shadow DOM' 

創建 Shadow DOM

使用 createShadowRoot 創建影子根節點,其餘的操作跟普通 DOM 操作沒有太大區別。

<div class="widget">Hello, world!</div>  
<script>  
  var host = document.querySelector('.widget');
  var root = host.createShadowRoot();
    
  var header = document.createElement('h1');
  header.textContent = 'Hello, I am Shadow DOM.';

  var paragraph = document.createElement('p');
  paragraph.textContent = 'This is the content.';

  root.appendChild(header);
  root.appendChild(paragraph);
</script> 

宿主節點的原有內容 Hello, world! 不會被渲染,取而代之的是 shadow root 裏的內容。

使用 content 標籤

<div class="widget">shadow dom</div>  
<template>
<h1>Hello, I am <content></content></h1>
</template>
<script>  
  var host = document.querySelector('.widget');
  var root = host.createShadowRoot();
    
  var template = document.querySelector('template').content;
  root.appendChild(document.importNode(template, true));
</script> 

使用 <content> 標籤,我們創建了一個插入,其將 .widget 中的文本投射出來,使之得以在我們的影子節點 <h1> 中展示。上面的例子最終渲染成 Hello, I am shadow dom

Shadow DOM 樣式

Shadow DOM 和常規 DOM 之間存在一個邊界,這個邊界能防止常規 DOM 的樣式泄露到 Shadow DOM 中來。

<style>
p.normal, p.shadow {
  color: red;
  font-size: 18px;
}
</style>
<p class="normal">我是一個普通文本</p>
<p class="shadow"></p>
<script>
    var host = document.querySelector('.shadow');
    var root = host.createShadowRoot();
    root.innerHTML = `
    <style>
    p { 
      color: blue; 
      font-size: 24px; 
    } 
    </style>
    <p>我是一個影子文本</p>`;
</script>

:host 選擇器

通過 :host 選擇器可以設置宿主元素的樣式。

<style>
p {
  color: red;
  font-size: 18px;
}
</style>
<p class="normal">我是一個普通文本</p>
<p class="shadow"></p>
<script>
  var host = document.querySelector('.shadow');
  var root = host.createShadowRoot();
  root.innerHTML = `
  <style>
  :host(p.shadow) { 
    color: blue; 
    font-size: 24px; 
  } 
  </style>
  我是一個影子文本`;
</script>

注意上例中 shadow DOM 內的選擇器是 :host(p.shadow),而不是跟外部平級的 :host(p)。 因爲:host(p) 的優先級低於外部的 p 選擇器,所以不會生效。需要使用 :host(p.shadow) 提升優先級,才能將 .shadow 中的樣式覆蓋。

::shadow 僞類選擇器

有時你可能會想讓使用者打破影子邊界的壁壘,讓他們能夠給你的組件添加一些樣式,使用 ::shadow 僞類選擇器我們可以賦予用戶重寫我們默認定義的自由。

<style>
p span, 
p::shadow span {
  color: red;
  font-size: 18px;
}
</style>
<p class="normal"><span>我是一個普通文本</span></p>
<p class="shadow"></p>
<script>
  var host = document.querySelector('.shadow');
  var root = host.createShadowRoot();
  root.innerHTML = `
  <style>
  span { 
    color: blue; 
    font-size: 24px; 
  } 
  </style>
  <span>我是一個影子文本</span>`;
</script>

參考文獻

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