1. はじめに
最終更新日: 2021 年 8 月 10 日
ウェブ コンポーネント
ウェブ コンポーネントは、ウェブページやウェブアプリで使用する新しいカスタムの再利用可能なカプセル化された HTML タグを作成できるウェブ プラットフォーム API のセットです。ウェブ コンポーネント標準に基づいて構築されたカスタム コンポーネントとウィジェットは、最新のブラウザで動作し、HTML で動作する JavaScript ライブラリやフレームワークで使用できます。
Lit とは
Lit は、任意のフレームワークで動作する高速で軽量なウェブ コンポーネントを構築するためのシンプルなライブラリです。フレームワークを使用しない場合でも動作します。共有可能なコンポーネントやアプリケーション、設計システムなどの構築に使用できます。
Lit は、プロパティ、属性、レンダリングの管理など、一般的なウェブ コンポーネントのタスクを簡素化する API を提供します。
学習内容
- ウェブ コンポーネントとは
- ウェブ コンポーネントのコンセプト
- ウェブ コンポーネントの構築方法
- lit-html と LitElement とは
- ウェブ コンポーネントの上に Lit が行う処理
作成するアプリの概要
- バニラの「高評価」/「低評価」ウェブ コンポーネント
- 高評価 / 低評価の Lit ベースのウェブ コンポーネント
必要なもの
- 最新のブラウザ(Chrome、Safari、Firefox、Chromium Edge)。ウェブ コンポーネントはすべての最新ブラウザで動作し、Microsoft Internet Explorer 11 と非 Chromium 版 Microsoft Edge ではポリフィルを利用できます。
- HTML、CSS、JavaScript、Chrome DevTools に関する知識。
2. 環境の設定と Playground の確認
コードへのアクセス
この Codelab では、次のような Lit 環境へのリンクが設けられています。
この環境は、ブラウザで完全に動作するコード サンドボックスです。TypeScript や JavaScript のファイルをコンパイルして実行できるだけでなく、次のようなノード モジュールへの読み込みを自動的に解決することも可能です。
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://unpkg.com/lit?module';
これらのチェックポイントを出発点に、Lit 環境でチュートリアル全体を実施できます。VS Code を使用する場合は、これらのチェックポイントから任意のステップの開始用コードをダウンロードしたり、そのコードを使って製品をチェックしたりできます。
Lit 環境の UI

