今天 ,Web 組件已經從本質上改變了HTML。初次接觸時,它看起來像一個全新的技術。Web組件最初的目的是使開發人員擁有擴展瀏覽器標簽的能力,可以自由的進行定制組件。面對新的技術,你可能會覺得無從下手。那這篇文章將為你揭開Web組件神秘的面紗。如果你已經熟知HTML標簽和DOM編程,已經擁有了大量可用的Web組件,那么你已經是Web組件專家了。
Web組件的現狀
隨著各式各樣的用戶需求,瀏覽器的原生組件已經無法滿足需求。Web組件也就變得越來越重要。
我們將以自定義一個傳統三方插件為例來介紹Web組件。
首先,需要引用插件的CSS和JavaScript資源:
<link rel="stylesheet" type="text/css" href="my-widget.css" /> <script src="my-widget.js"></script>
接下來,我們需要向頁面中添加占位符。
<div data-my-widget></div>
最后,我們需要使用腳本來找到并且實例化這個占位符為Web組件。
// 使用 jQuery 初始化組件 $(function() { $('[data-my-widget]').myWidget(); });
通過以上是三個基本步驟。已經完成了在頁面中添加了自定義插件,但是瀏覽器無法確定自定義組件的生命周期,如果通過以下方式聲明則使自定義組件生命周期變得清晰了。
el.innerHTML = '<div data-my-widget></div>';
因為這不是一個內置的組件,我們現在必須手動實例化新組件,
$(el).find('[data-my-widget]').myWidget();
避免這種復雜設置方法的有效方式是完全抽象DOM交互。不過,這個動作也比較復雜,需要創建框架或者庫來自定義組件。
面臨的問題
組件一旦被聲明,占位符已經被替代為原生的HTML標記:
<div data-my-widget> <div class="my-widget-foobar"> <input type="text" class="my-widget-text" /> <button class="my-widget-button">Go</button> </div> </div>
這樣做的弊端是,自定義組件的標記和普通HTML組件的標記混雜在一起,沒有清晰的分割和封裝。這就不可避免的會出現命名及樣式等沖突。
Web組件的產生
隨著三方Web組件的發展,它已經成為了Web開發不可或缺的部分:
<!—導入: --> <link rel="import" href="my-widget.html" /> <!—使用:--> <my-widget />
在這個實例中,我們通過導入HTML來添加組件并且立即使用。
更重要的是,因為<my-widget />是瀏覽器原生支持的組件,它直接掛在瀏覽器的生命周期中,允許我們像添加原生組件一樣添加三方組件。
el.innerHTML = '<my-widget />'; // 插件當前已經被實例化
當查看這個組件的HTML 源碼,你會發現它僅僅是一個單一的標簽。如果啟用瀏覽器Shadow DOM 特性,才可以查看標簽內的組件,你將會發現一些有趣的事情,
當我們談論Web組件時,我們不是在談論一門新技術。Web組件最初的目的是給我們封裝能力,它可以通過自定義組件和Shadow DOM 技術來實現。所以,接下來,我們將著重介紹下這兩項技術。介紹以上兩個技術之前,我們最好先梳理下已知瀏覽器原生組件。
已知的HTML組件
我們知道組件可以通過HTML標記或JavaScript來實例化:
<input type="text" /> document.createElement('input'); el.innerHTML = '<input type="text" />';
使用JaveScript實例化:
document.createElement('input')
document.createElement('div')
添加帶有屬性的HTML標簽:
// 創建帶有屬性的input標簽... el.innerHTML = '<input type="text" value="foobar" />'; //這時value屬性已經同步 el.querySelector('input').value;
組件可以響應屬性的變化:
// 如果我們更改value 屬性值 input.setAttribute('value', 'Foobar'); //屬性值會立即更改 input.value === 'Foobar'; // true
組件可以有內部隱藏的DOM結構:
<!—使用一個input實現復雜的日歷功能--> <input type="date" /> // 盡管其內部結構比較復雜,但是已經封裝成為一個組件 dateInput.children.length === 0; // true
組件可以使用子組件:
<!—可以給組件提供任意個 'option' 標簽--> <select> <option>1</option> <option>2</option> <option>3</option> </select>
組件可以為其子組件提供樣式:
dialog::backdrop { background: rgba(0, 0, 0, 0.5); }
最后,組件可以有內置樣式。和自定義插件不同,我們不需要為瀏覽器的原生控件引用CSS文件。
有了以上的了解,我們已經具備了解Web組件的基礎。使用自定義組件和Shadow DOM,我們可以在我們的插件中定義所有這些標準行為。
自定義組件
注冊一個新組件也比較簡單:
var MyElement = document.register('my-element'); // 'document.register' 返回一個構造函器
你也許注意到上面的自定義組件名稱包含一個連接符。這是為了確保自定義組件名稱不和瀏覽器內置組件不沖突。
現在<my-element />這個組件具備了原生組件的特性,
所以,自定義組件也同樣可以進行普通的DOM操作:
document.create('my-element'); el.innerHTML = '<my-element />'; document.create('my-element');
構建自定義組件
當前,這個自定義組件僅僅有框架,而沒有內容,下面讓我們向其中添加一些內容:
//我們將提供'document.register'的第二個參數: document.register('my-element', { prototype: Object.create(HTMLElement.prototype, { createdCallback: { value: function() { this.innerHTML = '<h1>ELEMENT CREATED!</h1>'; } } }) });
在這個例子中,我們設置自定義組件的prototype,使用Object.create 方法創建一個繼承于HTMLElement的對象。在這個方法中修改該組件的屬性 innerHTML。
我們定義了createdCallback方法,在每次聲明實例時調用。你同樣可以有選擇性的定義attributeChangedCallback、 enteredViewCallback 和leftViewCallback等方法。
目前為止我們實現了動態修改自定義組件內容的功能,我們仍然需要提供自定義組件的封裝方法,用于隱藏其內部組件。
使用Shadow DOM實現封裝
我們需要完善下createdCallback方法。本次,除了修改innerHTML之外,我們添加一些額外的操作:
createdCallback: { value: function() { var shadow = this.createShadowRoot(); shadow.innerHTML = '<h1>SHADOW DOM!</h1>'; } }
在這個例子中, 你會注意到‘SHADOW DOM!’,但是查看源碼時你會發現只有空白的<my-element /> 標簽而已。這里使用創建Shadow Root 方法替代了直接修改頁面。
Shadow Root中的任何組件,是肉眼可見的,但是和當前頁面的樣式和DOM API相隔離。這樣就實現了自定義組件是一個獨立組件的假象。
添加“輕量級DOM”
目前為止,我們的自定義組件是空標簽,但是如果向其中添加內部組件會出現什么現象呢?
我們假設自定義組件包含的節點如下,
<my-element> 這是一個輕量級 DOM。 <i>hello</i> <i>world</i> </my-element>
一旦針對于這個組件的 Shadow Root 被創建,它的子節點不再存在。我們這些隱藏的子節點封裝為輕量級DOM節點。
如果禁用了 Shadow DOM,上面這個例子僅僅會顯示為:這是一個輕量級 DOM‘hello world’。
當我們在createdCallback方法中設置 Shadow DOM后,我們可以使用新增內容分配輕量級DOM組件到Shadow DOM 中。
createdCallback: { value: function() { var shadow = this.createShadowRoot(); // 子組件'i' 標簽現在已經消失了 shadow.innerHTML = ‘輕量級 DOM 中的 "i" 標簽為: ' + '<content select="i" />'; //現在,在 Shadow DOM 中只有 'i' 標簽是可以見的。 } }
封裝樣式
Shadow DOM 最重要的作用是創建了和當前頁面隔離的Web組件,使Web組件不受當前頁面樣式和JaveScript腳本的影響。
createdCallback: { value: function() { var shadow = this.createShadowRoot(); shadow.innerHTML = "<style>span { color: green }</style>" + "<span>I'm green</span>"; } }
反之,在 Shadow DOM 中定義的樣式也不會影響之外的標簽樣式。
<my-element /> <span>I'm not green</span>
揭露鉤子的秘密
當隱藏自定義組件內部標記,有時也需要在當前頁面對組件中的內部特定組件進行樣式設置。
例如,如果我們自定義一個日歷插件,在不允許用戶控制整個插件的情況下,允許最終用戶去定義按鈕的樣式。
這是其中的部分特性和偽組件:
createdCallback: { value: function() { var shadow = this.createShadowRoot(); shadow.innerHTML = 'Hello <em part="world">World</em>'; } }
這是在當前頁面設置自定義組件內部組件樣式的方法:
my-element::part(world) {
color: green;
}
這部分內容介紹了封裝web組件的基本方式。Shadow DOM 是我們可以任意修改Web組件中的標簽。在例子中,我們設置了“World”的樣式,但是使用者卻無法判斷它是<em>標簽。
在你嘗試自定義Web組件之前,需要確保瀏覽器的相關特性已經打開。如果使用 Chrome,在 Chrome 中打開chrome://flags ,并且開啟“experimental Web Platform features”。
這僅僅是個開始
所有本文中介紹的內容,都是模擬一些簡單的瀏覽器標準行為。我們已經習慣于和原生的瀏覽器組件進行交互,因此自定義組件的步驟并不是想象中的那個難。Web組件最終提供我們一種實現簡單、一致、可復用、封裝和組合部件的方法,這是一個有意義的開始。
文章列表