1. 簡介
什麼是 Lit?
Lit 是一個簡單的程式庫,可用於建構快速、輕量化的網頁元件,這些元件可在任何架構中運作,甚至不需架構即可運作。您可以使用 Lit 建構可供共用的元件、應用程式、設計系統等。
課程內容
如何將幾個 React 概念轉換為 Lit,例如:
- JSX 和範本
- 元件和道具
- 狀態和生命週期
- Hooks
- 子項
- 參考
- 中介狀態
建構項目
在本程式碼研究室的結尾,能將 React 元件的概念轉換為 Lit 類記錄檔。
軟硬體需求
- 最新版本的 Chrome、Safari、Firefox 或 Edge。
- 瞭解 HTML、CSS、JavaScript 和 Chrome 開發人員工具。
- React 知識
- (進階) 如果您想要獲得最佳的開發體驗,請下載 VS Code。您還需要 VS Code 專用的 lit-plugin 和 NPM。
2. Lit 與 React
Lit 的核心概念和功能在許多方面都與 React 相似,但 Lit 也有幾項重要差異和特色:
它很小
Lit 是極小的:與 React + ReactDOM 的 40 KB 相比,壓縮後經過 約 5 KB 壓縮和 gzip 壓縮。
速度快
比較 Lit 的範本系統 lit-html 和 React 的 VDOM 後,lit-html 的執行速度是 React 的 2 至 10% ,常見用途中則快了 50%以上。
LitElement (Lit 的元件基礎類別) 為 lit-html 增加的額外負擔很少,但在比較元件功能 (例如記憶體用量、互動和啟動時間) 時,效能比 React 高出 16 到 30%。
不需要建構作業
使用 ES 模組等新的瀏覽器功能,以及標記範本常值,不需編譯就能執行。也就是說,您可以使用指令碼標記 + 瀏覽器 + 伺服器來設定開發環境,即可開始運作。
透過 ES 模組和 Skypack 或 UNPKG 等新型 CDN,您甚至可能不需要 NPM 就能開始使用!
但如有需要,您仍然可以建構和最佳化 Lit 程式碼。近期開發人員針對原生 ES 模組的整合工作對 Lit 來說是好事,因為 Lit 只是一般 JavaScript,不需要特定架構的 CLI 或建構處理程序。
各架構通用
Lit 元件是根據一組稱為「網頁元件」的網路標準建構而成。也就是說,在 Lit 中建構的元件將適用目前與未來的架構。如果支援 HTML 元素,就支援網頁元件。
架構互通性唯一的問題,是當架構對 DOM 的支援受到限制時。React 就是其中一種架構,但它確實允許透過 Refs 逃逸,而 React 中的 Refs 並非良好的開發人員體驗。
Lit 團隊一直在開發名為 @lit-labs/react
的實驗性專案,這個專案會自動剖析 Lit 元件並產生 React 包裝函式,因此您不必使用 React。
此外,Custom Elements Everywhere 會顯示哪些架構和程式庫可與自訂元素搭配使用!
一流的 TypeScript 支援
雖然您可以使用 JavaScript 編寫所有 Lit 程式碼,但 Lit 是以 TypeScript 編寫,Lit 團隊建議開發人員一併使用 TypeScript!
Lit 團隊一直與 Lit 社群合作,使用 lit-analyzer
和 lit-plugin
維護支援 TypeScript 類型檢查與智慧 Lit 範本的專案。
瀏覽器內建開發人員工具
Lit 元件只是 DOM 中的 HTML 元素。也就是說,為了檢查元件,您不需要為瀏覽器安裝任何工具或執行程式。
只要開啟開發人員工具、選取元素,並探索其屬性或狀態即可。
此應用程式是依據伺服器端轉譯 (SSR) 設計
Lit 2 是基於 SSR 支援而打造。在撰寫本程式碼研究室時,Lit 團隊尚未以穩定形式發布 SSR 工具,但 Lit 團隊已經部署了伺服器端算繪的元件,並已在 Google 產品中部署,且也在 React 應用程式內測試 SSR。Lit 團隊期望很快就能在 GitHub 上的外部發布這些工具。
在此期間,你可以按這裡追蹤 Lit 團隊的進度。
入場費低
Lit 不需要極大量的承諾使用合約!您可以在 Lit 中建構元件,然後加入現有專案。如果不符合這些條件,您就無須一次轉換整個應用程式,因為網頁元件可搭配其他架構運作!
您是否已在 Lit 中建構整個應用程式,並想要改用其他工具?那麼,您可以將目前的 Lit 應用程式放入新架構中,並將您想要的任何內容遷移至新架構的元件。
此外,許多新型架構都支援網頁元件的輸出內容,因此通常能包含在 Lit 元素本身中。
3. 設定及探索 Playground
您可以透過兩種方式完成這個程式碼研究室:
- 您可以透過瀏覽器在線上完成這項操作
- (進階) 您可以在本機電腦上使用 VS Code
存取程式碼
在整個程式碼研究室中,您會看到 Lit 遊樂場的連結,如下所示:
Playground 是一個程式碼沙箱,可完全在瀏覽器中執行。這個程式庫可以編譯及執行 TypeScript 和 JavaScript 檔案,也可以自動解析匯入節點模組的行為,例如:
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://cdn.skypack.dev/lit';
您可以在 Lit Playground 中進行整個教學課程,使用這些查核點作為起點。如果您使用 VS Code,就可以透過這些檢查點下載任何步驟的起始程式碼,並使用這些檢查點檢查您的工作。
探索 lit playground UI
Lit Playground UI 螢幕截圖醒目顯示您將在這個程式碼研究室中使用的部分。
- 檔案選取器。請注意加號按鈕...
- 檔案編輯器。
- 程式碼預覽。
- 「重新載入」按鈕。
- 下載按鈕。
VS Code 設定 (進階)
使用這項 VS Code 設定的優點如下:
- 範本類型檢查
- 範本 intellisense 和自動完成
如果您已安裝 NPM、VS Code (搭配 lit-plugin 外掛程式),且知道如何使用該環境,只要執行下列操作,即可下載並啟動這些專案:
- 按下下載按鈕
- 將 tar 檔案的內容解壓縮至目錄
- (如果是 TS) 請設定快速 tsconfig,以便輸出 es 模組和 es2015 以上版本
- 安裝可解析裸模組指定符的開發伺服器 (Lit 團隊建議使用 @web/dev-server)
- 以下是
package.json
範例。
- 以下是
- 執行開發伺服器並開啟瀏覽器 (如果您使用的是 @web/dev-server,則可使用
npx web-dev-server --node-resolve --watch --open
)- 如果您要使用
package.json
範例,請使用npm run dev
- 如果您要使用
4. JSX 與範本
本節將說明 Lit 範本的基本概念。
JSX 和 Lit 範本
JSX 是 JavaScript 的語法擴充功能,可讓 React 使用者輕鬆在 JavaScript 程式碼中編寫範本。Lit 範本的用途類似,就是將元件的 UI 做為其狀態函式。
基本語法
在 React 中,您會轉譯類似下方的 JSX hello world:
import 'react';
import ReactDOM from 'react-dom';
const name = 'Josh Perez';
const element = (
<>
<h1>Hello, {name}</h1>
<div>How are you?</div>
</>
);
ReactDOM.render(
element,
mountNode
);
在上述範例中,有兩個元素和一個包含的「name」變數。在 Lit 中,您需要執行以下操作:
import {html, render} from 'lit';
const name = 'Josh Perez';
const element = html`
<h1>Hello, ${name}</h1>
<div>How are you?</div>`;
render(
element,
mountNode
);
請注意,Lit 範本不需要 React 片段,即可在範本中將多個元素分組。
在 Lit 中,範本會以 html
標記範本 LITeral 包裝,而 Lit 的名稱就是由此而來!
範本值
Lit 範本可接受其他稱為 TemplateResult
的 Lit 範本。例如,將 name
包在斜體 (<i>
) 標記中,並加上標記範本字面值 N.B. 請務必使用反引號字元 (`
),而非單引號字元 ('
)。
import {html, render} from 'lit';
const name = html`<i>Josh Perez</i>`;
const element = html`
<h1>Hello, ${name}</h1>
<div>How are you?</div>`;
render(
element,
mountNode
);
Lit TemplateResult
可接受陣列、字串、其他 TemplateResult
和指令。
針對運動,請嘗試將下列 React 程式碼轉換為 Lit:
const itemsToBuy = [
<li>Bananas</li>,
<li>oranges</li>,
<li>apples</li>,
<li>grapes</li>
];
const element = (
<>
<h1>Things to buy:</h1>
<ol>
{itemsToBuy}
</ol>
</>);
ReactDOM.render(
element,
mountNode
);
答案:
import {html, render} from 'lit';
const itemsToBuy = [
html`<li>Bananas</li>`,
html`<li>oranges</li>`,
html`<li>apples</li>`,
html`<li>grapes</li>`
];
const element = html`
<h1>Things to buy:</h1>
<ol>
${itemsToBuy}
</ol>`;
render(
element,
mountNode
);
傳球及設定道具
JSX 和 Lit 語法最大的其中一項差異就是資料繫結語法。舉例來說,取得具有繫結的這個 React 輸入內容:
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
<input
disabled={disabled}
className={`static-class ${myClass}`}
defaultValue={value}/>;
ReactDOM.render(
element,
mountNode
);
在上述範例中,輸入內容會定義如下:
- 將 disabled 設為已定義的變數 (在本例中為 false)
- 將類別設為
static-class
加上變數 (在本例中為"static-class my-class"
) - 設定預設值
在 Lit 中,您需要執行以下操作:
import {html, render} from 'lit';
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
<input
?disabled=${disabled}
class="static-class ${myClass}"
.value=${value}>`;
render(
element,
mountNode
);
在 Lit 範例中,我們新增了布林值繫結,用於切換 disabled
屬性。
接著,您會看到直接綁定至 class
屬性,而非 className
。除非使用 classMap
指令來切換類別的宣告式輔助程式,否則 class
屬性中可以加入多個繫結。
最後,輸入上會設定 value
屬性。與 React 不同,這不會像 React 一樣,必須遵循原生的實作和輸入行為,將輸入元素設為唯讀。
Lit 屬性繫結語法
html`<my-element ?attribute-name=${booleanVar}>`;
?
前置字串是用於切換元素屬性的繫結語法- 等同於
inputRef.toggleAttribute('attribute-name', booleanVar)
- 適用於使用
disabled
的元素,因為inputElement.hasAttribute('disabled') === true
會讓 DOM 將disabled="false"
讀取為 true
html`<my-element .property-name=${anyVar}>`;
.
前置字元是設定元素屬性的繫結語法- 等同於
inputRef.propertyName = anyVar
- 適合傳送物件、陣列或類別等複雜資料
html`<my-element attribute-name=${stringVar}>`;
- 繫結至元素的屬性
- 等同於
inputRef.setAttribute('attribute-name', stringVar)
- 適用於基本值、樣式規則選取器和 querySelector
傳遞處理常式
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element =
<input
onClick={() => console.log('click')}
onChange={e => console.log(e.target.value)} />;
ReactDOM.render(
element,
mountNode
);
在上述範例中,我們定義了一個輸入內容,執行以下操作:
- 使用者點選輸入內容時,記錄「click」這個字詞
- 記錄使用者輸入字元時的輸入值
在 Lit 中,您需要執行以下操作:
import {html, render} from 'lit';
const disabled = false;
const label = 'my label';
const myClass = 'my-class';
const value = 'my value';
const element = html`
<input
@click=${() => console.log('click')}
@input=${e => console.log(e.target.value)}>`;
render(
element,
mountNode
);
在 Lit 範例中,我們使用 @click
將監聽器新增至 click
事件。
接著,我們改為綁定 <input>
的原生 input
事件,因為原生 change
事件只會在 blur
上觸發 (React 會對這些事件進行抽象)。
Lit 事件處理常式語法
html`<my-element @event-name=${() => {...}}></my-element>`;
@
前置字串是事件監聽器的繫結語法- 等同於
inputRef.addEventListener('event-name', ...)
- 使用原生 DOM 事件名稱
5. 元件和道具
在本節中,您將瞭解 Lit 類別元件和函式。後續章節會更詳細說明狀態和鉤子。
類別元件與 LitElement
Lit 等同於 React 類別元件的 LitElement,而 Lit 的「反應式屬性」概念是 React 的 props 和狀態組合。例如:
import React from 'react';
import ReactDOM from 'react-dom';
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {name: ''};
}
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
在上述範例中,React 元件具有以下特性:
- 算繪
name
- 將
name
的預設值設為空字串 (""
) - 將
name
重新指派給"Elliott"
這就是在 LitElement 中執行這項操作的方式
在 TypeScript 中:
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
@property({type: String})
name = '';
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
}
在 JavaScript 中:
import {LitElement, html} from 'lit';
class WelcomeBanner extends LitElement {
static get properties() {
return {
name: {type: String}
}
}
constructor() {
super();
this.name = '';
}
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
}
customElements.define('welcome-banner', WelcomeBanner);
然後在 HTML 檔案中:
<!-- index.html -->
<head>
<script type="module" src="./index.js"></script>
</head>
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
關於上述範例中情況的審核:
@property({type: String})
name = '';
- 定義公用回應屬性 (元件公用 API 的一部分)
- 在元件上公開屬性 (預設) 和屬性
- 定義如何將元件的屬性 (字串) 轉譯為值
static get properties() {
return {
name: {type: String}
}
}
- 這與
@property
TS 修飾符提供的函式相同,但會在 JavaScript 中以原生方式執行
render() {
return html`<h1>Hello, ${this.name}</h1>`
}
- 當任何回應屬性變更時,系統就會呼叫此方法
@customElement('welcome-banner')
class WelcomeBanner extends LitElement {
...
}
- 這會將 HTML 元素標記名稱與類別定義建立關聯
- 根據自訂元素標準,標記名稱必須包含連字號 (-)
- LitElement 中的
this
是指自訂元素的例項 (本例中的<welcome-banner>
)
customElements.define('welcome-banner', WelcomeBanner);
- 這是
@customElement
TS 修飾器的 JavaScript 等價
<head>
<script type="module" src="./index.js"></script>
</head>
- 匯入自訂元素定義
<body>
<welcome-banner name="Elliott"></welcome-banner>
</body>
- 將自訂元素新增至頁面
- 將
name
屬性設為'Elliott'
函式元件
Lit 不會使用 JSX 或預先處理器,無法 1:1 解讀函式元件。不過,只要編寫函式,即可輕鬆擷取屬性,並根據這些屬性算繪 DOM。例如:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Elliott"/>
ReactDOM.render(
element,
mountNode
);
在 Lit 中,這會是:
import {html, render} from 'lit';
function Welcome(props) {
return html`<h1>Hello, ${props.name}</h1>`;
}
render(
Welcome({name: 'Elliott'}),
document.body.querySelector('#root')
);
6. 狀態和生命週期
在本節中,您將瞭解 Lit 的狀態和生命週期。
州
Lit 的「Reactive Properties」概念結合了 React 的狀態和 props。當回應式屬性發生變更時,可以觸發元件生命週期。回應式屬性有兩種變化版本:
公開被動性質
// React
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props)
this.state = {name: 'there'}
}
componentWillReceiveProps(nextProps) {
if (this.props.name !== nextProps.name) {
this.setState({name: nextProps.name})
}
}
}
// Lit (TS)
import {LitElement} from 'lit';
import {property} from 'lit/decorators.js';
class MyEl extends LitElement {
@property() name = 'there';
}
- 由「
@property
」定義 - 類似 React 的 props 和狀態,但可變動
- 元件消費者存取及設定的公用 API
內部回應狀態
// React
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props)
this.state = {name: 'there'}
}
}
// Lit (TS)
import {LitElement} from 'lit';
import {state} from 'lit/decorators.js';
class MyEl extends LitElement {
@state() name = 'there';
}
- 由
@state
定義 - 類似 React 的狀態,但可變動
- 私人內部狀態,通常從元件或子類別中存取
生命週期
Lit 生命週期與 React 的生命週期非常類似,但仍有一些明顯差異。
constructor
// React (js)
import React from 'react';
class MyEl extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this._privateProp = 'private';
}
}
// Lit (ts)
class MyEl extends LitElement {
@property({type: Number}) counter = 0;
private _privateProp = 'private';
}
// Lit (js)
class MyEl extends LitElement {
static get properties() {
return { counter: {type: Number} }
}
constructor() {
this.counter = 0;
this._privateProp = 'private';
}
}
- Lit 等價項目也是
constructor
- 不需要將任何資料傳遞至超級呼叫
- 由下列項目叫用 (不完全包含):
document.createElement
document.innerHTML
new ComponentClass()
- 如果網頁上有未升級的代碼名稱,且定義已載入
@customElement
或customElements.define
進行註冊
- 與 React 的
constructor
的函式類似
render
// React
render() {
return <div>Hello World</div>
}
// Lit
render() {
return html`<div>Hello World</div>`;
}
- 加油也是
render
- 可傳回任何可轉譯的結果,例如
TemplateResult
或string
等。 - 與 React 類似,
render()
應為純函式 - 會顯示在任何
createRenderRoot()
傳回的節點 (預設為ShadowRoot
)
componentDidMount
componentDidMount
類似於 Lit 的 firstUpdated
和 connectedCallback
生命週期回呼的組合。
firstUpdated
import Chart from 'chart.js';
// React
componentDidMount() {
this._chart = new Chart(this.chartElRef.current, {...});
}
// Lit
firstUpdated() {
this._chart = new Chart(this.chartEl, {...});
}
- 當元件範本首次算繪至元件根目錄時,就會呼叫
- 只有在元素已連線時才會呼叫 (例如在該節點附加至 DOM 樹狀結構之前,不會透過
document.createElement('my-component')
呼叫) - 這是執行元件設定的絕佳位置,因為這類設定需要由元件算繪 DOM
- 與 React 對
firstUpdated
中反應屬性的componentDidMount
變更不同,會導致重新算繪,但瀏覽器通常會將變更批次處理至同一個影格。如果這些變更不需要存取根 DOM,通常應放入willUpdate
connectedCallback
// React
componentDidMount() {
this.window.addEventListener('resize', this.boundOnResize);
}
// Lit
connectedCallback() {
super.connectedCallback();
this.window.addEventListener('resize', this.boundOnResize);
}
- 每次自訂元素插入 DOM 樹狀結構時呼叫
- 與 React 元件不同,當自訂元素從 DOM 中分離時,不會遭到銷毀,因此可以多次「連結」
- 系統不會再次呼叫
firstUpdated
- 系統不會再次呼叫
- 有助於重新初始化 DOM,或重新附加在連線中斷時清除的事件監聽器
- 注意:
connectedCallback
可能會在firstUpdated
之前呼叫,因此在第一次呼叫時,DOM 可能無法使用
componentDidUpdate
// React
componentDidUpdate(prevProps) {
if (this.props.title !== prevProps.title) {
this._chart.setTitle(this.props.title);
}
}
// Lit (ts)
updated(prevProps: PropertyValues<this>) {
if (prevProps.has('title')) {
this._chart.setTitle(this.title);
}
}
- 等同於
updated
(使用英文「update」的過去式) - 與 React 不同的是,
updated
也會在初始轉譯時呼叫 - 與 React 的
componentDidUpdate
的函式類似
componentWillUnmount
// React
componentWillUnmount() {
this.window.removeEventListener('resize', this.boundOnResize);
}
// Lit
disconnectedCallback() {
super.disconnectedCallback();
this.window.removeEventListener('resize', this.boundOnResize);
}
- 效果相當於
disconnectedCallback
- 與 React 元件不同,當自訂元素從 DOM 中卸離時,元件不會遭到刪除
- 與
componentWillUnmount
不同,系統會在元素從樹狀結構中移除元素「之後」呼叫disconnectedCallback
- 根中的 DOM 仍會與根層級的子樹狀結構連結
- 適合用來清除事件監聽器和洩漏的參照,以便瀏覽器對元件進行垃圾收集
運動
import React from 'react';
import ReactDOM from 'react-dom';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
在上述範例中,有一個簡單的時鐘可以進行下列操作:
- 會算繪「Hello World!然後顯示時間
- 每隔幾秒就會更新時鐘
- 卸載時,會清除呼叫 tick 的間隔
首先,從元件類別宣告開始:
// Lit (TS)
// some imports here are imported in advance
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
}
// Lit (JS)
// `html` is imported in advance
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
}
customElements.define('lit-clock', LitClock);
接下來,請初始化 date
,並使用 @state
宣告它為內部回應式屬性,因為元件使用者不會直接設定 date
。
// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
@state() // declares internal reactive prop
private date = new Date(); // initialization
}
// Lit (JS)
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
static get properties() {
return {
// declares internal reactive prop
date: {state: true}
}
}
constructor() {
super();
// initialization
this.date = new Date();
}
}
customElements.define('lit-clock', LitClock);
接著,算繪範本。
// Lit (JS & TS)
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
現在,請實作 tick 方法。
tick() {
this.date = new Date();
}
接下來是 componentDidMount
的實作方式。同樣地,Lit 類比是 firstUpdated
和 connectedCallback
的組合。在這個元件中,使用 setInterval
呼叫 tick
不需要存取根目錄中的 DOM。此外,當元素從文件樹狀結構中移除時,間隔就會清除,因此如果重新連結元素,間隔就會重新開始。因此,connectedCallback
是較佳的選擇。
// Lit (TS)
@customElement('lit-clock')
class LitClock extends LitElement {
@state()
private date = new Date();
// initialize timerId for TS
private timerId = -1 as unknown as ReturnType<typeof setTimeout>;
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
...
}
// Lit (JS)
constructor() {
super();
// initialization
this.date = new Date();
this.timerId = -1; // initialize timerId for JS
}
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
最後,請清除間隔,以便在元素與文件樹狀結構中斷連線後,不執行 tick。
// Lit (TS & JS)
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
將所有內容組合起來,應該會像這樣:
// Lit (TS)
import {LitElement, html} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('lit-clock')
class LitClock extends LitElement {
@state()
private date = new Date();
private timerId = -1 as unknown as ReturnType<typeof setTimeout>;
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.date = new Date();
}
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
}
// Lit (JS)
import {LitElement, html} from 'lit';
class LitClock extends LitElement {
static get properties() {
return {
date: {state: true}
}
}
constructor() {
super();
this.date = new Date();
}
connectedCallback() {
super.connectedCallback();
this.timerId = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.date = new Date();
}
render() {
return html`
<div>
<h1>Hello, World!</h1>
<h2>It is ${this.date.toLocaleTimeString()}.</h2>
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.timerId);
}
}
customElements.define('lit-clock', LitClock);
7. 鉤子
在本節中,您將瞭解如何將 React Hook 概念轉換為 Lit。
React 掛鉤的概念
React 掛鉤可讓函式元件「掛鉤」至狀態。這麼做有幾個好處。
- 這類函式可簡化有狀態邏輯的重複使用過程
- 協助將元件分割為較小的函式
此外,以函式為基礎的元件重點解決了 React 以類別為基礎的語法中的一些問題,例如:
- 必須將
props
從constructor
傳遞至super
constructor
- 中的屬性初始化不整齊
- 這是 React 團隊在當時所述原因,但後來 ES2019 解決的原因
this
不再參照元件所造成的問題
Lit 中的 React 掛鉤概念
如「Components &Props」一節所述,Lit 無法讓您透過函式建立自訂元素,但 LitElement 可以解決 React 類別元件的大部分主要問題。例如:
// React (at the time of making hooks)
import React from 'react';
import ReactDOM from 'react-dom';
class MyEl extends React.Component {
constructor(props) {
super(props); // Leaky implementation
this.state = {count: 0};
this._chart = null; // Deemed messy
}
render() {
return (
<>
<div>Num times clicked {count}</div>
<button onClick={this.clickCallback}>click me</button>
</>
);
}
clickCallback() {
// Errors because `this` no longer refers to the component
this.setState({count: this.count + 1});
}
}
// Lit (ts)
class MyEl extends LitElement {
@property({type: Number}) count = 0; // No need for constructor to set state
private _chart = null; // Public class fields introduced to JS in 2019
render() {
return html`
<div>Num times clicked ${count}</div>
<button @click=${this.clickCallback}>click me</button>`;
}
private clickCallback() {
// No error because `this` refers to component
this.count++;
}
}
Lit 如何解決這些問題?
constructor
不接受任何引數- 所有
@event
繫結都會自動繫結至this
- 大多數情況下,
this
都是自訂元素的參照 - 類別屬性現在可以做為類別成員進行個體化。這可清除以建構函式為基礎的實作
被動控制器
Hooks 背後的主要概念在 Lit 中稱為反應控制器。回應式控制器模式可讓您共用有狀態邏輯、將元件分割成更小、更小的模組位元,以及掛鉤元素的更新生命週期。
回應式控制器是一種物件介面,可掛接到 LitElement 等控制器主機的更新生命週期。
ReactiveController
和 reactiveControllerHost
的生命週期如下:
interface ReactiveController {
hostConnected(): void;
hostUpdate(): void;
hostUpdated(): void;
hostDisconnected(): void;
}
interface ReactiveControllerHost {
addController(controller: ReactiveController): void;
removeController(controller: ReactiveController): void;
requestUpdate(): void;
readonly updateComplete: Promise<boolean>;
}
只要建構回應式控制器,並使用 addController
將其附加至主機,控制器的生命週期就會與主機的生命週期一併呼叫。舉例來說,請回想「狀態與生命週期」一節中的時鐘範例:
import React from 'react';
import ReactDOM from 'react-dom';
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
在上述範例中,有一個簡單的時鐘會執行以下操作:
- 會顯示「Hello World!」然後顯示時間
- 每隔幾秒就會更新時鐘
- 卸載時,會清除呼叫 tick 的間隔
建構元件鷹架
首先,從元件類別宣告開始,然後新增 render
函式。
// Lit (TS) - index.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
@customElement('my-element')
class MyElement extends LitElement {
render() {
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${'time to get Lit'}.</h2>
</div>
`;
}
}
// Lit (JS) - index.js
import {LitElement, html} from 'lit';
class MyElement extends LitElement {
render() {
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${'time to get Lit'}.</h2>
</div>
`;
}
}
customElements.define('my-element', MyElement);
建構控制器
現在,請切換至 clock.ts
,為 ClockController
建立類別並設定 constructor
:
// Lit (TS) - clock.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';
export class ClockController implements ReactiveController {
private readonly host: ReactiveControllerHost;
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
}
private tick() {
}
hostDisconnected() {
}
}
// Lit (JS) - clock.js
export class ClockController {
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
}
tick() {
}
hostDisconnected() {
}
}
只要會共用 ReactiveController
介面,即可建立回應式控制器,但透過可接受 ReactiveControllerHost
介面和初始化控制器所需的任何其他屬性的 constructor
類別,是 Lit 團隊偏好用於大多數基本情況的模式。
接下來,您需要將 React 生命週期回呼轉換為控制器回呼。簡單來說,
componentDidMount
- 到 LitElement 的
connectedCallback
- 連線到控制器的
hostConnected
- 到 LitElement 的
ComponentWillUnmount
- 將 LitElement 的
disconnectedCallback
- 連線到控制器的
hostDisconnected
- 將 LitElement 的
如要進一步瞭解如何將 React 生命週期轉譯為 Lit 生命週期,請參閱狀態與生命週期一節。
接著,實作 hostConnected
回呼和 tick
方法,並清除 hostDisconnected
中的間隔,如「狀態與生命週期」一節的範例所示。
// Lit (TS) - clock.ts
export class ClockController implements ReactiveController {
private readonly host: ReactiveControllerHost;
private interval = 0 as unknown as ReturnType<typeof setTimeout>;
date = new Date();
constructor(host: ReactiveControllerHost) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.interval = setInterval(() => this.tick(), 1000);
}
private tick() {
this.date = new Date();
}
hostDisconnected() {
clearInterval(this.interval);
}
}
// Lit (JS) - clock.js
export class ClockController {
interval = 0;
host;
date = new Date();
constructor(host) {
this.host = host;
host.addController(this);
}
hostConnected() {
this.interval = setInterval(() => this.tick(), 1000);
}
tick() {
this.date = new Date();
}
hostDisconnected() {
clearInterval(this.interval);
}
}
使用控制器
如要使用時鐘控制器,請匯入控制器,並在 index.ts
或 index.js
中更新元件。
// Lit (TS) - index.ts
import {LitElement, html, ReactiveController, ReactiveControllerHost} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock.js';
@customElement('my-element')
class MyElement extends LitElement {
private readonly clock = new ClockController(this); // Instantiate
render() {
// Use controller
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
</div>
`;
}
}
// Lit (JS) - index.js
import {LitElement, html} from 'lit';
import {ClockController} from './clock.js';
class MyElement extends LitElement {
clock = new ClockController(this); // Instantiate
render() {
// Use controller
return html`
<div>
<h1>Hello, world!</h1>
<h2>It is ${this.clock.date.toLocaleTimeString()}.</h2>
</div>
`;
}
}
customElements.define('my-element', MyElement);
如要使用這個控制器,您需要傳入控制器主機 (<my-element>
元件) 的參照來將控制器例項化,然後在 render
方法中使用控制器。
在控制器中觸發重新算繪
此時畫面會顯示時間,但不會更新時間。這是因為控制器每秒都會設定日期,但主機不會更新。這是因為 date
會在 ClockController
類別上變更,而非元件。也就是說,在控制器上設定 date
後,主機就必須指示主機使用 host.requestUpdate()
執行更新生命週期。
// Lit (TS & JS) - clock.ts / clock.js
private tick() {
this.date = new Date();
this.host.requestUpdate();
}
計時器現在應該會開始計時!
如要深入分析與掛鉤的常見用途比較,請參閱進階主題 - 掛鉤一節。
8. 兒童
在本節中,您將瞭解如何使用空格來管理 Lit 中的子項。
版位和子項
運算單元可讓您建立元件巢狀結構,進而形成組合。
在 React 中,子項是透過道具繼承。預設的時間間隔為 props.children
,render
函式會定義預設時間間隔的位置。例如:
const MyArticle = (props) => {
return <article>{props.children}</article>;
};
請注意,props.children
是 React 元件,而非 HTML 元素。
在 Lit 中,子項會在轉譯函式中使用插槽元素進行組合。請注意,子項的繼承方式與 React 不同。在 Lit 中,子項是附加至插槽的 HTMLElement。這個附件稱為「投影」。
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<slot></slot>
</article>
`;
}
}
多個插槽
在 React 中,新增多個運算單元與繼承更多屬性基本上相同。
const MyArticle = (props) => {
return (
<article>
<header>
{props.headerChildren}
</header>
<section>
{props.sectionChildren}
</section>
</article>
);
};
同樣地,新增更多 <slot>
元素會在 Lit 中建立更多版位。多個版位以 name
屬性定義:<slot name="slot-name">
。這樣一來,子項就能宣告要指派哪個時段。
@customElement("my-article")
export class MyArticle extends LitElement {
render() {
return html`
<article>
<header>
<slot name="headerChildren"></slot>
</header>
<section>
<slot name="sectionChildren"></slot>
</section>
</article>
`;
}
}
預設版位內容
如果沒有節點投射到該時段,時段就會顯示子樹狀結構。當節點投射到一個版位時,該版位不會顯示其子樹,而是顯示投射的節點。
@customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`
<section>
<div>
<slot name="slotWithDefault">
<p>
This message will not be rendered when children are attached to this slot!
<p>
</slot>
</div>
</section>
`;
}
}
將子項指派給運算單元
在 React 中,子項會透過元件的屬性指派給版位。在以下範例中,React 元素會傳遞至 headerChildren
和 sectionChildren
屬性。
const MyNewsArticle = () => {
return (
<MyArticle
headerChildren={<h3>Extry, Extry! Read all about it!</h3>}
sectionChildren={<p>Children are props in React!</p>}
/>
);
};
在 Lit 中,子項會使用 slot
屬性指派至空格。
@customElement("my-news-article")
export class MyNewsArticle extends LitElement {
render() {
return html`
<my-article>
<h3 slot="headerChildren">
Extry, Extry! Read all about it!
</h3>
<p slot="sectionChildren">
Children are composed with slots in Lit!
</p>
</my-article>
`;
}
}
如果沒有預設的插槽 (例如 <slot>
),且沒有任何插槽具有與自訂元素子項 (例如 <div slot="foo">
) 的 slot
屬性相符的 name
屬性 (例如 <slot name="foo">
),則該節點不會投放,也不會顯示。
9. 參照
開發人員有時可能需要存取 HTMLElement 的 API。
在本節中,您將瞭解如何在 Lit 取得元素參照。
React 參照
React 元件會轉譯為一系列函式呼叫,這些函式會在呼叫時建立虛擬 DOM。這個虛擬 DOM 是由 ReactDOM 解譯,會轉譯 HTMLElements。
在 React 中,Refs 是記憶體中的空間,可包含產生的 HTMLElement。
const RefsExample = (props) => {
const inputRef = React.useRef(null);
const onButtonClick = React.useCallback(() => {
inputRef.current?.focus();
}, [inputRef]);
return (
<div>
<input type={"text"} ref={inputRef} />
<br />
<button onClick={onButtonClick}>
Click to focus on the input above!
</button>
</div>
);
};
在上述範例中,React 元件會執行以下操作:
- 算繪空白文字輸入欄位和含文字的按鈕
- 在按下按鈕時將焦點放在輸入內容
初始轉譯完成後,React 會透過 ref
屬性,將 inputRef.current
設為產生的 HTMLInputElement
。
使用 @query
點亮「參考資料」
Lit 位於瀏覽器附近,可以為原生瀏覽器功能提供非常薄的抽象化機制。
相當於 Lit 中的 refs
的 React 是由 @query
和 @queryAll
裝飾器傳回的 HTMLElement。
@customElement("my-element")
export class MyElement extends LitElement {
@query('input') // Define the query
inputEl!: HTMLInputElement; // Declare the prop
// Declare the click event listener
onButtonClick() {
// Use the query to focus
this.inputEl.focus();
}
render() {
return html`
<input type="text">
<br />
<!-- Bind the click listener -->
<button @click=${this.onButtonClick}>
Click to focus on the input above!
</button>
`;
}
}
在上述範例中,Lit 元件會執行以下操作:
- 使用
@query
修飾符 (為HTMLInputElement
建立 getter) 在MyElement
上定義屬性。 - 宣告並附加名為
onButtonClick
的點擊事件回呼。 - 將焦點移至按鈕點選時的輸入動作
在 JavaScript 中,@query
和 @queryAll
修飾符會分別執行 querySelector
和 querySelectorAll
。這是 @query('input') inputEl!: HTMLInputElement;
的 JavaScript 等價
get inputEl() {
return this.renderRoot.querySelector('input');
}
在 Lit 元件將 render
方法的範本提交至 my-element
的根目錄後,@query
修飾器現在會允許 inputEl
傳回在轉譯根目錄中找到的第一個 input
元素。如果 @query
找不到指定元素,則會傳回 null
。
如果轉譯根目錄中有多個 input
元素,@queryAll
會傳回節點清單。
10. 仲裁狀態
在本節中,您將瞭解如何在 Lit 中協調元件之間的狀態。
可重複使用的元件
React 模仿功能性算繪管道,由上而下的資料流。家長可透過道具提供兒童國籍。孩子會透過 props 中的回呼與父母溝通。
const CounterButton = (props) => {
const label = props.step < 0
? `- ${-1 * props.step}`
: `+ ${props.step}`;
return (
<button
onClick={() =>
props.addToCounter(props.step)}>{label}</button>
);
};
在上述範例中,React 元件會執行以下操作:
- 根據
props.step
值建立標籤。 - 顯示具有「+step」或「-step」標籤的按鈕
- 在點擊時,以
props.step
做為引數呼叫props.addToCounter
,藉此更新父項元件
雖然可以在 Lit 中傳遞回呼,但傳統模式有所不同。上例中的 React 元件可寫成下例中的 Lit 元件:
@customElement('counter-button')
export class CounterButton extends LitElement {
@property({type: Number}) step: number = 0;
onClick() {
const event = new CustomEvent('update-counter', {
bubbles: true,
detail: {
step: this.step,
}
});
this.dispatchEvent(event);
}
render() {
const label = this.step < 0
? `- ${-1 * this.step}` // "- 1"
: `+ ${this.step}`; // "+ 1"
return html`
<button @click=${this.onClick}>${label}</button>
`;
}
}
在上例中,Lit 元件會執行以下操作:
- 建立回應式屬性「
step
」 - 分派名為
update-counter
的自訂事件,發生點擊時含有元素的step
值
瀏覽器事件會從子項元素向上傳遞至父項元素。事件可讓孩子廣播互動事件和狀態變更。React 基本上會以相反方向傳遞狀態,因此很少看到 React 元件以與 Lit 元件相同的方式調度及監聽事件。
有狀態元件
在 React 中,通常會使用掛鉤來管理狀態。您可以重複使用 CounterButton
元件來建立 MyCounter
元件。請注意,addToCounter
如何傳遞至 CounterButton
的兩個例項。
const MyCounter = (props) => {
const [counterSum, setCounterSum] = React.useState(0);
const addToCounter = useCallback(
(step) => {
setCounterSum(counterSum + step);
},
[counterSum, setCounterSum]
);
return (
<div>
<h3>Σ: {counterSum}</h3>
<CounterButton
step={-1}
addToCounter={addToCounter} />
<CounterButton
step={1}
addToCounter={addToCounter} />
</div>
);
};
上述範例會執行以下動作:
- 可建立
count
狀態。 - 建立可將數字新增至
count
狀態的回呼。 CounterButton
使用addToCounter
在每次點擊時,透過step
更新count
。
也可以在 Lit 中完成類似的 MyCounter
實作。請注意,addToCounter
無法傳遞至 counter-button
。而是將回呼繫結為父項元素上 @update-counter
事件的事件監聽器。
@customElement("my-counter")
export class MyCounter extends LitElement {
@property({type: Number}) count = 0;
addToCounter(e: CustomEvent<{step: number}>) {
// Get step from detail of event or via @query
this.count += e.detail.step;
}
render() {
return html`
<div @update-counter="${this.addToCounter}">
<h3>Σ ${this.count}</h3>
<counter-button step="-1"></counter-button>
<counter-button step="1"></counter-button>
</div>
`;
}
}
上述範例會執行以下操作:
- 建立名為
count
的回應式屬性,在值變更時更新元件 - 將
addToCounter
回呼繫結至@update-counter
事件監聽器 - 透過新增
update-counter
事件detail.step
中的值,更新count
- 透過
step
屬性設定counter-button
的step
值
在 Lit 中使用回應式屬性,是更常見的做法,可用於將變更從父項廣播至子項。同樣地,建議您使用瀏覽器的事件系統,由下往上找出詳細資料。
這個方法遵循最佳做法,並遵循 Lit 為網頁元件提供跨平台支援的目標。
11. 樣式
本節將說明 Lit 的樣式設定。
樣式
Lit 提供多種元素樣式設定方式,以及內建解決方案。
內嵌樣式
Lit 支援內嵌樣式,以及繫結至這些樣式。
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
@customElement('my-element')
class MyElement extends LitElement {
render() {
return html`
<div>
<h1 style="color:orange;">This text is orange</h1>
<h1 style="color:rebeccapurple;">This text is rebeccapurple</h1>
</div>
`;
}
}
在上述範例中,有 2 個標題,每個標題都有內嵌樣式。
現在匯入並將 border-color.js
的邊框繫結至橘色文字:
...
import borderColor from './border-color.js';
...
html`
...
<h1 style="color:orange;${borderColor}">This text is orange</h1>
...`
因為每次都要計算樣式字串可能會造成一些困擾,所以 Lit 提供了指令協助您完成這項工作。
styleMap
styleMap
「指令」directive可讓您更輕鬆地使用 JavaScript 設定內嵌樣式。例如:
import {LitElement, html, css} from 'lit';
import {customElement, property} from 'lit/decorators.js';
import {styleMap} from 'lit/directives/style-map.js';
@customElement('my-element')
class MyElement extends LitElement {
@property({type: String})
color = '#000'
render() {
// Define the styleMap
const headerStyle = styleMap({
'border-color': this.color,
});
return html`
<div>
<h1
style="border-style:solid;
<!-- Use the styleMap -->
border-width:2px;${headerStyle}">
This div has a border color of ${this.color}
</h1>
<input
type="color"
@input=${e => (this.color = e.target.value)}
value="#000">
</div>
`;
}
}
上述範例會執行以下操作:
- 顯示帶有邊框和顏色挑選器的
h1
- 將
border-color
變更為顏色挑選器的值
此外,還有 styleMap
,可用於設定 h1
的樣式。styleMap
遵循類似 React 的 style
屬性繫結語法的語法。
CSSResult
建議您使用 css
標記範本字面值,為元件設定樣式。
import {LitElement, html, css} from 'lit';
import {customElement} from 'lit/decorators.js';
const ORANGE = css`orange`;
@customElement('my-element')
class MyElement extends LitElement {
static styles = [
css`
#orange {
color: ${ORANGE};
}
#purple {
color: rebeccapurple;
}
`
];
render() {
return html`
<div>
<h1 id="orange">This text is orange</h1>
<h1 id="purple">This text is rebeccapurple</h1>
</div>
`;
}
}
上方範例會執行以下操作:
- 宣告含有繫結的 CSS 標記範本字面值
- 使用 ID 設定兩個
h1
的顏色
使用 css
範本代碼的好處:
- 每個類別和每個例項各剖析一次
- 以模組可重複使用性為考量進行實作
- 可輕鬆將樣式分離至各自的檔案
- 與 CSS 自訂屬性 polyfill 相容
此外,請注意 index.html
中的 <style>
標記:
<!-- index.html -->
<style>
h1 {
color: red !important;
}
</style>
Lit 會將元件的樣式範圍限制在根目錄。也就是說,樣式不會在進入和離開時外洩。如要將樣式傳遞至元件,Lit 團隊建議使用 CSS 自訂屬性,因為這些屬性可穿透 Lit 樣式範圍。
樣式標記
您也可以在範本中直接內嵌 <style>
代碼。瀏覽器會刪除重複的樣式標記,但將樣式標記放在範本中後,系統就會依元件執行個體剖析這些標記,而不是依據類別進行剖析,這點與使用 css
標記範本的情況不同。此外,瀏覽器對 CSSResult
的去重作業速度也大幅提升。
連結標記
在範本中使用 <link rel="stylesheet">
也是樣式選項之一,但這也不建議使用,因為這可能會導致初始閃爍未設定樣式的內容 (FOUC)。
12. 進階主題 (選填)
JSX 和範本
蓋和虛擬 DOM
Lit-html 不包含傳統的虛擬 DOM,因此無法比較個別節點。而是使用效能功能內建 ES2015 的標記範本常值規格。標記範本常值為範本常值字串,附加了標記函式。
以下是範本常值的範例:
const str = 'string';
console.log(`This is a template literal ${str}`);
以下是標記範本字面值的範例:
const tag = (strings, ...values) => ({strings, values});
const f = (x) => tag`hello ${x} how are you`;
console.log(f('world')); // {strings: ["hello ", " how are you"], values: ["world"]}
console.log(f('world').strings === f(1 + 2).strings); // true
在上述範例中,標記是 tag
函式,而 f
函式會傳回標記範本文字常值的叫用。
Lit 的許多效能魔法來自於傳遞至標記函式的字串陣列具有相同的指標 (如第二個 console.log
所示)。瀏覽器不會在每次標記函式叫用時重新建立新的 strings
陣列,因為它使用的是相同的範本字面值 (也就是 AST 中的相同位置)。因此 Lit 的繫結、剖析和範本快取功能可充分運用這些功能,因此不會造成太多的執行階段負擔。
標記範本文字的這種內建瀏覽器行為,可為 Lit 帶來相當出色的效能優勢。大多數傳統的虛擬 DOM 會在 JavaScript 中執行大部分工作。不過,標記的範本常值在瀏覽器的 C++ 中大多會發生不同的差異。
如果想開始將 HTML 標記的範本常值與 React 或 Preact 搭配使用,則 Lit 團隊建議使用 htm
程式庫。
雖然就如同 Google 程式碼研究室網站和多個線上程式碼編輯器的情況一樣,您會注意到範本常值語法標示的標示情形並不常見。部分 IDE 和文字編輯器預設支援這些元件,例如 Atom 和 GitHub 的程式碼區塊醒目顯示工具。Lit 團隊也與社群密切合作維護專案,例如 lit-plugin
這個 VS Code 外掛程式,可為 Lit 專案新增語法醒目顯示、類型檢查和智慧功能。
Lit 和 JSX + React DOM
JSX 不會在瀏覽器中執行,而是使用前置處理器將 JSX 轉換為 JavaScript 函式呼叫 (通常是透過 Babel)。
舉例來說,Babel 會轉換以下內容:
const element = <div className="title">Hello World!</div>;
ReactDOM.render(element, mountNode);
改為:
const element = React.createElement('div', {className: 'title'}, 'Hello World!');
ReactDOM.render(element, mountNode);
React DOM 接著會擷取 React 輸出內容,然後將其轉譯為實際的 DOM,即屬性、屬性、事件監聽器和所有項目。
Lit-html 使用的標記範本常值可以在瀏覽器中運作,無需經過轉碼或預先處理器。這表示,您只要準備 HTML 檔案、ES 模組指令碼和伺服器,就能開始使用 Lit。以下是完全在瀏覽器中執行的指令碼:
<!DOCTYPE html>
<html>
<head>
<script type="module">
import {html, render} from 'https://cdn.skypack.dev/lit';
render(
html`<div>Hello World!</div>`,
document.querySelector('.root')
)
</script>
</head>
<body>
<div class="root"></div>
</body>
</html>
此外,由於 Lit 的模板系統 lit-html 並未使用傳統的虛擬 DOM,而是直接使用 DOM API,因此 Lit 2 的大小在經過最小化和壓縮後小於 5 KB,而 React (2.8 KB) + react-dom (39.4 KB) 的大小則為 40 KB。
活動
React 使用綜合事件系統。也就是說,React DOM 必須定義每個元件都會使用的每個事件,並為每個節點類型提供 camelCase 事件監聽器等價物件。因此,JSX 沒有方法可定義自訂事件的事件監聽器,開發人員必須使用 ref
,然後強制套用事件監聽器。如此一來,當整合的程式庫沒有考慮 React 時,開發人員體驗就會有不如預期的體驗,進而必須編寫 React 專屬的包裝函式。
Lit-html 會直接存取 DOM 並使用原生事件,因此新增事件監聽器的操作就像 @event-name=${eventNameListener}
一樣簡單。也就是說,在新增事件監聽器和觸發事件時,所需的執行階段剖析作業會減少。
元件和道具
回應元件和自訂元素
實際上,LitElement 會使用自訂元素來包裝元件。就元件化而言,自訂元素會產生 React 元件之間的一些取捨 (如要進一步瞭解狀態和生命週期,請參閱狀態和生命週期一節)。
自訂元素做為元件系統的優點如下:
- 瀏覽器原生功能,不需要任何工具。
- 適合
innerHTML
和document.createElement
至querySelector
的所有瀏覽器 API - 通常可跨架構使用
- 可延遲註冊
customElements.define
和「hydrate」 DOM
相較於 React 元件,自訂元素的一些缺點:
- 無法在未定義類別的情況下建立自訂元素 (因此沒有 JSX 類型的函式元件)
- 必須包含結尾標記
- 注意:儘管開發人員方便瀏覽器供應商傾向於後悔採用自閉標記規格,但新規格通常不會納入自閉標記
- 為 DOM 樹狀結構引加入額外節點,可能導致版面配置問題
- 必須透過 JavaScript 註冊
Lit 採用自訂元素而非自訂元素系統,是因為自訂元素已內建於瀏覽器,而 Lit 團隊認為跨架構的好處勝過元件抽象層提供的好處。事實上,Lit 團隊在 lit-ssr 空間中所做的努力,已克服 JavaScript 註冊的主要問題。此外,部分公司 (例如 GitHub) 會使用自訂元素延遲註冊功能,藉由選用的功能逐步強化網頁。
將資料傳遞至自訂元素
自訂元素的常見誤解是,資料只能以字串形式傳入。這種誤解的起因可能是元素屬性只能寫成字串。雖然 Lit 會將字串屬性投放至其定義的型別,但自訂元素也可以接受複雜資料作為屬性。
以下列 LitElement 定義為例:
// data-test.ts
import {LitElement, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('data-test')
class DataTest extends LitElement {
@property({type: Number})
num = 0;
@property({attribute: false})
data = {a: 0, b: null, c: [html`<div>hello</div>`, html`<div>world</div>`]}
render() {
return html`
<div>num + 1 = ${this.num + 1}</div>
<div>data.a = ${this.data.a}</div>
<div>data.b = ${this.data.b}</div>
<div>data.c = ${this.data.c}</div>`;
}
}
定義了原始回應式屬性 num
,會將屬性的字串值轉換為 number
,然後導入複雜的資料結構,並導入 attribute:false
來停用 Lit 的屬性處理功能。
以下是將資料傳遞至此自訂元素的方法:
<head>
<script type="module">
import './data-test.js'; // loads element definition
import {html} from './data-test.js';
const el = document.querySelector('data-test');
el.data = {
a: 5,
b: null,
c: [html`<div>foo</div>`,html`<div>bar</div>`]
};
</script>
</head>
<body>
<data-test num="5"></data-test>
</body>
狀態和生命週期
其他回應生命週期回呼
static getDerivedStateFromProps
Lit 中沒有對應的項目,因為 props 和 state 都是相同的類別屬性
shouldComponentUpdate
- 加值等於
shouldUpdate
- 在首次算繪時呼叫與 React 不同
- 與 React 的
shouldComponentUpdate
的函式類似
getSnapshotBeforeUpdate
在 Lit 中,getSnapshotBeforeUpdate
與 update
和 willUpdate
類似
willUpdate
- 在
update
之前呼叫 - 與
getSnapshotBeforeUpdate
不同,willUpdate
是在render
之前呼叫 - 在
willUpdate
中對回應式屬性所做的變更不會重新觸發更新週期 - 適合用來計算依附其他屬性的屬性值,且在更新程序其餘部分使用時的最佳位置
- 系統會在 SSR 伺服器上呼叫這個方法,因此不建議在此處存取 DOM
update
- 在
willUpdate
之後呼叫 - 與
getSnapshotBeforeUpdate
不同,update
是在render
之前呼叫 - 在
update
中對回應式屬性所做的變更不會重新觸發更新週期 (如果在呼叫super.update
「之前」進行變更) - 建議在算繪的輸出內容對 DOM 之前,從元件周圍的 DOM 擷取資訊
- 這個方法不會在 SSR 中的伺服器上呼叫
其他 Lit 生命週期回呼
在前一個章節中,我們沒有提到幾個生命週期回呼,因為 React 中沒有相應的回呼。這 3 個子類型如下:
attributeChangedCallback
當元素的其中一個 observedAttributes
變更時,系統就會叫用此方法。observedAttributes
和 attributeChangedCallback
都是自訂元素規格的一部分,由 Lit 負責實作,以便為 Lit 元素提供屬性 API。
adoptedCallback
當元件移至新文件時,系統會叫用此方法,例如從 HTMLTemplateElement
的 documentFragment
移至主要 document
。這個回呼也是自訂元素規格說明的一部分,且應僅用於元件變更文件的進階用途。
其他生命週期方法和屬性
這些方法和屬性是可呼叫、覆寫或等候的類別成員,可用於操控生命週期程序。
updateComplete
當元素完成更新和轉譯生命週期為非同步時,這是 Promise
即可解析。範例:
async nextButtonClicked() {
this.step++;
// Wait for the next "step" state to render
await this.updateComplete;
this.dispatchEvent(new Event('step-rendered'));
}
getUpdateComplete
這是在 updateComplete
解析時,應覆寫自訂的方法。當元件算繪子元件,且兩者的算繪週期必須同步時,就會發生這種情況,例如:
class MyElement extends LitElement {
...
async getUpdateComplete() {
await super.getUpdateComplete();
await this.myChild.updateComplete;
}
}
performUpdate
這個方法會呼叫更新生命週期回呼。除非是同步更新或自訂排程的罕見情況,否則通常不需要使用此方法。
hasUpdated
如果元件至少更新過一次,這個屬性就是 true
。
isConnected
這是自訂元素規格的一部分,如果元素目前已附加至主要文件樹狀結構,這個屬性就會是 true
。
Lit 更新生命週期視覺化
更新生命週期分為 3 個部分:
- 更新前
- 更新
- 更新後
更新前
requestUpdate
後,系統會等待預定的更新。
更新
更新後
Hooks
使用 Hooks 的原因
我們在 React 導入了掛鉤,適用於需要狀態的簡單函式元件用途。在許多簡單的情況下,含有鉤子的函式元件通常比類別元件更簡單,也更易於閱讀。不過,在導入非同步狀態更新以及在掛鉤或效果之間傳遞資料時,掛鉤模式往往無法滿足,而回應式控制器這類以類別為基礎的解決方案往往會顯得突發。
API 要求掛鉤和控制項
一般來說,您會編寫從 API 要求資料的掛鉤。例如,這個 React 函式元件會執行以下作業:
index.tsx
- 顯示文字
- 算繪
useAPI
的回應- 使用者 ID + 使用者名稱
- 錯誤訊息
- 觸及使用者 11 時為 404 (根據設計)
- 如果 API 擷取作業中止,則中止錯誤
- 載入訊息
- 算繪動作按鈕
- 下一位使用者:擷取 API 供下一位使用者使用
- 取消:會中斷 API 擷取作業並顯示錯誤
useApi.tsx
- 定義
useApi
自訂掛鉤 - 會從 API 以非同步方式擷取使用者物件
- 發出:
- 使用者名稱
- 擷取作業是否正在載入
- 所有錯誤訊息
- 用於中止擷取作業的回呼
- 如果卸載,系統會取消進行中的擷取作業
- 定義
以下是 Lit + Reactive Controller 實作。
重點整理:
- 回應式控制器和自訂掛鉤十分相似
- 在回呼和效果之間傳遞無法轉譯的資料
- React 會使用
useRef
在useEffect
和useCallback
之間傳遞資料 - Lit 使用私人類別屬性
- React 基本上就是模仿私人類別屬性的行為
- React 會使用
此外,如果您很喜歡 React 函式元件語法搭配掛鉤,但與 Lit 相同的無建構環境,則 Lit 團隊非常建議使用「Hunted」程式庫。
子項
預設時段
如果 HTML 元素未指定 slot
屬性,系統會將該元素指派至預設的未命名版位。在以下範例中,MyApp
會將一個段落放入已命名的空格。另一段落則會預設為未命名的插槽」
@customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`
<section>
<div>
<slot></slot>
</div>
<div>
<slot name="custom-slot"></slot>
</div>
</section>
`;
}
}
@customElement("my-app")
export class MyApp extends LitElement {
render() {
return html`
<my-element>
<p slot="custom-slot">
This paragraph will be placed in the custom-slot!
</p>
<p>
This paragraph will be placed in the unnamed default slot!
</p>
</my-element>
`;
}
}
運算單元更新
當空白區後代結構變更時,系統會觸發 slotchange
事件。Lit 元件可將事件監聽器繫結至 slotchange
事件。在以下範例中,shadowRoot
中找到的第一個運算單元會在 slotchange
將 assignedNodes 記錄到控制台。
@customElement("my-element")
export class MyElement extends LitElement {
onSlotChange(e: Event) {
const slot = this.shadowRoot.querySelector('slot');
console.log(slot.assignedNodes({flatten: true}));
}
render() {
return html`
<section>
<div>
<slot @slotchange="{this.onSlotChange}"></slot>
</div>
</section>
`;
}
}
參考
參考資料產生
在 render
函式呼叫後,Lit 和 React 都會公開對 HTMLElement 的參照。但是,有必要檢閱 React 和 Lit 如何組成之後透過 Lit @query
裝飾器或 React 參照的 DOM。
React 是功能管道,會建立 React 元件,而非 HTMLElement。由於 Ref 是在 HTMLElement 轉譯前宣告,因此會分配記憶體空間。因此,系統會將 null
顯示為 Ref 的初始值,因為實際的 DOM 元素尚未建立 (或算繪) (即 useRef(null)
)。
ReactDOM 將 React 元件轉換為 HTMLElement 後,會在 ReactComponent 中尋找名為 ref
的屬性。如果可用,ReactDOM 會將 HTMLElement 的參照置於 ref.current
。
LitElement 會使用 lit-html 的 html
範本標記函式,在幕後組合範本元素。LitElement 會在轉譯後,將範本內容蓋上自訂元素的 shadow DOM。shadow DOM 是受影根封裝的受限 DOM 樹狀結構。@query
修飾子會為屬性建立 getter,該 getter 基本上會在已指定範圍的根目錄上執行 this.shadowRoot.querySelector
。
查詢多個元素
在以下範例中,@queryAll
修飾符會將陰影根目錄中的兩個段落做為 NodeList
傳回。
@customElement("my-element")
export class MyElement extends LitElement {
@queryAll('p')
paragraphs!: NodeList;
render() {
return html`
<p>Hello, world!</p>
<p>How are you?</p>
`;
}
}
基本上,@queryAll
會建立 paragraphs
的 getter,以傳回 this.shadowRoot.querySelectorAll()
的結果。在 JavaScript 中,您可以宣告 getter 來執行相同的用途:
get paragraphs() {
return this.renderRoot.querySelectorAll('p');
}
查詢變更元素
如果節點可以根據其他元素屬性的狀態變更,@queryAsync
修飾符更適合用來處理這類節點。
在以下範例中,@queryAsync
會尋找第一個段落元素。不過,只有在 renderParagraph
隨機產生奇數時,才會顯示段落元素。@queryAsync
指令會傳回一個承諾,在第一個段落可用時會解析。
@customElement("my-dissappearing-paragraph")
export class MyDisapppearingParagraph extends LitElement {
@queryAsync('p')
paragraph!: Promise<HTMLElement>;
renderParagraph() {
const randomNumber = Math.floor(Math.random() * 10)
if (randomNumber % 2 === 0) {
return "";
}
return html`<p>This checkbox is checked!`
}
render() {
return html`
${this.renderParagraph()}
`;
}
}
仲裁狀態
在 React 中,慣例是使用回呼,因為狀態是由 React 本身調解。React 最好不要依賴元素提供的狀態。DOM 只是轉譯程序的效果。
外部狀態
除了 Lit 以外,您還可以使用 Redux、MobX 或任何其他狀態管理程式庫。
Lit 元件是在瀏覽器範圍內建立。因此,Lint 可以使用瀏覽器範圍內的任何程式庫。許多令人驚豔的程式庫已建構完成,可在 Lit 中使用現有的狀態管理系統。
以下是 Vaadin 的系列,說明如何在 Lit 元件中使用 Redux。
請參考 Adobe 的 lit-mobx 網站,瞭解大型網站如何在 Lit 中運用 MobX。
您也可以參閱「Apollo 元素」,瞭解開發人員如何在網頁元件中加入 GraphQL。
Lit 可與原生瀏覽器功能搭配運作,瀏覽器範圍內的大多數狀態管理解決方案都能在 Lit 元件中使用。
樣式
Shadow DOM
為了在自訂元素中原生封裝樣式和 DOM,Lit 會使用 Shadow DOM。陰影根會產生與主要文件樹狀結構分開的陰影樹狀結構。這表示大部分樣式都只適用於這份文件。部分樣式 (例如顏色) 和其他字型相關樣式確實會漏光。
Shadow DOM 也為 CSS 規格引進了新概念和選取器:
:host, :host(:hover), :host([hover]) {
/* Styles the element in which the shadow root is attached to */
}
slot[name="title"]::slotted(*), slot::slotted(:hover), slot::slotted([hover]) {
/*
* Styles the elements projected into a slot element. NOTE: the spec only allows
* styling the direcly slotted elements. Children of those elements are not stylable.
*/
}
分享樣式
透過 css
範本標記,Lit 可讓您輕鬆以 CSSTemplateResults
的形式,在元件之間共用樣式。例如:
// typography.ts
export const body1 = css`
.body1 {
...
}
`;
// my-el.ts
import {body1} from './typography.ts';
@customElement('my-el')
class MyEl Extends {
static get styles = [
body1,
css`/* local styles come after so they will override bod1 */`
]
render() {
return html`<div class="body1">...</div>`
}
}
主題設定
使用陰影根源是傳統主題設定上的挑戰,但通常是由上而下。對於使用 Shadow DOM 的網頁元件設定主題設定,傳統的做法是透過 CSS 自訂屬性顯示樣式 API。例如,Material Design 使用的模式如下:
.mdc-textfield-outline {
border-color: var(--mdc-theme-primary, /* default value */ #...);
}
.mdc-textfield--input {
caret-color: var(--mdc-theme-primary, #...);
}
接著,使用者可以套用自訂屬性值來變更網站的主題:
html {
--mdc-theme-primary: #F00;
}
html[dark] {
--mdc-theme-primary: #F88;
}
如果「由上而下」的主題設定不可公開,且您無法公開樣式,那麼一律可以透過覆寫 createRenderRoot
來停用 Shadow DOM,以傳回 this
,然後將元件範本轉譯至自訂元素本身,而非附加至自訂元素的陰影根層級。您將無法使用:樣式封裝、DOM 封裝和插槽。
正式版
IE 11
如果您需要支援 IE 11 等舊版瀏覽器,則必須載入部分 polyfill;此作業約為 33 KB。詳情請參閱這裡。
條件式組合
Lit 團隊建議提供兩種不同的套裝組合,一個用於 IE 11,另一個適用於新型瀏覽器。這麼做有幾個好處:
- 服務 ES 6 服務速度較快,能為大多數客戶提供服務
- 經過轉譯的 ES 5 會大幅增加套件大小
- 條件式套裝組合可提供兩種優勢
- 支援 IE 11
- 新型瀏覽器運作速度不會變慢
如要進一步瞭解如何建立有條件提供的套裝組合,請參閱說明文件網站。