1. はじめに
最終更新日: 2021 年 8 月 10 日
ウェブ コンポーネント
Web Components は、ウェブページやウェブアプリで使用する、再利用可能なカプセル化されたカスタム HTML タグを作成できるウェブ プラットフォーム API セットです。ウェブ コンポーネント標準に基づいて構築されたカスタム コンポーネントやウィジェットは、最新のブラウザで動作し、HTML で動作する任意の JavaScript ライブラリやフレームワークで使用できます。
Lit とは
Lit は、あらゆるフレームワークで動作する、またはフレームワークをまったく必要としない、高速で軽量のウェブ コンポーネントを構築するためのシンプルなライブラリです。Lit では、共有可能なコンポーネント、アプリケーション、デザイン システムなどを作成できます。
Lit には、プロパティ、属性、レンダリングの管理など、Web Components の一般的なタスクを簡素化するための API が用意されています。
学習内容
- Web コンポーネントとは
- Web Components のコンセプト
- ウェブ コンポーネントを作成する方法
- lit-html と LitElement とは
- ウェブ コンポーネント上での Lit の動作
作成するアプリの概要
- 標準的な高評価 / 低評価ウェブ コンポーネント
- 高評価 / 低評価 Lit ベースのウェブ コンポーネント
必要なもの
- 更新された最新のブラウザ(Chrome、Safari、Firefox、Chromium Edge)。Web Components は最新のすべてのブラウザで動作し、ポリフィルは Microsoft Internet Explorer 11 と Chrome 以外の Microsoft Edge で使用できます。
- HTML、CSS、JavaScript、Chrome DevTools の知識。
2. 設定とプレイグラウンドを探索する
コードへのアクセス
Codelab の随所で、次のように Lit プレイグラウンドへのリンクが表示されます。
このプレイグラウンドは、ブラウザで完全に動作するコード サンドボックスです。TypeScript ファイルと JavaScript ファイルをコンパイルして実行できるほか、ノード モジュールへのインポートを自動的に解決することもできます。例:
// before
import './my-file.js';
import 'lit';
// after
import './my-file.js';
import 'https://unpkg.com/lit?module';
これらのチェックポイントを開始点として使用して、Lit Playground でチュートリアル全体を進めることができます。VS Code を使用している場合は、これらのチェックポイントを使用して、任意のステップの開始コードをダウンロードしたり、それらを使用して作業をチェックしたりできます。
点灯している Playground 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. カスタム要素を定義する
カスタム要素
ウェブ コンポーネントとは、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 Dev Tools で <rating-element>
を選択することで、カスタム要素の定義が成功したことを確認できます。要素セレクタを使用し、コンソールで以下を呼び出します。
$0.constructor
出力は次のようになります。
class RatingElement extends HTMLElement {}
カスタム要素のライフサイクル
カスタム要素には一連のライフサイクル フックがあります。それらは次のとおりです。
constructor
connectedCallback
disconnectedCallback
attributeChangedCallback
adoptedCallback
constructor
は、document.createElement(‘rating-element')
や new RatingElement()
の呼び出しなど、要素が最初に作成されるときに呼び出されます。コンストラクタは要素を設定する場所として適していますが、要素の「起動」のためにコンストラクタ内で DOM 操作を行うことは一般的におすすめできません。パフォーマンス上の理由を確認できます。
connectedCallback
は、カスタム要素が DOM に追加されると呼び出されます。通常、この段階で最初の DOM 操作が行われます。
disconnectedCallback
は、カスタム要素が DOM から削除されると呼び出されます。
attributeChangedCallback(attrName, oldValue, newValue)
は、ユーザー指定の属性のいずれかが変更されると呼び出されます。
adoptedCallback
は、HTMLTemplateElement
などで、adoptNode
を介して別の documentFragment
からメイン ドキュメントにカスタム要素が採用されたときに呼び出されます。
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 が選ばれる理由
前の手順で挿入した 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>
出力では、評価のスパンの周りに赤い枠線ボックスが表示されます。これはごくわずかなケースですが、DOM カプセル化がないため、より複雑なアプリケーションでは大きな問題が発生する可能性があります。そこで役に立つのが Shadow DOM です。
Shadow ルートのアタッチ
要素に Shadow Root をアタッチし、そのルート内に 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);
ページを更新すると、メイン ドキュメントのスタイルでは Shadow Root 内のノードを選択できなくなっていることがわかります。
この操作をどのように行いましたか。connectedCallback
で、要素にシャドウルートをアタッチする this.attachShadow
を呼び出しました。open
モードは、シャドウ コンテンツを検査可能で、this.shadowRoot
を介してシャドウルートにもアクセスできることを意味します。Chrome インスペクタでウェブ コンポーネントも確認します。
コンテンツを保持している展開可能なシャドウルートが表示されます。その Shadow ルートの内側にあるものはすべて、Shadow DOM と呼ばれます。Chrome Dev Tools で評価要素を選択して $0.children
を呼び出すと、子が返されないことがわかります。これは、Shadow DOM が直接の子と同じ DOM ツリーの一部ではなく、Shadow Tree とみなされるためです。
Light 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>
要素です。テンプレートでは、inert 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 を初期化します。
これで、Web Component が完成しました。まだ何の機能もありません。次に、いくつかの機能を追加します。
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;
}
評価プロパティにセッターとゲッターを追加し、評価要素のテキストが利用できる場合は更新します。つまり、この要素に評価プロパティを設定すると、ビューが更新されます。Dev Tools コンソールで簡単なテストを行います。
属性バインディング
次に、属性が変更されたときにビューを更新します。これは、<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 ルートがアタッチされているノードまたはカスタム要素を参照します。この場合、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
で null
を使用して _vote
インスタンス プロパティを初期化し、セッターで新しい値が異なるかどうかを確認します。その場合は、それに応じて評価を調整します。また、重要な点として、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
属性バインディングで行ったプロセスと同じです。vote
を observedAttributes
に追加し、vote
プロパティを attributeChangedCallback
に設定するとします。最後に、クリック イベント リスナーを追加して、ボタンに機能を追加します。
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 は Lit のレンダリング システムです。JavaScript で HTML テンプレートを記述し、データとともにそれらのテンプレートを効率的にレンダリングおよび再レンダリングして、DOM を作成および更新します。これは一般的な JSX ライブラリや VDOM ライブラリに似ていますが、ブラウザでネイティブに実行され、多くの場合、はるかに効率的に実行されます。
Lit HTML の使用
次に、ネイティブ ウェブ コンポーネント rating-element
を移行して、タグ付きテンプレート リテラルを使用する Lit テンプレートを使用します。これは、テンプレート文字列を、特別な構文で引数として受け取る関数です。その後、Lit は内部でテンプレート要素を使用して、レンダリングを高速化するとともに、セキュリティのためのサニタイズ機能を提供します。まず、ウェブ コンポーネントに render()
メソッドを追加して、index.html
の <template>
を Lit テンプレートに移行します。
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
からテンプレートを削除することもできます。このレンダリング メソッドでは、template
という変数を定義し、html
タグ付きのテンプレートのリテラル関数を呼び出します。また、${...}
のテンプレート リテラル補間構文を使用して、span.rating
要素内で単純なデータ バインディングを実行しています。つまり、最終的にそのノードを命令的に更新する必要がなくなるということです。さらに、lit render
メソッドを呼び出して、テンプレートを Shadow ルートに同期的にレンダリングします。
宣言型構文への移行
<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
メソッドを使用するようにプロパティ セッターを更新して、プロパティまたは属性が変更されたときにドメインを更新できるようにします。
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 回呼び出される可能性があります。レンダリングの繰り返し呼び出しは基本的に NoOps で効率的ですが、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 タグ付きのテンプレート リテラルを定義できます。
次に、スタイルを render メソッドから 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 はこれらのスタイルを利用し、Constructable Stylesheets などのブラウザ機能を使用してレンダリング時間を短縮します。また、必要に応じて古いブラウザの Web Components ポリフィルにも渡します。
Lifecycle
Lit には、ネイティブのウェブ コンポーネントのコールバックに加えて、レンダリング ライフサイクル コールバック メソッドのセットが導入されています。これらのコールバックは、宣言された 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
属性が自動的に更新されます。
これで静的プロパティ ブロックが用意できたので、属性とプロパティのレンダリング更新ロジックをすべて削除できます。つまり、以下のメソッドを削除できます。
connectedCallback
observedAttributes
attributeChangedCallback
rating
(セッターとゲッター)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 はプロパティの変更をバッチ処理し、レンダリングを非同期で行うため、更新対象のプロパティが変更されるたびに、willUpdate
メソッドが render
の前に呼び出されます。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 は超小型(圧縮して gzip 圧縮したものが 5 KB 未満)、非常に高速で、コーディングも非常に楽しい。コンポーネントを他のフレームワークで使用できるようにすることも、それを使用して本格的なアプリを構築することもできます。
以上で、ウェブ コンポーネントの概要、作成方法、Lit を使用して簡単に作成する方法を学びました。
コードのチェックポイント
完成したコードを Google のコードと照らし合わせてチェックしますか?こちらで比較できます。
次のステップ
他の Codelab もぜひご覧ください。
参考資料
- Lit のインタラクティブなチュートリアル
- Lit ドキュメント
- Open Web Components - ガイダンスやツールを実行するコミュニティ
- WebComponents.dev - 既知のすべてのフレームワークでウェブ コンポーネントを作成する
コミュニティ
- Lit and Friends Slack - 最大のウェブ コンポーネント コミュニティ
- Twitter の@buildWithLit - Lit を開発したチームの Twitter アカウント
- Web Components SF - サンフランシスコの Web Components イベント