🚀

Introducing "Lit" for Web Components

2021/04/27に公開

概要

2021/04/22に新しいWeb Componentsライブラリ、Lit(Lit 2.0)がリリースされ、同日ローンチイベントもYouTubeで生配信されました。
https://twitter.com/buildWithLit/status/1384929943386214401?s=20

https://www.youtube.com/watch?v=f1j7b696L-E

それに伴いPolymer ProjectLitに改名されロゴが刷新されました。
https://twitter.com/buildWithLit/status/1384572664002478093?s=20

ウェブサイトも新しく公開されました。チュートリアルとPlaygroundが刷新され、非常に便利になりました。
https://lit.dev/

実装は https://github.com/lit/lit に公開されています。

npm i lit

LitElementとlit-htmlのおさらい

旧Polymer Project(Lit 1.0)では、lit-html(HTMLテンプレートライブラリ)とLitElement(Web Componentsを実装するためのライブラリ)の2つのライブラリが提供されていました。
LitElementにもテンプレート機能があったので、2つとも独立したライブラリとして提供されていました。

Lit(Lit 2.0)の特徴

Litlit-htmlLitElementなどのWeb Componentsを開発するのに必要なライブラリを1つにまとめたものです。
コードも1つのリポジトリにまとめられmonorepoになっています。
https://github.com/lit/lit

Simple. Fast. Web Components. というフレーズからわかるようにシンプルかつスタンダードなWeb Componentsをより簡単に開発することにフォーカスしているのが特徴です。

詳しい機能についてはドキュメントを読んでください。

https://lit.dev/docs/

Lit 1.0からの改善点

軽量化とパフォーマンス向上

APIの再設計や不要な機能の削除、polyfillの分離などを行った結果、新機能を追加したにもかかわらず軽量化とパフォーマンス向上に成功しました。ローンチイベントで発表された具体的な数値は次のとおりです。

  • minify時のサイズが30%減少(15KB)
  • 初期レンダリング速度が5%〜20%改善
  • レンダリング更新速度が7%〜15%改善

SSR対応

LitDeclarative Shadow DOMという仕様を利用してShadow DOMのSSRに対応しました。(おそらくWeb Components系ライブラリでは初。)
SSR対応のために内部実装の再設計やlit-htmlに実装されていたマイナーな低レベルAPIを削除したとイベントの中で言っていました。

SSR対応はまだ正式にはリリースされていませんが、実装はすでにありGitHubでコードが公開されています。(後述のlit-labs/ssrを参照。)

基本的な書き方

Litの基本的な書き方は次のとおりです。

import {LitElement, css, html} from 'lit';
import {customElement, property} from 'lit/decorators.js';

@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
  // Define scoped styles right with your component, in plain CSS
  static styles = css`
    :host {
      color: blue;
    }
  `;

  // Declare reactive properties
  @property()
  name?: string = 'World';

  // Render the UI as a function of component state
  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}

ドキュメントのサンプルコードはほぼ全てデコレーターを使って書かれていますが、デコレーターを使わずに書くことも可能です。

import {LitElement, css, html} from 'lit';

export class SimpleGreeting extends LitElement {
  name?: string;

  static styles = css`
    :host {
      color: blue;
    }
  `;

  static get properties() {
    return {
      name: {}
    }
  }

  constructor() {
    super();
    this.name = 'World';
  }

  render() {
    return html`<p>Hello, ${this.name}!</p>`;
  }
}

customElements.define('simple-greeting', SimpleGreeting);

Lit 2.0の新機能

Lit 2.0で新しく追加された機能の中から注目度の高い3つの機能を紹介します。

ReactiveController

ReactiveControllerは今回の目玉機能の1つです。
https://lit.dev/docs/composition/controllers/

ReactiveControllerは、コンポーネントのライフサイクルにアクセスできるオブジェクトです。この仕組を使うことでロジックや状態管理をコンポーネントの外側にまとめて定義でき、複数のコンポーネントで再利用できるようになります。

以下のサンプルコードでは時計を表示するコンポーネントをコントローラーを利用して構築しています。

clock-controller.ts
import {ReactiveController, ReactiveControllerHost} from 'lit';

export class ClockController implements ReactiveController {
  host: ReactiveControllerHost;

