😗

WebComponentsを「外部ライブラリなし」or「lit-html」or「lit-element」で実装して比較

2021/02/20に公開1

フレームワークに依存せずに、Web標準の技術で再利用可能なコンポーネントをつくれるWebComponents。

今回、WebComponentsを以下の3つの方法で実装し、比較してみました。

  1. 外部ライブラリを何も使わずに実装する
  2. lit-htmlを使用して実装する
  3. lit-elementを使用して実装する

実装するのは

<custom-element message1="hello" message2="world"></custom-element>

とかくと、

<div>
  <div>hello</div>
  <div>world</div>
</div>

が展開される、超シンプルなコンポーネントです。

1. 外部ライブラリを何も使わずに実装する

export class CustomElement extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: "open" })
    this.render()
  }
  static get observedAttributes() {
    return ['message1', 'message2']
  }
  attributeChangedCallback(name, oldValue, newValue) {
    this.render()
  }
  render() {
    const message1 = this.getAttribute('message1')
    const message2 = this.getAttribute('message2')
    this.shadowRoot.innerHTML = `<div><div>${message1}</div><div>${message2}</div></div>`
  }
}

このとき、renderingがどう行われるのか確認してみましょう。

3秒後に message 属性の値を変更してみます。

import { CustomElement } from './CustomElement'
customElements.define('custom-element', CustomElement)

const customElement = document.getElementsByTagName('custom-element')[0]
console.log(customElement)
setTimeout(() => {
  customElement.setAttribute('message1', 'hoge')
}, 3000)

当たり前ですが、shadowRoot全体が再描画されていますね。

message1が変更されたときはmessage1のみ変更したいですが、それを自前で実装するのはちょっと面倒です。

2. lit-htmlを使用する

ここでlit-html(HTML template library)を使って実装してみます。

lit-html

import { html, render } from 'lit-html' // 追加

export class CustomElement extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({ mode: "open" })
    this.render()
  }
  static get observedAttributes() {
    return ['message1', 'message2']
  }
  attributeChangedCallback(name, oldValue, newValue) {
    this.render()
  }
  render() {
    const message1 = this.getAttribute('message1')
    const message2 = this.getAttribute('message2')
    render(html`<div><div>${message1}</div><div>${message2}</div></div>`, this.shadowRoot) // 変更
  }
}

先程と違い、message1のみ再描画されます。

(なお、lit-htmlは仮想DOMを使用していません。DOM tree全体を比較するようなアルゴリズムでは動いていないようです)

参考

3. lit-elementを使用する

最後に、WebComponentsを楽に実装できるベースクラスを提供している「lit-element」を利用して書き直してみます。

lit-element

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

export class CustomElement extends LitElement {
  constructor() {
    super()
    this.message1 = 'hello'
    this.message2 = 'world'
  }
  static get properties() {
    return {
      message1: { type: String },
      message2: { type: String }
    };
  }
  render() {
    return html`<div><div>${this.message1}</div><div>${this.message2}</div></div>`
  }
}

attachShadow, getAttributeなどHTMLElementが持っているpropertyの記述が消えました。

かなり直感的に理解しやすいコードになったと思います。

lit-elementを使用しないときは、監視するpropertyを「observedAttributes」で定義し、「attributeChangedCallback」でrenderを実行することにより、変数の変更とviewの変更をbindingしていました。

lit-elementを使用すると、上記のように書けば実現できます。

(変更を監視したくない場合は、myProp: { attribute: false } のように指定すればOK)

自動で変更を監視&viewの更新をしてくれるのはかなり楽ですが、

気をつけないと更新タイミングが追いづらくなることもありそうなので、LitElement自体の理解が必要になってきそうです。

まとめ

簡単に部分描画を実現したい場合、lit-htmlはとても便利。

lit-elementを利用すると、より記述量を減らし、直感的にWebComponentsを実装できる。

ただし、更新タイミングが追いづらくならないように注意が必要。

Discussion

カザオキ狩野カザオキ狩野

カスタム要素での WebComponent を作ろうと色々調べてて、この記事を拝見しました。自分でフルスクラッチするかフレームワーク使うか悩んでいたのでとても参考になりました!
ありがとうございます。

尚、現行の LitElement + TypeScript で書くと上記スクリプトはもっと簡潔になるようでしたので、ご参考まで。😉

custom-element.ts
import { LitElement, html, customElement, property } from 'lit-element'

@customElement('custom-element')
export class CustomElement extends LitElement {

    @property() // 監視したくない場合は @property({attribute: false})
    message1!: string;

    @property()
    message2!: string;

    render(){
        return html `
            <div>
              <div>${this.message1}</div>
              <div>${this.message2}</div>
            </div>
        `
    }
}

ポイントは「@~」で始まる "デコレーター" ってやつですね。分かりやすくてとってもスッキリ!(デコレーター自体はまだ実験的な機能のようですが・・)