LitElement - 像 React 一樣寫 WebComponent
LitElement - 像 React 一樣寫 WebComponent
2018/6/21
在 React, Vue 還不存在的時代,純粹透過 jQuery 或是 DOM API 撰寫 User interface ,不過問題就是 html, js 容易進入混亂的狀態,沒有元件的概念,不好模組化,所以許多人一直發(ㄔㄨㄥˊ)明(ㄗㄠˋ)工(ㄔㄜ)具(ㄌㄨㄣˊ)幫助開發
也有另一派人馬希望可以透過改善瀏覽器本身 API 來做到元件化開發,因此 WebComponent 的概念就出現了,WebComponent 包含了許多面向,我覺得最重要的部分是 Custom Elements
什麼是 Custom Element?
簡單來說,透過它可以宣告自定義的 html element
舉個例子,你可以定義一個 hello-custom-element
:
class HelloCustomElement extends HTMLElement {}
customElements.define('hello-custom-element', HelloCustomElement);
這樣寫就有一個最基本的 custom element 了,不過沒有任何功能,如果你在 HTML 裡面這樣寫:
<hello-custom-ele>
<h1>Original H1</h1>
</hello-custom-ele>
看起來什麼事情都沒發生,就像是亂寫一個 HTML tag 而已,那個 h1
還是正常的顯示出來,但是其實 <hello-custom-ele>
已經是一個可以客製行為的 html 元件,我們來驗證一下:
class HelloCustomElement extends HTMLElement {
constructor() {
super()
debugger
}
}
一個有效的 custom element 最基本要
extends HTMLElement
並且在constructor()
裡頭先呼叫super()
進入中斷點之後,嘗試在 console 上輸入 this.
,你會發現有 this.innerHTML
, this.getAttribute('some-prop')
等 API 可呼叫
完整實作請參考 Github Repo 或是 commit,可以使用 http-server
真正的隔離 - shadow DOM
在 HelloCustomElement
建立一個 shadow root (DOM):
constructor() {
super()
this.attachShadow({mode: 'open'})
debugger
}
進入中斷點應該可以發現畫面上的 Original H1
h1
消失了,這時在 console 上輸入 this.shadowRoot.innerHTML = 'hello'
,去看 HTML 的樣子:
接著有趣的地方來了,如果在 <hello-custom-ele>
外加上隨便的 h1
:
<hello-custom-ele>
<h1>original H1</h1>
</hello-custom-ele>
<h1>out side of hello-custom-ele</h1>
並且讓 HelloCustomElement
的 innerHTML 來點 style:
constructor() {
super()
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style> h1 { color: #5bbd72; } </style>
<h1>hello-custom-ele rendered!</h1>
`
}
你會發現外面的 out side of hello-custom-ele
h1
依然還是原本的顏色,這個就是 shadow root 的特性,裡面跟外面的 selector 是互相選取不到的,同時 document.querySelectorAll('h1')
也互相選不到,這樣 css rule 就可以寫得更簡單了,也可以確保內外行為不會互相干擾
完整實作請參考 commit
lit-html
一直用 innerHTML
來建立元件內容也不是個辦法,這也是 React, Vue 等等 framework 想要解決的問題,這邊要來介紹兩個東西
- HTML template: 其實就是一個預設不會 render 的 HTML DOM,作為模板使用,有點 VDOM 的感覺
- JS template literals: ES6 用反引號(back-tick)包起來的模板字串,可以寫成
`hello ${name}`
,不過還有一個更神奇的some_fun`some ${blabla}`
,稱為 tagged template literal- 這個
some_fun
可以拿到這個 template 裡面每個挖空區域的內容${...}
,可以試試看在 console 上輸入console.log`1 + 1 = ${1 + 1}`
- 基本上這樣的設計就是讓大家可以在 JS 裡面內嵌其他語言
- 這個
JS template LITeral + HTML template = lit-html,大概是這樣的感覺,這是 2017 年 Google Polymer 的實驗專案,Github repo 在此,直接把 <hello-custom-ele />
改用 lit-html render 吧:
yarn add lit-html
import { html, render as litRender } from '/node_modules/lit-html/lit-html.js';
class HelloCustomEle extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'});
litRender(this.render('#5bbd72'), this.shadowRoot);
}
render(color) {
return html`
<style> h1 { color: ${color}; } </style>
<h1>hello-custom-ele rendered!</h1>
`
}
}
customElements.define('hello-custom-ele', HelloCustomEle);
有點像 React 了?分析一下發生了什麼事情:
html`
` 像是寫 JSX 一樣寫 HTML 結構,建立並回傳TemplateResult
,一個 lit-html 的資料格式litRender(TemplateResult, this.shadowRoot)
(lit-html
提供的render
function)- 如果
TemplateResult
是第一個 instance 的第一次 render,建立 HTML template 並且 cache 起來(lit-html 內部有個 template cache) - 如果這個 instance
this.shadowRoot
的第一次 render,把 HTML template 內容複製到this.shadowRoot
上面,否則用新的挖空處值${...}
值更新 DOM
- 如果
注意這邊用了 ES6
import
語法,<script />
要有type='module'
才能動 完整實作請參考 commit,要先用yarn
安裝lit-html
LitElement
既然開始有點像 React 了,那來看一下缺了哪些 React 的主要功能:
props
,state
and updating- Event handling
從上面的 <hello-custom-ele />
看來要自己加 callback 來做到這些事情,而且我們可以開始注意到有許多共通行為應該要包成一個 base class 來做了,像是 shadow DOM 的建立跟 component lifecycle (initialization, rendering, re-rendering) 等等
2018 Google IO 發表了 Polymer 3.0,但是我有興趣的是 lit-html 的完整包裝 LitElement,改用 LitElement 之後:
import { LitElement, html } from '@polymer/lit-element';
const color1 = '#5bbd72'
const color2 = '#42a2c3'
class HelloCustomEle extends LitElement {
static get properties() { return { color: String } }
constructor() {
super();
this.color = this.color || color1;
}
_render({ color }) {
return html`
<style> h1 { color: ${color}; } </style>
<h1 on-click="${() => { this.color = this.color === color1 ? color2 : color1 } }">
hello-custom-ele rendered!
</h1>
`
}
}
customElements.define('hello-custom-ele', HelloCustomEle);
LitElement
作為 base class 可以幫你:
- 建立 shadow DOM 以及 render
- 用
static get properties()
當成state
/props
,而且弄成 setter,被設定時會 re-render on-some-event
掛上 listener
去瀏覽器點 hello-custom-ele rendered!
應該可以看到顏色的改變,也可以 <hello-custom-ele color='#42a2c3' />
指定 property,看起來就像是一個 native html element,component 實作起來跟 React 也有 87% 像了
或許大家都不否認 React JSX 定義 Component 結構的方式簡單又明瞭,lit-html 的出現可能只是順水推舟,核心實作也沒多少東西,但是看到這邊大家一定會想:
瀏覽器支援度呢?
以上的範例只有 import
的路徑有透過 polymer-cli
改寫過,我用 Chrome 67 可以在不加任何 polyfill 的情況下動起來,但是其他瀏覽器就無法保證了,可參考各個 feature 相容性:
基本上 Polymer / WebComponent 都是由 Google 推動的,有點像是把這些 feature 趕上自己的瀏覽器,然後就說現在大部分的瀏覽器都支援了…(Chrome 加 android Chrome 市佔率就超過 50%)
現在 Custom Element 已經被寫進 HTML Spec,因此今天來嚐鮮撰寫 Custom Element 的工具,如果順利的話這可能是未來透過瀏覽器內建 API 撰寫網頁元件的好方法之一!