  value = new Date();
  timeout: number;
  private _timerID?: number;

  constructor(host: ReactiveControllerHost, timeout = 1000) {
    (this.host = host).addController(this);
    this.timeout = timeout;
  }
  hostConnected() {
    // Start a timer when the host is connected
    this._timerID = setInterval(() => {
      this.value = new Date();
      // Update the host with new value
      this.host.requestUpdate();
    });
  }
  hostDisconnected() {
    // Clear the timer when the host is disconnected
    clearInterval(this._timerID);
    this._timerID = undefined;
  }
}
my-element.ts
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ClockController} from './clock-controller.js';

@customElement('my-element')
class MyElement extends LitElement {
  // Create the controller and store it
  private clock = new ClockController(this, 100);

  // Use the controller in render()
  render() {
    const formattedTime = timeFormat.format(this.clock.value);
    return html`Current time: ${formattedTime}`;
  }
}

const timeFormat = new Intl.DateTimeFormat('en-US', {
  hour: 'numeric', minute: 'numeric', second: 'numeric',
});

ReactiveControllerHost

ReactiveControllerの要はこのReactiveControllerHost APIにあります。

ReactiveControllerHostはコントローラーのコンポーネントへの追加や更新のリクエストを行い、Litのライフサイクルメソッドを呼び出す役割を持つオブジェクトです。

ReactiveControllerHostLitElementである必要はなく、公開されているinterfaceに則ったオブジェクトであれば何でもOKです。

interface ReactiveControllerHost {
  addController(controller: ReactiveController): void;
  removeController(controller: ReactiveController): void;
  requestUpdate(): void;
  readonly updateComplete: Promise<boolean>;
}

そのため、このReactiveControllerHostの役割を持ったオブジェクトを実装すれば他のフレームワーク中でもコントローラーを利用できるようになります。

後述するlit-labs/reactはReactとコントローラーを接続するための機能を提供するライブラリで、内部でReactiveControllerHostが利用されています。現在、他のフレームワーク向けの実装も進められているみたいです。
https://github.com/lit/lit/issues/1682

ref()

refLitBuilt-in directivesの1つで、DOM要素への参照を取得します。
https://lit.dev/docs/templates/directives/#ref

下のサンプルコードではinput要素への参照を取得しフォーカスさせています。

@customElement('my-element')
class MyElement extends LitElement {

  inputRef: Ref<HTMLInputElement> = createRef();

  render() {
    // Passing ref directive a Ref object that will hold the element in .value
    return html`<input ${ref(this.inputRef)}>`;
  }

  firstUpdated() {
    const input = this.inputRef.value;
    input.focus();
  }
}

補足: ディレクティブとは?

ディレクティブとは、テンプレートのレンダリングをカスタマイズできる関数です。
様々なディレクティブがLitに組み込まれている他、カスタムディレクティブを作ることもできます。
https://lit.dev/docs/templates/custom-directives/

import { Directive, directive } from 'lit/directive.js';

// Define directive
class HelloDirective extends Directive {
  render() {
    return `Hello!`;
  }
}
// Create the directive function
const hello = directive(HelloDirective);

// Use directive
const template = html`<div>${hello()}</div>`;

@state

これはInternal reactive stateを定義するためのデコレーターです。
@stateを使って定義されたプロパティはコンポーネントの外部から参照されず、コンポーネントのアトリビュートとして利用することはできません。

export class MyElement extends LitElement {

  // Private. Doesn't have an attribute. 
  @state()
  protected _active = false;

  // Public. May have an assosiated attribute.
  @property()
  name: string;

}

また、デコレーターを使わなくてもInternal reactive stateを定義できます。

static get properties() {
  return {
    _active: { state: true }
  }
}

constructor() {
  this._active = false;
}

後方互換性

LitLitElementlit-htmlで書かれたほとんどのコードが動作するように設計されています。
APIのリネームやマイナーなBreaking Changeはありますが、大抵の場合はライブラリを置き換えるだけで問題ないでしょう。

1つ注意が必要な点は、IE11対応のためのPolyfillが別パッケージ(lit/polyfill-support.js)として切り離されたことでしょう。LitElementでは本体に組み込まれていましたが、LitではPolyfillを別途インポートする必要があります。(公式ドキュメントのサンプルコードではplatform-support.jsとなっていますがpolyfill-support.jsが正しいです。)