Lit 環境の UI のスクリーンショットで、この Codelab で使用するセクションがハイライト表示されています。
- ファイル選択ツール。プラスボタンをご確認ください。
- ファイル エディタ。
- コード プレビュー。
- 再読み込みボタン。
- ダウンロード ボタン。
VS Code の設定(上級者向け)
この VS Code の設定を使用すると、次の特典を利用できます。
- テンプレートの型チェック
- テンプレートの入力支援とオートコンプリート
NPM と VS Code(および lit-plugin プラグイン)がインストール済みで、これらの環境の使用方法をご存知の場合は、次の手順でプロジェクトをダウンロードして開始できます。
- ダウンロード ボタンを押す
- tar ファイルの中身をディレクトリに展開する
- ベアモジュール指定子(Lit チームの推奨は @web/dev-server)を解決できる開発サーバーをインストールする
- 次に例を示します。
package.json
- 次に例を示します。
- 開発サーバーを実行し、ブラウザを開く(
@web/dev-serverを使用している場合はnpx web-dev-server --node-resolve --watch --openを使用できます)package.jsonの例を使用している場合は、npm run serveを使用します。
3. カスタム要素を定義する
カスタム要素
Web Components は、4 つのネイティブ ウェブ API の集合です。それらは次のとおりです。
- ES モジュール
- カスタム要素
- Shadow DOM
- HTML テンプレート
ES モジュール仕様はすでに使用しています。この仕様では、<script type="module"> でページに読み込まれるインポートとエクスポートを含む JavaScript モジュールを作成できます。
カスタム要素を定義する
カスタム要素の仕様により、ユーザーは JavaScript を使用して独自の HTML 要素を定義できます。ネイティブ ブラウザ要素と区別するため、名前にはハイフン(-)を含める必要があります。index.js ファイルを消去し、カスタム要素クラスを定義します。
index.js
class RatingElement extends HTMLElement {}
customElements.define('rating-element', RatingElement);
カスタム要素は、HTMLElement を拡張するクラスをハイフン付きのタグ名に関連付けることで定義されます。customElements.define の呼び出しは、ブラウザにクラス RatingElement を tagName ‘rating-element' に関連付けるよう指示します。つまり、ドキュメント内の <rating-element> という名前のすべての要素がこのクラスに関連付けられます。
ドキュメントの本文に <rating-element> を配置して、レンダリングされる内容を確認します。
index.html
<body>
<rating-element></rating-element>
</body>
出力を見ると、何もレンダリングされていないことがわかります。これは、ブラウザに <rating-element> のレンダリング方法を伝えていないため、想定内のことです。カスタム要素の定義が成功したことを確認するには、Chrome DevTools の要素セレクタで <rating-element> を選択し、コンソールで次のコマンドを呼び出します。
$0.constructor
出力は次のようになります。
class RatingElement extends HTMLElement {}
カスタム要素のライフサイクル
カスタム要素には、一連のライフサイクル フックが付属しています。それらは次のとおりです。
constructorconnectedCallbackdisconnectedCallbackattributeChangedCallbackadoptedCallback
constructor は、要素が初めて作成されたときに呼び出されます(document.createElement(‘rating-element') や new RatingElement() の呼び出しなど)。コンストラクタは要素を設定するのに適していますが、要素の「起動」パフォーマンス上の理由から、コンストラクタで DOM 操作を行うことは一般的に避けるべきです。
カスタム要素が DOM に接続されると、connectedCallback が呼び出されます。通常、ここで初期 DOM 操作が行われます。
disconnectedCallback は、カスタム要素が DOM から削除された後に呼び出されます。
attributeChangedCallback(attrName, oldValue, newValue) は、ユーザーが指定した属性のいずれかが変更されたときに呼び出されます。
adoptedCallback は、HTMLTemplateElement のように、別の documentFragment から adoptNode を介してメイン ドキュメントにカスタム要素が採用されたときに呼び出されます。
DOM をレンダリング
カスタム要素に戻り、DOM を関連付けます。要素が DOM に接続されたときに、要素のコンテンツを設定します。
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
this.innerHTML = `
<style>
rating-element {
display: inline-flex;
align-items: center;
}
rating-element button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
`;
}
}
customElements.define('rating-element', RatingElement);
constructor で、rating というインスタンス プロパティを要素に保存します。connectedCallback では、DOM の子を <rating-element> に追加して、高評価ボタンと低評価ボタンとともに現在の評価を表示します。
4. Shadow DOM
Shadow DOM を使用する理由
前のステップで挿入したスタイルタグのセレクタは、ページ上のすべての評価要素とすべてのボタンを選択します。この場合、スタイルが要素から漏れ出て、意図しないノードが選択されることがあります。また、このカスタム要素以外のスタイルが、カスタム要素内のノードに意図せずスタイルを適用してしまう可能性があります。たとえば、メイン ドキュメントの head に style タグを配置してみます。
index.html
<!DOCTYPE html>
<html>
<head>
<script src="./index.js" type="module"></script>
<style>
span {
border: 1px solid red;
}
</style>
</head>
<body>
<rating-element></rating-element>
</body>
</html>
出力では、評価の span の周りに赤い枠線が表示されます。これは簡単な例ですが、DOM のカプセル化がないと、より複雑なアプリケーションで大きな問題が発生する可能性があります。ここで Shadow DOM が登場します。
シャドウルートをアタッチする
要素にシャドウルートをアタッチし、そのルート内の DOM をレンダリングします。
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
`;
}
}
customElements.define('rating-element', RatingElement);
ページを更新すると、メイン ドキュメントのスタイルでシャドー ルート内のノードを選択できなくなります。
この操作をどのように行いましたか。呼び出した connectedCallback で、要素にシャドー ルートを付加する this.attachShadow を呼び出しました。open モードでは、シャドー コンテンツを検査でき、this.shadowRoot を介してシャドー ルートにもアクセスできます。Chrome インスペクタで Web コンポーネントも確認してみましょう。

コンテンツを保持する展開可能なシャドウ ルートが表示されます。シャドウルート内のすべてのものが Shadow DOM と呼ばれます。Chrome Dev Tools で評価要素を選択して $0.children を呼び出すと、子要素が返されないことがわかります。これは、Shadow DOM が直接の子と同じ DOM ツリーの一部ではなく、シャドウ ツリーと見なされるためです。
ライト DOM
実験: <rating-element> の直接の子としてノードを追加します。
index.html
<rating-element>
<div>
This is the light DOM!
</div>
</rating-element>
ページを更新すると、このカスタム要素の Light DOM 内の新しい DOM ノードがページに表示されないことがわかります。これは、Shadow DOM には <slot> 要素を介して Light DOM ノードを Shadow DOM に投影する方法を制御する機能があるためです。
5. HTML テンプレート
テンプレートを使用する理由
サニタイズなしで innerHTML とテンプレート リテラル文字列を使用すると、スクリプト インジェクションに関するセキュリティ上の問題が発生する可能性があります。過去には DocumentFragment を使用する方法もありましたが、テンプレートが定義されたときに画像が読み込まれたりスクリプトが実行されたりするなどの問題や、再利用性の妨げになるという問題もありました。ここで <template> 要素が登場します。テンプレートは、不活性 DOM、ノードを複製するパフォーマンスの高いメソッド、再利用可能なテンプレート機能を提供します。
テンプレートの使用
次に、HTML テンプレートを使用するようにコンポーネントを移行します。
index.html
<body>
<template id="rating-element-template">
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
</style>
<button class="thumb_down" >
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating"></span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>
</template>
<rating-element>
<div>
This is the light DOM!
</div>
</rating-element>
</body>
ここでは、DOM コンテンツをメイン ドキュメントの DOM のテンプレート タグに移動しました。カスタム要素の定義をリファクタリングします。
index.js
class RatingElement extends HTMLElement {
constructor() {
super();
this.rating = 0;
}
connectedCallback() {
const shadowRoot = this.attachShadow({mode: 'open'});
const templateContent = document.getElementById('rating-element-template').content;
const clonedContent = templateContent.cloneNode(true);
shadowRoot.appendChild(clonedContent);
this.shadowRoot.querySelector('.rating').innerText = this.rating;
}
}
customElements.define('rating-element', RatingElement);
このテンプレート要素を使用するには、テンプレートをクエリしてそのコンテンツを取得し、templateContent.cloneNode でノードを複製します。ここで、true 引数はディープクローンを実行します。次に、データを使用して dom を初期化します。
これで、ウェブ コンポーネントが作成されました。残念ながら、まだ何も機能しないため、次に機能を追加します。
6. 機能を追加する
プロパティ バインディング
現在、rating-element の評価を設定する唯一の方法は、要素を構築し、オブジェクトの rating プロパティを設定してから、ページに配置することです。残念ながら、ネイティブの HTML 要素はこのような動作をしません。ネイティブの HTML 要素は、プロパティと属性の両方の変更で更新される傾向があります。
次の行を追加して、rating プロパティが変更されたときにカスタム要素がビューを更新するようにします。
index.js
constructor() {
super();
this._rating = 0;
}
set rating(value) {
this._rating = value;
if (!this.shadowRoot) {
return;
}
const ratingEl = this.shadowRoot.querySelector('.rating');
if (ratingEl) {
ratingEl.innerText = this._rating;
}
}
get rating() {
return this._rating;
}
評価プロパティのセッターとゲッターを追加し、評価要素のテキストが使用可能な場合は更新します。つまり、要素に評価プロパティを設定すると、ビューが更新されます。DevTools コンソールで簡単にテストしてみましょう。
属性バインディング
次に、属性が変更されたときにビューを更新します。これは、<input value="newValue"> を設定したときに入力がビューを更新するのと同様です。幸いなことに、ウェブ コンポーネントのライフサイクルには attributeChangedCallback が含まれています。次の行を追加して、評価を更新します。
index.js
static get observedAttributes() {
return ['rating'];
}
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'rating') {
const newRating = Number(newValue);
this.rating = newRating;
}
}
attributeChangedCallback をトリガーするには、RatingElement.observedAttributes which defines the attributes to be observed for changes の静的ゲッターを設定する必要があります。次に、DOM で評価を宣言的に設定します。ぜひお試しください。
index.html
<rating-element rating="5"></rating-element>
これで、評価が宣言的に更新されるようになります。
ボタンの機能
あとはボタンの機能を追加するだけです。このコンポーネントの動作では、ユーザーが 1 回の賛成または反対の投票を行えるようにし、ユーザーに視覚的なフィードバックを提供する必要があります。これは、いくつかのイベント リスナーと反映プロパティで実装できますが、まず次の行を追加して、スタイルを更新し、視覚的なフィードバックを提供します。
index.html
<style>
...
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
</style>
Shadow DOM では、:host セレクタは Shadow Root がアタッチされているノードまたはカスタム要素を参照します。この場合、vote 属性が "up" の場合は高評価ボタンが緑色になりますが、vote が "down", then it will turn the thumb-down button red の場合は高評価ボタンが緑色になりません。次に、rating を実装したときと同様に、vote のリフレクティング プロパティ / 属性を作成して、このロジックを実装します。プロパティのセッターとゲッターから始めます。
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
}
set vote(newValue) {
const oldValue = this._vote;
if (newValue === oldValue) {
return;
}
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
this._vote = newValue;
this.setAttribute('vote', newValue);
}
get vote() {
return this._vote;
}
constructor で _vote インスタンス プロパティを null で初期化し、セッターで新しい値が異なるかどうかを確認します。その場合は、評価を適宜調整し、重要なこととして、vote 属性を this.setAttribute でホストに反映します。
次に、属性バインディングを設定します。
index.js
static get observedAttributes() {
return ['rating', 'vote'];
}
attributeChangedCallback(attributeName, oldValue, newValue) {
if (attributeName === 'rating') {
const newRating = Number(newValue);
this.rating = newRating;
} else if (attributeName === 'vote') {
this.vote = newValue;
}
}
これも rating 属性バインディングと同じプロセスです。observedAttributes に vote を追加し、attributeChangedCallback で vote プロパティを設定します。最後に、ボタンにクリック イベント リスナーを追加して、ボタンに機能を追加します。
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
this._boundOnUpClick = this._onUpClick.bind(this);
this._boundOnDownClick = this._onDownClick.bind(this);
}
connectedCallback() {
...
this.shadowRoot.querySelector('.thumb_up')
.addEventListener('click', this._boundOnUpClick);
this.shadowRoot.querySelector('.thumb_down')
.addEventListener('click', this._boundOnDownClick);
}
disconnectedCallback() {
this.shadowRoot.querySelector('.thumb_up')
.removeEventListener('click', this._boundOnUpClick);
this.shadowRoot.querySelector('.thumb_down')
.removeEventListener('click', this._boundOnDownClick);
}
_onUpClick() {
this.vote = 'up';
}
_onDownClick() {
this.vote = 'down';
}
constructor では、クリック リスナーを要素にバインドし、参照を保持します。connectedCallback では、ボタンのクリック イベントをリッスンします。disconnectedCallback では、これらのリスナーをクリーンアップし、クリック リスナー自体で vote を適切に設定します。
これで、機能が充実したウェブ コンポーネントが完成しました。ボタンをクリックしてみてください。問題は、JS ファイルが 96 行、HTML ファイルが 43 行に達し、このような単純なコンポーネントにしてはコードがかなり冗長で命令的になっていることです。ここで Google の Lit プロジェクトの出番です。
7. Lit-html
コードのチェックポイント
lit-html を選ぶ理由
まず、<template> タグは便利でパフォーマンスも優れていますが、コンポーネントのロジックとパッケージ化されていないため、テンプレートを他のロジックとともに配布することが困難です。また、テンプレート要素の使用方法が本質的に命令型コードにつながるため、宣言型コーディング パターンと比較して、コードの可読性が低下する場合があります。
ここで lit-html の出番です。Lit html は、JavaScript で HTML テンプレートを記述し、それらのテンプレートをデータとともに効率的にレンダリングして再レンダリングし、DOM を作成して更新できる Lit のレンダリング システムです。一般的な JSX ライブラリや VDOM ライブラリと似ていますが、ブラウザでネイティブに実行され、多くの場合、はるかに効率的です。
Lit HTML の使用
次に、ネイティブの Web コンポーネント rating-element を移行して、Lit テンプレートを使用します。Lit テンプレートは、特別な構文でテンプレート文字列を引数として受け取る関数であるタグ付きテンプレート リテラルを使用します。Lit は、内部でテンプレート要素を使用して、高速レンダリングとセキュリティのためのサニタイズ機能を提供します。まず、index.html の <template> を Lit テンプレートに移行します。そのためには、ウェブ コンポーネントに render() メソッドを追加します。
index.js
// Dont forget to import from Lit!
import {render, html} from 'lit';
class RatingElement extends HTMLElement {
...
render() {
if (!this.shadowRoot) {
return;
}
const template = html`
<style>
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
</style>
<button class="thumb_down">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button class="thumb_up">
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>`;
render(template, this.shadowRoot);
}
}
index.html からテンプレートを削除することもできます。この render メソッドでは、template という変数を定義し、html タグ付きテンプレート リテラル関数を呼び出します。また、${...} のテンプレート リテラル補間構文を使用して、span.rating 要素内で簡単なデータ バインディングを実行していることもわかります。つまり、最終的にはそのノードを命令的に更新する必要がなくなります。また、lit の render メソッドを呼び出して、テンプレートをシャドウルートに同期的にレンダリングします。
宣言型構文への移行
<template> 要素を削除したので、代わりに新しく定義した render メソッドを呼び出すようにコードをリファクタリングします。まず、lit のイベント リスナー バインディングを活用して、リスナー コードを整理します。
index.js
<button
class="thumb_down"
@click=${() => {this.vote = 'down'}}>
...
<button
class="thumb_up"
@click=${() => {this.vote = 'up'}}>
Lit テンプレートでは、@EVENT_NAME バインディング構文を使用してノードにイベント リスナーを追加できます。この例では、ボタンがクリックされるたびに vote プロパティを更新します。
次に、constructor、connectedCallback、disconnectedCallback のイベント リスナーの初期化コードをクリーンアップします。
index.js
constructor() {
super();
this._rating = 0;
this._vote = null;
}
connectedCallback() {
this.attachShadow({mode: 'open'});
this.render();
}
// remove disonnectedCallback and _onUpClick and _onDownClick
3 つのコールバックすべてからクリック リスナーのロジックを削除し、disconnectedCallback を完全に削除することもできました。また、connectedCallback から DOM 初期化コードをすべて削除して、コードをよりエレガントにすることもできました。つまり、_onUpClick と _onDownClick のリスナー メソッドを削除できます。
最後に、プロパティ セッターを更新して render メソッドを利用し、プロパティまたは属性のいずれかが変更されたときに DOM を更新できるようにします。
index.js
set rating(value) {
this._rating = value;
this.render();
}
...
set vote(newValue) {
const oldValue = this._vote;
if (newValue === oldValue) {
return;
}
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
this._vote = newValue;
this.setAttribute('vote', newValue);
// add render method
this.render();
}
ここでは、rating セッターから DOM 更新ロジックを削除し、vote セッターから render への呼び出しを追加しました。バインディングとイベント リスナーが適用されている場所がわかるため、テンプレートの可読性が大幅に向上しました。
ページを更新すると、評価ボタンが機能するようになります。高評価ボタンを押すと、次のように表示されます。

8. LitElement
LitElement を選ぶ理由
コードにはまだ問題が残っています。まず、vote プロパティまたは属性を変更すると、rating プロパティが変更され、render が 2 回呼び出される可能性があります。render の繰り返し呼び出しは実質的に何もしないため効率的ですが、JavaScript VM はその関数を同期的に 2 回呼び出すのに時間を費やしています。2 つ目は、新しいプロパティと属性を追加する作業が面倒であることです。多くのボイラープレート コードが必要になるためです。そこで LitElement の出番です。
LitElement は、フレームワークや環境をまたいで使用できる高速で軽量なウェブ コンポーネントを作成するための Lit の基本クラスです。次に、実装を変更して LitElement を使用することで、rating-element で何ができるかを見てみましょう。
LitElement の使用
まず、lit パッケージから LitElement 基本クラスをインポートしてサブクラス化します。
index.js
import {LitElement, html, css} from 'lit';
class RatingElement extends LitElement {
// remove connectedCallback()
...
rating-element の新しい基本クラスである LitElement をインポートします。次に html インポートを保持し、最後に css を保持します。これにより、css 数学、テンプレート、その他の機能の css タグ付きテンプレート リテラルを定義できます。
次に、レンダリング メソッドから Lit の静的スタイルシートにスタイルを移動します。
index.js
class RatingElement extends LitElement {
static get styles() {
return css`
:host {
display: inline-flex;
align-items: center;
}
button {
background: transparent;
border: none;
cursor: pointer;
}
:host([vote=up]) .thumb_up {
fill: green;
}
:host([vote=down]) .thumb_down {
fill: red;
}
`;
}
...
Lit では、ほとんどのスタイルがここにあります。Lit はこれらのスタイルを取得し、構築可能なスタイルシートなどのブラウザ機能を使用して、レンダリング時間を短縮します。また、必要に応じて、古いブラウザの Web コンポーネント ポリフィルを介して渡します。
Lifecycle
Lit では、ネイティブの Web コンポーネント コールバックに加えて、一連のレンダリング ライフサイクル コールバック メソッドが導入されています。これらのコールバックは、宣言された Lit プロパティが変更されるとトリガーされます。
この機能を使用するには、どのプロパティがレンダリング ライフサイクルをトリガーするかを静的に宣言する必要があります。
index.js
static get properties() {
return {
rating: {
type: Number,
},
vote: {
type: String,
reflect: true,
}
};
}
// remove observedAttributes() and attributeChangedCallback()
// remove set rating() get rating()
ここでは、rating と vote が LitElement レンダリング ライフサイクルをトリガーすることを定義し、文字列属性をプロパティに変換するために使用される型を定義します。
<user-profile .name=${this.user.name} .age=${this.user.age}>
${this.user.family.map(member => html`
<family-member
.name=${member.name}
.relation=${member.relation}>
</family-member>`)}
</user-profile>
また、vote プロパティの reflect フラグは、vote セッターで手動でトリガーしたホスト要素の vote 属性を自動的に更新します。
静的プロパティ ブロックができたので、属性とプロパティのレンダリング更新ロジックをすべて削除できます。つまり、次のメソッドを削除できます。
connectedCallbackobservedAttributesattributeChangedCallbackrating(セッターとゲッター)vote(セッターとゲッター。ただし、セッターの変更ロジックは保持)
constructor を維持し、新しい willUpdate ライフサイクル メソッドを追加します。
index.js
constructor() {
super();
this.rating = 0;
this.vote = null;
}
willUpdate(changedProps) {
if (changedProps.has('vote')) {
const newValue = this.vote;
const oldValue = changedProps.get('vote');
if (newValue === 'up') {
if (oldValue === 'down') {
this.rating += 2;
} else {
this.rating += 1;
}
} else if (newValue === 'down') {
if (oldValue === 'up') {
this.rating -= 2;
} else {
this.rating -= 1;
}
}
}
}
// remove set vote() and get vote()
ここでは、rating と vote を初期化し、vote セッター ロジックを willUpdate ライフサイクル メソッドに移動します。LitElement はプロパティの変更をバッチ処理し、レンダリングを非同期にするため、更新プロパティが変更されるたびに render の前に willUpdate メソッドが呼び出されます。willUpdate でリアクティブ プロパティ(this.rating など)を変更しても、不要な render ライフサイクル呼び出しはトリガーされません。
最後に、render は LitElement のライフサイクル メソッドです。Lit テンプレートを返す必要があります。
index.js
render() {
return html`
<button
class="thumb_down"
@click=${() => {this.vote = 'down'}}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"/></svg>
</button>
<span class="rating">${this.rating}</span>
<button
class="thumb_up"
@click=${() => {this.vote = 'up'}}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewbox="0 0 24 24" width="24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>
</button>`;
}
シャドー ルートを確認する必要がなくなり、以前に 'lit' パッケージからインポートした render 関数を呼び出す必要もなくなりました。
要素がプレビューにレンダリングされるので、クリックしてみましょう。
9. 完了
お疲れさまでした。これで、ウェブ コンポーネントをゼロから作成し、LitElement に進化させることができました。
Lit は非常に小さく(5 KB 未満に軽量化され gzip 圧縮)、非常に高速で、コーディングがとても楽しいフレームワークです。他のフレームワークで使用されるコンポーネントを作成することも、それを使用して本格的なアプリを構築することもできます。
これで、ウェブ コンポーネントとは何か、その作成方法、Lit を使用して簡単に作成する方法を理解できました。
コードのチェックポイント
最終的なコードを私たちのコードと照らし合わせてみましょう。こちらで比較できます。
次のステップ
他の Codelab もご覧ください。
参考資料
- Lit のインタラクティブ チュートリアル
- The Lit Docs
- Open Web Components - コミュニティ運営のガイダンスとツール コミュニティ
- WebComponents.dev - 既知のすべてのフレームワークでウェブ コンポーネントを作成する
コミュニティ
- Lit and Friends Slack - 最大の Web コンポーネント コミュニティ
- Twitter の@buildWithLit - Lit を作成したチームの Twitter アカウント
- Web Components SF - サンフランシスコのウェブ コンポーネント ミートアップ