前言
我寫過一篇關於 Lit 的文章,Material Design, Angular Material, MDC, MWC, Lit 的關係。
如今 material-web MWC 已經發布 1.0 了,估計 Angular 也會在不遠的將來從 material-components-web MDC 遷移到 MWC。
以後,我們要想深入理解 Angular Material 就必須對 MWC 有一定了解,而 MWC 又是基於 Lit 開發的,所以我們也需要了解 Lit。
這篇就讓我們來看看 Lit 吧。
參考
Lit 介紹
Lit 的前生是 Google 的 Polymer。它是一個幫助我們寫標準 Web Components 的庫。
它有 2 個特點:
- 標準 W3C Web Components
Lit 開發出來的組件是 W3C 規範的 Web Components,不像 Angular、React、Vue 那些都是仿冒的。
符合標準的好處是可以 Plug and Play,組件本身不依賴框架技術。
我用 Lit 寫出來的組件,可以拿到 Angular、React、Vue 任何項目裏跑。 - 提升開發體驗
在不借助任何庫的情況下,手寫 W3C Web Components 開發體驗是很差的,代碼可讀性也差。
Lit 主要就是爲了解決這些問題而誕生的。
它藉助 Decorator 和 Template literals 特性,實現了聲明式定義組件和 MVVM。
Lit Getting Started
Lit 的目的是開發出 W3C Web Components,所以要掌握 Lit 就必須先掌握 W3C Web Components。
不熟悉的朋友們,請先看我以前寫的這篇 DOM – Web Components。
安裝
Lit 是可以用在純 HTML、CSS、JS 上的,但是會降低開發體驗。所以我還是鼓勵大家用 TypeScript。
我這裏搭配 Vite 做演示,你想改用 Webpack 或 Rollup 也都可以。
yarn create vite
它默認會有一些 sample code,我們洗掉它,從一個乾淨的開始。
index.css
* { margin: 0; padding: 0; box-sizing: border-box; }
index.ts 清空
index.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite + Lit + TS</title> <link rel="stylesheet" href="./src/index.css" /> <script type="module" src="/src/index.ts"></script> </head> <body> </body> </html>
創建組件
Web Components 是由 HTML Templates、Shadow DOM、Custom Elements 三種獨立的技術搭配而成的。
而 Lit 把它們組合在一起了。
hello-world.ts
import { LitElement, css, html } from 'lit'; import { customElement } from 'lit/decorators.js'; // 定義組件 // 取代了 window.customElements.define('hello-world', HelloWorldElement); @customElement('hello-world') export class HelloWorldElement extends LitElement { // 取代了 <template> render() { return html`<h1>Hello World</h1>`; } // 取代了 <style> static styles = css` h1 { color: red; } `; } // declare type for TypeScript declare global { interface HTMLElementTagNameMap { 'hello-world': HelloWorldElement; } }
HTML 和 CSS 用了 Template literals 技術。它不像 Angular 搞 compiler 黑魔法,這個只是單純的 JS runtime render。
Tips: template literals 對 IDE 不友好,需要插件 lit-html 和 vscode-styled-components。
使用組件
index.ts
import './hello-world'; setTimeout(() => { // 動態使用 const helloWorld = document.createElement('hello-world'); document.body.appendChild(helloWorld); }, 3000);
記得要 import 先。
index.html
<body> <!-- 靜態使用 --> <hello-world></hello-world> </body>
效果
Lit の Shadow DOM
所有原生 Shadow DOM 的特性,在 Lit 都可以用。
比如::host、<slot>、::slotted()、::part()。
除了 :host-context(),相關提問:Stack Overflow – :host-context not working as expected in Lit-Element web component
主要是因爲 Firefox 和 Safari 本來就不支持 :host-context 所以 Lit 乾脆就完全不支持了。可以使用 CSS Variables 作爲替代方案。
從這裏也能看出,Lit 實現 Web Components 的手法我們直覺認爲的不太一樣,它裏面動了一些手腳。
Lit の Custom Elements
所有原生 Custom Elements 的特性,在 Lit 都可以用。
比如 lifecycle:connectedCallback、disconnectedCallback、attributeChangedCallback、static get observedAttributes。
Lit の MVVM
到目前爲止,我們看到的 @customElement、extends LitElement、html``、css`` 只是 Lit 的小角色。
真正讓 Lit 發亮起來的是它的 MVVM,這也是 W3C Web Components 最缺失的功能。
MVVM 的中心思想
MVVM 的宗旨就是不要直接操作 DOM,不要調用 DOM API,凡事都通過 MVVM 庫去控制。
Lit 提供了很多種 binding、listening、query 的方式去取代 DOM 操作。
binding & listening
@customElement('hello-world') export class HelloWorldElement extends LitElement { @state() private value = 'default value'; private updateValue() { this.value = 'new value'; } render() { return html` <h1>${this.value}</h1> <button @click="${this.updateValue}">update value</button> `; } static styles = css``; }
效果
如果你熟悉 Angular,那應該對 Lit 的語法不會感到太陌生。它倆其實挺像的。畢竟都是 Google 出品,師出同門嘛。
我們逐個來看
有一個 value 屬性,@state() 表示這個屬性會被用於模板。這時 Lit 就知道每當這個屬性值發生變化,那就需要 re-render(它具體如何實現重渲染我不清楚,估計不會是大面積的替換,應該會做性能優化)
有一個 updateValue 方法,調用它就更新 value 屬性。
把 value 屬性插入模板。
@click 是一個特殊字符串,表示監聽 click 事件,接着把 updateValue 方法插入模板。
至此,Lit 就掌握足夠信息,可以做監聽和 update DOM 了。
Attribute & Property
@state 和 @property 的區別是,一個是 private 一個是 public。
@customElement('hello-world') export class HelloWorldElement extends LitElement { @property({ type: Number, attribute: 'number-value' }) numberValue!: number; @property({ type: Boolean, attribute: 'bool-value' }) boolValue = false; render() { return html` <h1>${this.numberValue.toFixed(4)}</h1> <h1>${this.boolValue}</h1>`; } static styles = css``; }
外部 HTML 控制
<hello-world number-value="50" bool-value></hello-world>
外部 JS 操控
document.querySelector('hello-world')!.numberValue = 50;
document.querySelector('hello-world')!.boolValue = false;
Dispatch Event
@customElement('hello-world') export class HelloWorldElement extends LitElement { private handleClick() { this.dispatchEvent(new CustomEvent('clickhelloworld', { bubbles: true })); } render() { return html`<h1 @click="${this.handleClick}">Hello World</h1>`; } static styles = css``; }
外部監聽
document.body.addEventListener('clickhelloworld', e => { console.log('clicked', e.target); // <hello-world> });
注:組件 this.dispatchEvent 的這個 this 只的是 <hello-world> 這個 element。所以 event 不需要設置 composed。
用 Lit 重寫 Counter Component
在 DOM – Web Components 文章的結尾,我寫了一個 Counter Component,我們現在用 Lit 重寫一遍。
最終效果是這樣
counter.ts
import { LitElement, css, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; @customElement('counter-component') export class HTMLCounterElement extends LitElement { @property({ type: Number }) step = 1; @state() private number = 0; private minus() { this.number -= this.step; } private plus() { this.number += this.step; } render() { return html` <div class="counter"> <button class="minus" @click="${this.minus}">-</button> <span class="number">${this.number}</span> <button class="plus" @click="${this.plus}">+</button> </div> `; } static styles = css` .counter { display: flex; gap: 16px; } .counter :is(.minus, .plus) { width: 64px; height: 64px; } .counter .number { width: 128px; height: 64px; border: 1px solid gray; font-size: 36px; display: grid; place-items: center; } `; } declare global { interface HTMLElementTagNameMap { 'counter-component': HTMLCounterElement; } }
index.html
<counter-component step="10"></counter-component>
Lit 的侷限
我們拿 material-web MWC 和 material-components-web MDC 做對比。
MDC 是傳統手法,先有 HTML、CSS,然後 JS 做 binding。
MWC 是 Web Components 手法,沒有 HTML、CSS,一切都是 JS 生成的。
結論就是 Lit 的渲染依賴於 JS。
如果我們用 Lit 開發企業網站,需要 SEO,那就需要搞服務端渲染 Server-side rendering。但這個目前還在 experimental 階段。