WebComponentsとLitElementについての話

こんにちは。サービス開発統括部 エンジニアリング部のYuto SAGAWAです。*1
最近、自分が取り組んでいるコンポーネントの実装や作成について紹介します。

概要

Webフロントエンド開発において、利用されているライブラリやフレームワークは数多く存在します。 移り変わりも早く日々進化しているように思えましたが、最近では落ち着いてきたように感じています。 そんなフロントエンド開発ですが、コンポーネントの粒度や変更に強いコンポーネントの実装など コンポーネント作成に関して課題があるように感じています。 本記事では、変更に強いコンポーネントを作れるように、 WebComponentsLitElement の紹介と、それらを使ったコンポーネント作成をしてみたいと思います。

WebComponentsとは

WebComponents とは、WebブラウザのAPI群で、下記のような特徴があります。

  • StyleやScriptを含みHTMLの要素をカプセル化できる
  • 再利用可能なパーツを作れる
  • ライブラリなしでコンポーネントが作れる
  • VirtualDOMではなくWeb標準
  • HTMLElementを拡張する

WebComponentsサンプル

例として、WebComponents<button> をカスタマイズして <my-button> を作成してみます。

class MyButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.innerHTML = `
      <style>
        button {
          border-radius: 3px;
          padding: 16px;
        }
      </style>
      <button type="button">
        <slot name="label" />
      </button>
    `;
  }
}

customElements.define("my-button", MyButton);

HTMLElement にStyleを適用し customElements として定義します。

これで WebComponents の作成は完了です。使う側で、作成した my-button をimportして使用します。

import MyButton from 'my-button'

/*
または
<script type="module" src="my-button.js" />
*/

<my-button>
  <span slot="label">My Button</span>
</my-button>

実行結果が下記になります。

ライブラリを使わずにWeb標準でコンポーネントを実装することができました。

Web標準でのコンポーネント

Web標準を使うことで、ReactやVue.jsなどをはじめ、BootstrapやBULUMAなどのCSSライブラリなども含めて フレームワーク論争から脱却することが期待できます。

...と期待はできますが、Web標準を利用することでの辛さももちろん存在します。

  • Dom APIベース
  • 差分更新がない
  • レンダリングパフォーマンスが低い
  • IE11対応じゃない

Dom APIベースであるため、覚えにくいし、読みづらい印象を受けます。 また、ReactやVue.jsにあるような差分更新がないため、レンダリングパフォーマンスが低下します。

Polymer Projectの誕生

GoogleのChrome開発チームによってPolymer Projectが発足されました。 Polymer Projectのミッションは「Webをより良くすること」。

About the Polymer Project As front-end engineers in the Chrome team, out mission is to make the web better.

refs: https://www.polymer-project.org/

Polymer Libraries

Polymer Projectとして、現在3つのライブラリが公開されています。 それぞれ簡単に特徴を見ていきます。

Polymer

Polymerは WebComponents におけるpolyfillの役割を果たし、さらに下記のような特徴があります。

  • WebComponents Sugaring
  • 双方向データバインディング
  • Material Designを採用
  • YouTube, Google Earth, Netflix などで利用されている

また、将来的には後述するLitElementが主流になりそうです。

refs: https://www.polymer-project.org/blog/2018-05-02-roadmap-faq#polymer-3.0-or-litelement

lit-html

テンプレートからDOMの生成及び更新する役割を果たし、さらに下記のような特徴があります。

  • TypeScriptで書かれている
  • 単方向バインディング

LitElement

LitElementWebComponents のためのベースクラスで、下記のような特徴があります。

  • TypeScriptで書かれている
  • レンダリングにlit-htmlを使用
  • 素早くWebComponentsを開発できる

LitElementについて

LitElement についてさらに見ていきます。 LitElementは前述したように、素早く WebComponents を開発するためのライブラリです。

LitElement Starter

TS, JSそれぞれに対応した LitElement Starter があります。

コンポーネントカタログ

LitElement Starter には Storybook のようなコンポーネントカタログが含まれています。

  • Markdownで記述できる
  • Layoutも変更できる

LitElement Sample

簡単なCounterコンポーネントを実装します。

import { LitElement, html, customElement, property, css } from 'lit-element';

@customElement('my-element')
export class MyElement extends LitElement {
  static styles = css`
    :host {
      display: block;
      padding: 16px;
      border: 1px solid black;
      width: 400px;
    }
  `;

  @property({type: Number})
  count = 0;

  render() {
    return html`
      <h1>Counter</h1>
      <h2>${this.count}</h2>
      <button @click=${() => {this._onClick(-1)}} part="button">
        -
      </button>
      <button @click=${() => {this._onClick(1)}} part="button">
        +
      </button>
    `;
  }

  private _onClick(num: number) {
    this.count += num;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'my-element': MyElement;
  }
}

カウントの追加と削減を機能を持ち、カウント数を表示するコンポーネントを my-element として定義します。

使う側で、作成した my-element をimportして使用します。

import { my-element } from 'my-element;

<my-element />

実行結果が下記になります。

LitElement Docs Sample

LitElement Starter のコンポーネントカタログを利用して、コンポーネントのドキュメントを作成します。 デフォルトでは下記のようなドキュメントが作成されます。

LitElementの使い所

LitElementWebComponents 標準に準拠しているので、共通のコンポーネントとして力を発揮します。 任意のフレームワークで動作するため、ReactやVue.jsなどを使用しているプロジェクトにも導入することができます。

React

import { my-element } from 'my-element;

export const Counter = () => {
  return <my-element />
}

Vue.js

<template>
  <my-element />
</template>

<script>
import { my-element } from 'my-element;

export default {
  comopnents: {
    MyElement
  }
}
</script>

プロジェクトの垣根を超えて使われるような、汎用的な処理などをカプセル化に含めてしまい、 WebComponents として作っておくのもいいかもしれません。

LitElementの辛いところ

  • 双方向でのデータのやり取りが複雑
  • クリックなどのイベントハンドリングを使う側で設定する必要がある

Reactの公式ドキュメントにも WebComponents についての記述があります。

Events emitted by a Web Component may not properly propagate through a React render tree. You will need to manually attach event handlers to handle these events within your React components.

refs: https://reactjs.org/docs/web-components.html

最後に

ここまで執筆しましたが、WebComponentsLitElement で本当に変更に強いコンポーネントが作れたとは言い切れません。本当に変更に強いコンポーネント開発の旅はまだまだ続きそうです。

現在、私達サービス開発統括部では、複数のサービスが存在しますが、 Vue.jsで書かれているプロジェクトだけではなく、Reactで書かれているプロジェクトもあります。 さらに、将来的には「24サービスの同時開発できる組織」を目指しています。 そのためにも、使い回せる汎用的なコンポーネントを作ることができれば開発速度もあがるかもしれません。 複数サービスがあり、異なったフレームワークやライブラリを使っているチームには汎用的なコンポーネントを WebComponentsとして作ってみてはいかがでしょうか。

おまけ

その他 WebComponents ライブラリ

Yuto SAGAWA

Yuto SAGAWAさんのプロフィール

サービス企画開発本部 サービス開発統括部 エンジニアリング部 サービスアーキテクトグループ

現在は退職

※2020年7月現在の情報です。

*1:※Yuto SAGAWAは退職していますが、本人の同意を得て掲載を継続しています。