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,如下:
然後,在命令行中開啓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
正在開發相關的實現。
現在我們可以看到在頁面上顯示:
當然,除此之外我們也可以使用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文件都加載進來了。
爲了解決這個問題,我們需要採取以下方法:
- 動態加載JS
//動態加載JS
const scriptElement = document.createElement('script');
scriptElement.src = '/elements/my-date.js';
document.head.appendChild(scriptElement);
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