程式碼研究室簡介
1. 簡介
什麼是 Lit?
Lit 是一種簡單的程式庫,可用來建構快速、輕量、可搭配任何架構,或完全沒有架構的網路元件。您可以利用 Lit 建構共用元件、應用程式、設計系統等。
課程內容
如何將多個 React 概念轉譯為 Lit,例如:
- JSX 和範本
- 元件和道具
- 州與生命週期
- Hooks
- 子項
- 參考
- 中介狀態
建構項目
在本程式碼研究室的結尾,能將 React 元件的概念轉換為 Lit 類記錄檔。
軟硬體需求
- 最新版本的 Chrome、Safari、Firefox 或 Edge。
- 瞭解 HTML、CSS、JavaScript 和 Chrome 開發人員工具。
- React 知識
- (進階) 如要享有最佳開發體驗,請下載 VS Code。此外,您還需要為 VS Code 和 NPM 安裝 lit-plugin 項目。
2. 文學與反應
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 只是一般的 JavaScript,不需要架構專屬 CLI 或建構處理。
各架構通用
Lit 的元件是以一組稱為「Web 元件」的網路標準為基礎。也就是說,在 Lit 中建構的元件將適用目前與未來的架構。如果它支援 HTML 元素,就表示支援網頁元件。
架構互通性問題的唯一問題在於架構對 DOM 的支援程度有限。React 是其中一種架構,但 Refs 允許使用 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 元素。也就是說,為了檢查元件,您不需要為瀏覽器安裝任何工具或執行程式。
只要開啟開發人員工具、選取元素,並探索其屬性或狀態即可。
Cloud Build 採用伺服器端轉譯 (SSR) 設計
Lit 2 是專為 SSR 而打造。在撰寫本程式碼研究室時,Lit 團隊尚未以穩定形式發布 SSR 工具,但 Lit 團隊已經部署了伺服器端算繪的元件,並已在 Google 產品中部署,並且也測試了 React 應用程式中的 SSR。Lit 團隊期望很快就能在 GitHub 上的外部發布這些工具。
在此期間,你可以按這裡追蹤 Lit 團隊的進度。
購買率低
Lit 不需要極大量的承諾使用合約!您可以在 Lit 中建構元件,然後加入現有專案。如果不符合這些條件,您就無須一次轉換整個應用程式,因為網頁元件可搭配其他架構運作!
您已經使用 Lit 建構了整個應用程式,但想變更其他內容嗎?然後,您可以將目前的 Lit 應用程式放入新架構中,然後將任何想要移轉到新架構元件的內容。
此外,許多新型架構都支援網頁元件的輸出內容,因此通常能包含在 Lit 元素本身中。
3. 開始設定與探索遊樂場
您可以透過以下兩種方式完成本程式碼研究室:
- 你可以透過瀏覽器在線上完成所有工作
- (進階) 您可以在本機電腦上使用 VS Code
存取程式碼
本程式碼研究室中會提供 Lit Playground 的連結,如下所示:
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,就可以透過這些檢查點下載任何步驟的起始程式碼,並使用這些檢查點檢查您的工作。
探索燈光效果的遊樂場 UI
Lit Playground UI 螢幕截圖醒目顯示您將在這個程式碼研究室中使用的部分。
- 檔案選取器。注意加號按鈕...
- 檔案編輯器。
- 程式碼預覽。
- 「重新載入」按鈕。
- 下載按鈕。
VS Code 設定 (進階)
使用這項 VS Code 設定的優點如下:
- 範本類型檢查
- 範本智慧功能和自動完成建議
如果您已安裝 NPM、VS Code (含 lit-plugin 外掛程式),並瞭解如何使用該環境,只要下載並按照下列步驟啟動這些專案即可:
- 按下「下載」按鈕
- 將 tar 檔案的內容解壓縮至目錄
- (如果是 TS) 設定快速 tsconfig 以輸出 es 模組和 es2015+
- 安裝可解析裸機模組指定碼的開發伺服器 (Lint 團隊建議採用 @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 和蓋子範本
JSX 是 JavaScript 的語法擴充功能,可讓 React 使用者輕鬆在 JavaScript 程式碼中編寫範本。Lit 範本的用途類似,就是將元件的 UI 做為其狀態函式。
基本語法
在 React 中,您會算繪 JSX Hello 的世界,如下所示:
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
);
請注意,Lint 範本不需要 React Fragment,即可將多個元素分組。
在 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
);
在上述範例中,輸入內容會定義如下:
- 設為停用已定義的變數 (在本例中為 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
做為disabled="false"
的元素,但 DOM 仍會讀取為 true,因為inputElement.hasAttribute('disabled') === 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
);
在上述範例中,輸入內容會定義如下:
- 記錄「按一下」一詞使用者點選輸入內容時
- 在使用者輸入字元時記錄輸入內容的值
在 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
事件。
接下來,而不是使用 onChange
,這是 <input>
的原生 input
事件繫結,因為原生 change
事件只會在 blur
觸發 (回應這些事件的摘要)。
Lit 事件處理常式語法
html`<my-element @event-name=${() => {...}}></my-element>`;
@
前置字串是事件監聽器的繫結語法- 等同於
inputRef.addEventListener('event-name', ...)
- 使用原生 DOM 事件名稱
5. 元件和道具
在本節中,您將瞭解 Lit 類別元件和函式。後續章節將詳細說明狀態與掛鉤。
類別元件與LitElement
Lit 相當於 React 類別元件的 Lit,並且是 Lit 的「被動屬性」概念由 React 的道具和狀態組成。例如:
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 的「被動屬性」概念結合 React 的狀態和道具。變更後,會觸發元件生命週期。回應式屬性有兩種變化版本:
公開被動性質
// 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 的道具和狀態,但可變動的
- 由元件使用者存取及設定的公用 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';
}
}
- 加油也是
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!」是」然後顯示時間
- 每隔幾秒就會更新時鐘
- 它會在卸載時清除呼叫滴答的間隔
首先,從元件類別宣告著手:
// 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() {
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
);
}
最後,請清理間隔,避免在元素與文件樹狀結構中斷連線後,執行滴答。
// 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. Hooks
本節將說明如何將 React Hook 概念轉譯成 Lit。
React hook 的概念
反應掛鉤可讓函式元件「掛鉤」並轉換成狀態這麼做有多項好處,
- 這類函式可簡化有狀態邏輯的重複使用過程
- 協助將元件分割為較小的函式
此外,重點在於以函式為基礎的元件,透過 React 的類別語法解決特定問題,例如:
- 將
props
從constructor
傳遞至super
constructor
中的屬性初始化作業- 這是 React 團隊在當時所述原因,但後來 ES2019 解決的原因
- 因
this
不再參照元件而造成的問題
《Lit》中的反應概念概念
如「元件與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
將此控制器附加至主機後,系統就會和主機呼叫控制器的生命週期。例如,請回想一下 State &Lifecycle:
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!」是」然後顯示時間
- 每隔幾秒就會更新時鐘
- 它會在卸載時清除呼叫滴答的間隔
建構元件鷹架
首先,從元件類別宣告開始,然後新增 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 生命週期,請參閱狀態與Lifecycle:
接下來,請實作 hostConnected
回呼和 tick
方法,並清除 hostDisconnected
中的間隔,如 State &生命週期」一節。
// 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
方法中使用控制器。
在控制器中觸發重新轉譯
請注意,系統會顯示時間,但不會更新時間。這是因為控制器會每秒設定日期,但主機並未更新。這是因為 ClockController
類別中的 date
有所變更,不再是元件。也就是說,在控制器上設定 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 中,子項都是附加至版位的 HTMLElements。這個附件稱為「投影」。
@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 中,系統會透過 Component 的屬性將子項指派給版位。在以下範例中,系統會將 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>
),而且版位中沒有 name
屬性 (例如 <slot name="foo">
) 與自訂元素子項的 slot
屬性 (例如 <div slot="foo">
) 相符,則系統不會投影該節點,也不會顯示。
9. 參考
開發人員有時可能需要存取 HTMLElement 的 API。
在本節中,您將瞭解如何在 Lit 取得元素參照。
回應參照
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>
`;
}
}
在上述範例中,Lint 元件執行以下操作:
- 使用
@query
修飾符在MyElement
上定義屬性 (為HTMLInputElement
建立 getter)。 - 宣告並附加名為
onButtonClick
的點擊事件回呼。 - 將焦點移至按鈕點選時的輸入動作
在 JavaScript 中,@query
和 @queryAll
修飾符會分別執行 querySelector
和 querySelectorAll
。這相當於 JavaScript 的 @query('input') inputEl!: HTMLInputElement;
get inputEl() {
return this.renderRoot.querySelector('input');
}
在 Lit 元件將 render
方法的範本提交至 my-element
的根層級後,@query
修飾符現在可讓 inputEl
傳回在轉譯根目錄中找到的第一個 input
元素。如果 @query
找不到指定元素,會傳回 null
。
如果轉譯根層級有多個 input
元素,@queryAll
會傳回節點清單。
10. 中介狀態
本節將說明如何調解 Lit 中元件之間的狀態。
可重複使用的元件
React 模仿功能性算繪管道,由上而下的資料流。家長可透過道具提供兒童國情服務。兒童透過道具中的回呼與家長溝通。
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 中傳遞回呼,但傳統模式有所不同。上例中的反應元件可以寫成 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>
`;
}
}
在上述範例中,Lint 元件將執行以下作業:
- 建立回應式屬性「
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
「指令」可讓您更輕鬆地使用 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>
蓋子會界定元件的範圍對應到根層級也就是說,樣式不會外洩。如要將樣式傳遞到元件,Lint 團隊建議使用 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 專案新增語法醒目顯示、類型檢查和智慧功能。
蓋和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,因此與 React (2.8kb) 壓縮及反應碼 (39.4kb) 相比,Lit 2 的大小不到 5 KB 壓縮,而使用 gzip 壓縮。
活動
React 使用綜合事件系統。這表示回應群必須定義每個元件會使用的每個事件,並為每個節點提供相等的駝峰式事件監聽器。因此,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 團隊在光電空間的努力克服了 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 中沒有對等的道具和狀態都是相同的類別屬性
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 中沒有與這些回呼的類似。這些因素包括:
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
掛鉤的原因
我們在 React 導入了掛鉤,適用於需要狀態的簡單函式元件用途。在許多簡單的情況下,含有掛鉤的函式元件往往比類別元件對應的項目更簡潔易讀。不過,在導入非同步狀態更新以及在掛鉤或效果之間傳遞資料時,掛鉤模式往往無法滿足,而回應式控制器這類以類別為基礎的解決方案往往會顯得突發。
API 要求掛鉤控制器
常見的做法是編寫掛鉤,要求 API 提供資料。例如,這個 React 函式元件會執行以下作業:
index.tsx
- 顯示文字
- 顯示
useAPI
的回應- 使用者 ID + 使用者名稱
- 錯誤訊息
- 觸及使用者 11 時為 404 (根據設計)
- 如果取消 API 擷取作業,系統會取消錯誤
- 載入訊息
- 顯示動作按鈕
- 下一位使用者:擷取 API 供下一位使用者使用
- 取消:停用 API 擷取並顯示錯誤
useApi.tsx
- 定義
useApi
自訂掛鉤 - 將透過非同步方式從 API 擷取使用者物件
- Emits:
- 使用者名稱
- 是否載入擷取
- 所有錯誤訊息
- 取消擷取的回呼
- 如果卸載,系統會取消進行中的擷取作業
- 定義
以下是 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>
`;
}
}
參考
產生參考檔案
呼叫 HTML Element 的 render
函式後,Lit 和 React 都會公開對 HTMLElement 的參照。但是,有必要檢閱 React 和 Lit 如何組成之後透過 Lit @query
裝飾器或 React 參照的 DOM。
React 是用於建立 React 元件而非 HTMLElements 的功能管道。由於在轉譯 HTMLElement 之前宣告 Ref,所以系統會分配記憶體中的空間。因此,系統會將 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,基本上會對限定範圍的根執行 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 元件中使用。
樣式
陰影 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.
*/
}
分享樣式
Lit 可讓您透過 css
範本標記,輕鬆在 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 支援
- 新型瀏覽器運作速度不會變慢
如要進一步瞭解如何建立有條件提供的套裝組合,請參閱說明文件網站。