作為開發者,我們都知道元件化、標準化和程式碼重用的重要性,前端也從未停止過對前端元件化的嘗試,產生了各式各樣的元件化技術,從 Vue React 等前端框架,到 webpack 這樣的全站打包工具
但前端一直缺乏這樣一個模組化標準和瀏覽器級別的原生元件化方案
Web Components 是 WHATWG 和 W3C 正在嘗試的 Web 元件化方案,為元件化的前端開發提供瀏覽器級別的支持。它由四項主要技術組成:Shadow DOM、Custom Elements、HTML Import、HTML Template
Polymer 專案是 Google 的基於 Web Components 機制的框架,定位於簡單的 Polyfill 和易用性封裝,包括資料綁定,模板聲明,事件系統等。Google 在去年就已經將其應用到了 YouTube 上
Polymer 3.0 在 20 天前剛剛發布,正好 B 站播放器近期需要重構所有 UI 元件,所以做了這樣的一個調研,下文所有 demo 托管在 polymer-demos,這些小 demo 只作為一些簡單體驗,想了解 Polymer 的完整功能建議閱讀官方文檔
瀏覽器支持#
目前使用 Web Components 的最大阻礙就是瀏覽器支持程度低,且 Polyfills 體積相對偏大(90+kb)
目前只有新版 Chrome Opera 和 Safari 可以提供完整的原生支持,具體支持情況可以參考 caniuse.com,使用 Polyfills 後可以支持到 Edge IE11+ Firefox Safari9+
Polyfills 有三個主要的文件:
webcomponents-bundle.js
: 包含了所有 polyfillswebcomponents-loader.js
: 可以檢測瀏覽器支持情況,然後去加載對應的 polyfills,對有原生支持的瀏覽器可以減少不必要的浪費custom-elements-es5-adapter.js
: 註冊 Custom Elements 時需要使用 ES6 語法,所以當瀏覽器不支持 ES6 時需要做額外的處理,再引用這個文件就好了
總的來說兼容最多瀏覽器的最佳實踐是這樣的:
<scirpt src="webcompoments-loader.js"></scirpt>
<scirpt src="custom-elements-es5-adapter.js"></scirpt>
<script src="index.js"></script>
其中 webcompoments-loader.js
必須單獨引用,custom-elements-es5-adapter.js
可以跟 polymer
和你的程式碼用 Webpack 合到一起,但注意 custom-elements-es5-adapter.js
不要做額外的編譯,其他程式碼用 babel 編譯成 ES5,完整實踐可以參考 polymer-demos
Custom elements#
下面嘗試定義一個最簡單的自定義元素,從 PolymerElement
繼承一個類,然後傳給 window.customElements.define
效果
{% raw %}
{% endraw %}
HTML 代碼
<demo-custom-elements></demo-custom-elements>
JS 代碼
import { PolymerElement } from '@polymer/polymer';
class DemoCustomElements extends PolymerElement {
constructor() {
super();
this.textContent = `我是自定義元素。`;
}
}
window.customElements.define('demo-custom-elements', DemoCustomElements);
Shadow dom#
Shadow dom 是一個隱藏、獨立的 DOM,它的 HTML CSS 和行為與常規的 DOM 樹分離,這樣不同的功能不會混在一起,內外的 CSS 也互不影響
Shadow dom 不是一個新事物,一直以來,瀏覽器用它來封裝一個元素的內部結構。以 <video>
元素為例。你所能看到的只是一個 <video>
標籤,實際上,在它的 Shadow dom 中包含一系列的按鈕和控制器
下面例子中,Shadow dom 裡的 p 標籤定義了 CSS 屬性 color
,它不會洩漏到外部
效果
{% raw %}
我在 demo-shadow-dom 外面。因為封裝,demo-shadow-dom 的樣式不會洩漏到我這裡。
{% endraw %}HTML 代碼
<style>
html {
--my-background: #eee;
}
</style>
<demo-shadow-dom></demo-shadow-dom>
<p>我在 demo-shadow-dom 外面。因為封裝,demo-shadow-dom 的樣式不會洩漏到我這裡。</p>
JS 代碼
import { PolymerElement, html } from '@polymer/polymer';
export class DemoShadowDom extends PolymerElement {
static get template () {
return html`
<style>
p {
color: #F5712C;
background-color: var(--my-background);
}
</style>
<p>我是 DOM 元素。</p>
<p>這是我的 shadow DOM!</p>
`;
}
}
window.customElements.define('demo-shadow-dom', DemoShadowDom);
HTML templates#
使用 <template>
和 <slot>
組成 shadow DOM
效果
{% raw %}
我是自定義插槽。
{% endraw %}
HTML 代碼
<demo-html-template>
<p>我是自定義插槽。</p>
</demo-html-template>
JS 代碼
import { PolymerElement, html } from '@polymer/polymer';
import '@polymer/polymer/lib/elements/dom-repeat.js'
import { DemoShadowDom } from './demo-shadow-dom';
class DemoHTMLTemplate extends DemoShadowDom {
constructor() {
super();
this.employees = [
{
name: 'Blog',
link: 'https://diygod.me'
},
{
name: 'GitHub',
link: 'https://github.com/DIYgod'
},
];
}
static get template () {
return html`
<strong>模板:</strong>
<template is="dom-repeat" items="{{employees}}">
<p><a href="{{item.link}}">{{item.name}}</a></p>
</template>
<strong>插槽:</strong>
<slot></slot>
<strong>超級模板:</strong>
${super.template}
`;
}
}
window.customElements.define('demo-html-template', DemoHTMLTemplate);
資料綁定#
支持雙向的資料綁定,你可以嘗試編輯下面的輸入框,或者直接在控制台修改屬性 document.querySelector('demo-data').owner1 = 'DIYgay'
,屬性改變會即時反映到 DOM 裡
效果
{% raw %}
{% endraw %}
HTML 代碼
<demo-data owner1="DIYgod1"></demo-data>
JS 代碼
import { PolymerElement, html } from '@polymer/polymer';
import '@polymer/iron-input';
class DemoData extends PolymerElement {
constructor() {
super();
this.owner3 = 'DIYgod3';
}
static get properties () {
return {
owner1: {
type: String,
value: 'DIYgod',
},
owner2: {
type: String,
value: 'DIYgod2',
}
};
}
static get template () {
return html`
<p>這是 <b>[[owner1]]</b> 的元素。</p>
<p>這是 <b>[[owner2]]</b> 的元素。</p>
<p>這是 <b>{{owner3}}</b> 的元素。</p>
<iron-input bind-value="{{owner1}}">
<input is="iron-input" placeholder="在這裡輸入你的名字...">
</iron-input>
`;
}
}
window.customElements.define('demo-data', DemoData);
自定義事件#
下面我們來給我們的自定義元素定義一個名為 diygod
的事件,綁定事件回調的方法跟正常事件一樣
效果
{% raw %}
{% endraw %}
HTML 代碼
<demo-events></demo-events>
<script>
document.querySelector('demo-events').addEventListener('diygod', function (e) {
alert(e.detail.msg);
});
</script>
JS 代碼
import { PolymerElement, html } from '@polymer/polymer';
export class DemoEvents extends PolymerElement {
static get template () {
return html`
<button on-click="handleClick">踢我</button>
`;
}
handleClick(e) {
this.dispatchEvent(new CustomEvent('diygod', {
detail: {
msg: 'diygod 事件觸發'
}
}));
}
}
window.customElements.define('demo-events', DemoEvents);