LitはWebのUIコンポーネント開発向けライブラリで、React.jsやVue.jsなどのオルタナティブです。
Web Components標準に準拠していることと、VirtualDOMによらないレンダリングが主な特徴です。
- 開発者独自のHTMLタグを生成する
- CustomElementsにより、開発したコンポーネントを直接HTMLから呼び出せる
- 外界のCSSと隔離できる
- ShadowDOMにより、コンポーネントのスコープが独立する。LightDOMも選択可能
- ビルド不要の書き方もある
- Web標準重視の設計により、ビルドツールへの依存性が低い
- 効率的な記述にはデコレータを利用した方が良い。2022年に主要仕様が TC39プローザルのステージ3になり、ブラウザ実装待ちまで来ている。
class
を用いてコンポーネントを実装する- デコレータの役割が大きいことと、ライフサイクルフックを明示的に定義できる
WebComponentsはブラウザ導入から数年が経過しており、Safari12などの古いブラウザでも動作します。
非サポートブラウザは、
Legacy browse breakdownのload polyfilsの欄が参考になります。Edge LegacyとInternet Explorerでは動作しないことが分かります。
Litのコード概観
Litのコードを既述の公式サイトから引用し、コメントを追記します。なお、Litの著作者はGoogle LLC.です。
// テンプレートタグと継承元クラス
import {html, css, LitElement} from 'lit';
// デコレータ
import {customElement, property} from 'lit/decorators.js';
// simple-greetingというカスタムエレメント(HTMLタグ)を生成
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
// CSS向けのテンプレートリテラル
static styles = css`p { color: blue }`;
// <some-greeting name="Somebody">に対応するプロパティ定義
@property()
name = 'Somebody';
render() {
// HTML向けのテンプレートリテラル
return html`<p>Hello, ${this.name}!</p>`;
}
}
コンポーネントのインターフェースは@customElement
や@property
などのデコレータに簡潔に記述できます。
Decoratorsにリファレンスがあります。
WebComponents標準に沿って<sample-greeting name="..."></sample-greeting>
というカスタムエレメントが生成されます。
テンプレート部分は、html``
やcss``
といったタグ付きの
テンプレートリテラルで記述します。
書き方を含めて標準のテンプレートリテラルと同じです。テンプレートを文字列として扱えるため、コードはJSX/TSXではなくJS/TSになります。
イベントハンドラなど数種類の拡張記法については、 Template Expressionsを参照してください。
また、htmlテンプレートを含むコードのフォーマットは、 Prettierを利用するとHTMLの階層も適切にインデントできます。
コンポーネント呼び出し
Litには他のコンポーネントを呼び出す記述例があまり見当たらないのですが、@customElement()
などのデコレータに定義したタグを記述すると呼び出せます。
import {html, css, LitElement} from 'lit';
import {customElement, state} from 'lit/decorators.js';
@customElement('simple-caller')
class SimpleCaller extends LitElement {
@state()
_somebody = 'Nobody';
render() {
return html`
<div>
<some-greeting name=${this._somebody}></some-greeting>
</div>
`;
}
}
なお、カスタムエレメントはブラウザに登録されるため、プロジェクトのどこかでインポートしていれば、個別に呼び出すファイルのimport
は不要です。クラスを継承しないのであればexport
も不要です。
詳細は、各種WebComponents標準の情報を参照してください。
このように、Litではコンポーネントをカスタムエレメントに登録して呼び出すという構成になります。
プロパティ指定
<some-greeting name=${this._somebody}></some-greeting>
のname属性のように、Litのテンプレートでコンポーネントを呼び出す際に、プロパティ類を指定できます。基本的にはReactなどと似ていますが、渡す値の種類ごとに多少書き分ける必要があります。
- HTML属性
name
などのようにHTML属性と同じ(記号なしの)記述はHTML属性として扱われます。文字や数値はこの書き方で問題ありません
- Boolean属性
?disabled=${false}
のように、Booleanを渡す属性には?
が必要です。disabled=${false}
と素朴に書くと"false"という文字列が渡り、意図に反してtruthyの挙動をとります?disabled=${false}
のケースでは、コンポーネントには値を渡さない挙動になります。コンポーネントプロパティのデフォルト値がtrue
の場合、falseがセットされない結果としてtrueになります。直観に反しているため、注意が必要です
- プロパティ
.params=${[0, 1, 2]}
のように、オブジェクトや配列といったコレクションを渡す際には.
が必要です。
Expressionsに解説があります。
Boolean、オブジェクト、配列はHTML属性として渡すと分かりづらいトラブルの要因となります。
たとえば、params=${[]}
のように空配列をHTML属性に指定すると、配列ではなく空文字列が渡ります。コンポーネントでparams.map()
のような処理を実装すると、map()
メソッドがないという想定外のエラーが起きます。
このようにHTML属性を使うと文字列変換される挙動は、HTML標準に合わせた仕様です。
ShadowDOM / LightDOM
LitはWebComponents標準の一部であるShadowDOMがデフォルトで有効になっています。
ShadowDOMを使うとCSSを含めてコンポーネント実装がカプセル化されます。従来型のLightDOMは、CSSが環境の汚染を受けます。
ただしケースにより、DOMスコープが隔離される挙動は想像と異なる場合があります。
以下のように
createRenderRootでthis
を返すとLightDOMになります。
export class LightDom extends LitElement {
createRenderRoot() {
return this;
}
}
イベントリスナー登録
イベントリスナーは、connectedCallback()
内でaddEventListener()
により追加する方法があります。
ただし、コールバックにコンポーネントのメソッドを指定する場合、this
がconnectedCallback()
の時点のthis
を指す挙動となり、分かりづらいトラブルの要因になることがあります。
render()
内の@click
などに指定する場合にはthis
は最新の状態になります。基本的には@
に定義した方が素朴な想定と挙動が一致するでしょう。
CSS
CSSはcss
リテラルを用いて基礎的なCSSと同様に記述できます。
LitElementクラス個別のスタイルはstatic styles
プロパティに定義できますが、メディアクエリは動作しません。
メディアクエリを使いたい場合には、render()
が返すhtml
リテラルの中にHTMLの<style>
タグを記述することで想定どおりに動作します。
ビルド
Litコードのビルドは、デコレータを含めてブラウザ間の非互換を吸収する目的のトランスパイルと言えます。
基本的にJavascriptの未来標準やTypescriptに準拠しているので、メジャーなTypescriptコンパイラで変換する手法が有望です。
たとえば、esbuildでは.ts
ファイルに保存してTypescriptと認識させることにより標準機能で変換できています。
Typescriptには必ずしも型を定義する必要はありません。
バンドルについても同様で、ESMをそのまま配布することも可能ですが、必要に応じて1ファイルにまとめても構いません。この点は一般的なブラウザスクリプトと同じであり、Lit特有の制約はありません。
ルーターに課題
Litそのものの問題ではないものの、WebComponents向けのクライアントサイドルーターの選択肢が乏しい点は課題になるでしょう。
ReactやVueには各コンポーネントに合わせたルーターがありますが、WebComponents向けのプロダクトはいずれも2020年頃に開発が途絶えています。
@vaadin/routerが比較的使われているようです。基礎的な機能に特に問題はありません。
ただしコード上でリダイレクトするRouter.go()
にはやや難があります。挙動にオプションがなくヒストリを置換できないのと、即座にリダイレクトしない挙動は様々なシーンでネックになり得ます。
とくにRouter.go()
後に処理中断しない挙動は、APIエラー時などのリダイレクトが後続の正常フローに打ち消されるためコード構成に注意を要します。
ReactとLitの比較と移行
ReactとLitの実用上の主な違いには以下のような点があります。
- LitのクラスベースとReactの関数ベースの違い
- LitのクラスはReactで以前主流だったクラスコンポーネントとほぼ同じだが、デコレータにより簡潔に書ける
- Reactの関数コンポーネントはHooksの第2引数で再レンダリングを定義するが、細かい制御が不能。Litは ライフサイクルフックに指定できる
- ReactHooksの第2引数によるバグには、バインドされた値が最新でないという派生形もある
- VirtualDOM由来のパフォーマンス構造
- ReactはHooksの難点により意図しない更新リクエストが多く、VirtualDOMの差分検出に負荷がかかりやすい
- VirtualDOMにはブラウザネイティブの支援がなく根本的な不利がある
- LitはCSSの扱い方を設計している
css
リテラルのインターフェース- ShadowDOMによるScopedCSSの挙動
JavascriptのUIライブラリは、とくに工夫をしなければシングルスレッドで動作するため、処理が輻輳すると画面がフリーズします。
WebAPIはオブジェクトや配列を扱うことが多く、コンポーネントでmap()
などの変形を経たデータを使うと、すべて描画更新リクエストになります。
Reactの関数コンポーネントは差分検出が走りやすい設計になっており、VirtualDOMにパワーを取られるとデータストア処理も遅くなります。
取り扱うデータ構造が大きくなるにつれ、Hooksの制御は機能しなくなっていきます。
React依存から脱却できない場合、典型的にはGraphQLの採用を迫られる展開になるでしょう。
Reactからの移行
LitはReactのプロジェクトに混在させられるため、移行は順次進められます。
npm install lit
でパッケージを追加- ファイルサイズが5KBしかないため、Reactプロジェクトに追加しても肥大化しない
- コンポーネントをJSXからLitに書き換え
- Reactで実現していることはLitでも特に問題なく実現できる
- JSXから呼び出し
- カスタムのHTMLタグを記述
JSXへの統合は以下のようなコードになります。
export function SampleJsx(props) {
return (
<div>
<some-greeting name={somebody}></some-greeting>
<section>This section is React</section>
</div>
)
}
カスタムエレメントはHTMLタグのバリアントであるため、Reactに限らずHTMLタグを呼べるテンプレートライブラリであれば、同じようにLitエレメントを呼び出せます。
なお、Litのコンポーネントは呼び出すコードからimport
する必要はありません。
カスタムエレメントはブラウザ上でHTMLタグになるため、ブラウザ環境に登録されたWebComponentsにアップグレードする挙動になります。
props
を描画するコンポーネントについては、単純な置き換えでLitを実装できます。Litになった部分は効率的な更新ロジックを利用でき、不要な再描画への耐性が高まります。
まとめ
じつはLitは新興フレームワークではなく、前身のPolymerから10年近くWebComponents標準のテストベッドであり続けてきました。
その過程ではHTML ImportsのようにSafariが拒否して消えた技術もあります。
ブラウザ標準が進化してきたことにより、ようやく近未来の標準技術の集大成といえる完成度になったように見えます。
大多数の機能をJSコードではなくブラウザ機能が直接担うアーキテクチャであることから、Litは軽量で堅牢なフレームワークと言えるでしょう。
より実践的なコード構成については、 Litのコード構成で解説しています。
Chuma Takahiro