<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js">
<script src="node_modules/lit/polyfill-support.js">

より詳しい変更点についてはアップグレードガイドを参照してください。
https://lit.dev/docs/releases/upgrade/

lit-labs

今回のリリースの重要なポイントの1つとしてこのlit-labsがあります。
https://github.com/lit/lit/tree/main/packages/labs

lit-labsLitの実験的なアイデアや機能を試す場所で、コミュニティにそれらを公開しフィードバックをもらうことで強力なエコシステムを構築するとともにコアの機能をより良くしていくことを目的にしているとローンチイベントで紹介されていました。

現在リポジトリにあるパッケージの中から注目度の高いものを2つ紹介します。

lit-labs/react

これはReactのコンポーネントとLitを接続するためのパッケージで、createComponentuseControllerの2つの関数が用意されています。
実装は https://github.com/lit/lit/tree/main/packages/labs/react

createComponent

ReactでWeb Componentsを利用する際、Custom ElementsのプロパティとReactのpropsの接続が難しいという問題がありましたが、Custom ElementsをcreateComponentでラップすることでpropsやイベントを簡単に接続できます。

createComponentを使うときは、第1引数はReactモジュール、第2引数にCustom Elementsのタグ名、第3引数にCustom Elementsのクラス名(customElements.defineで使用しているもの)を渡します。

第4引数はこのコンポーネントが受け取ることのできるイベントをリストアップしたオブジェクトで、オブジェクトのキーはReactのpropsで渡されるイベントプロパティ名、オブジェクトの値はそれに対応するCustom Elementsで生成されるイベントの名前です。
下記の例では、MyElementComponentonactivateを介してイベント関数が渡され、Custom Elementsのactivateイベントが発生したときにその関数が呼び出されます。

import * as React from 'react';
import {createComponent} from '@lit-labs/react';
import {MyElement} from './my-element.ts';

export const MyElementComponent = createComponent(
  React,
  'my-element',
  MyElement,
  {
    onactivate: 'activate',
    onchange: 'change',
  }
);

定義したコンポーネントは他のReactコンポーネントと同じように利用できます。

<MyElementComponent
  active={isActive}
  onactivate={(e) => (isActive = e.active)}
/>

useController

useControllerLitReactive ControllerをReactコンポーネント中で利用できるようにするためのReactフックです。
詳しいことはリポジトリのREADMEを読んでください。:pray:

import * as React from 'react';
import {useController} from '@lit-labs/react/use-controller.js';
import {MouseController} from '@example/mouse-controller';

// Write a React hook function:
const useMouse = () => {
  // Use useController to create and store a controller instance:
  const controller = useController(React, (host) => new MouseController(host));
  // return the controller: return controller;
  // or return a custom object for a more React-idiomatic API:
  return controller.position;
};

// Now use the new hook in a React component:
const Component = (props) => {
  const mousePosition = useMouse();
  return (
    <pre>
      x: {mousePosition.x}
      y: {mousePosition.y}
    </pre>
  );
};

lit-labs/ssr

これはLitのテンプレートやコンポーネントをSSRするためのパッケージです。
Next.jsやNuxt.jsなどを中心にSSRが普及した現在、Web ComponentsをSSRしたいという需要も高まっており、それに応えるかたちになったと言えます。

https://github.com/lit/lit/tree/main/packages/labs/ssr

まだプレリリースの段階だとREADMEには記載されていますが、ローンチイベントではEleventyのプラグインとして利用しマークダウン内に埋め込まれたCustom ElementsをレンダリングするデモやKoaのミドルウェアで利用するデモが披露されていました。

LitのSSRはDeclarative Shadow DOMという仕様を使って実現されています。この仕様は2021年4月現在Chromeでのみ実装されており、他のブラウザではPolyfillを使ってSSRを実現するようです。
Declarative Shadow DOMについてはこちらの記事で詳しく解説されています。
https://web.dev/declarative-shadow-dom/

まとめ

Litの登場でますますWeb Componentsの利用が広まっていく予感がします。
まずはPlaygroundでいろいろ触ってみましょう!
https://lit.dev/playground/

Discussion