Web Componentsで簡単にSSRを実装する方法

ハイサイ！オースティンやいびーん。

# 概要

最近のWeb開発では、クライアントサイドで実行するはずのJavaScriptを、サーバーで予め実行してその結果としてレンダーされるHTMLをクライアントに送るという作法が流行っています。

この作法のことをSSR（Server Side Rendering）と言います。

残念ながら、サーバー側でDOMを生成して予めブラウザと同様な環境を用意してHTMLまでレンダーするという作業が難しく、しかも基本的にJavaScript系のバックエンドフレームワークでないと不可能です。

強い機能性を持ったJavaScript部品も使いたい、SSRも使いたい、しかしPHPを使っている、そういうあなたにはWeb Componentsの**Declarative Shadow DOM**が味方になります。

## そもそもShadow DOMとは

Web Componentsには基本的に三つの重要な機能があります：

1. Custom Elements (`<my-element>`)
1. Shadow DOM
1. HTML templates (`<slot>`)

その中の一つのShadow DOMですが、これは通常のDOM（Light DOM）と独立した、Light DOMから影響を受けないDOMをそれぞれのWeb Componentが持てるDOMのことです。

Light DOMから影響を受けないというのは、つまり、CSSとJavaScriptから影響を受けないということです。

単独で予測できるようなスタイルを適応させたい時にとても便利なShadow DOM。CSSのカオスから守ってくれるShadow DOMです。

# Declarative Shadow DOMとは

そのShadow DOMはとても便利なのですが、ひっかけ問題があります；従来、**JavaScriptでしかこのShadow DOMを作れなかったのです**。

つまり、Webページを読み込んでそれがレンダーされてからやっとJavaScriptが実行され、Shadow DOMが生成されます。結果、画面がちかっとします。


https://tronicboy1.github.io/wc-virtual-keyboard/

上記のWebサイトを遅い通信環境で開くと、チカっとなるのがわかります。

しかも、React等と同様にクライアントでしかレンダーされないのでSEO的に問題があります。

そこで登場したのは、**Declarative Shadow DOM**です。

Declarative Shadow DOMは、JavaScriptではなく、HTMLの中でブラウザにShadow DOMを作るように指示する構文のことです。つまり、HTMLタグだけで`this.attachShadow`ができるのです。また、含めたいスタイルシートも簡単んいそこに含めることで、独立したDOMでも適応させたCSSだけを部分的に効かせることができます。

# Declarative Shadow DOMの使い方

Declarative Shadow DOMを使うのはとても簡単です。Custom Elementのタグの中で`<template shadowrootmode="open"></template>`を使います。

```html
    <my-comment-component>
      <template shadowrootmode="open">
        <link rel="stylesheet" href="style.css">
        <h2>Comment From User</h2>
        <p>This product is great. I use it all the time.</p>
      </template>
    </my-comment-component>
```

これだけで見てみると、`customElements.define`を呼んでCustom Elementの定義をした上で`attachShadow`を`connectedCallback`で呼ばなくても、ブラウザが勝手にShadow DOMを作ってくれることがわかります。

![Screenshot 2023-07-26 at 7.55.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/b21c3347-def5-9cc4-3c10-0c4c81060ee9.png)

さらに、JavaScriptでCustom Elementを定義してみても、きちんと`shadowRoot`が、`connectedCallback`時に用意されていることがわかります。

```html
  <body>
    <my-comment-component>
      <template shadowrootmode="open">
        <link rel="stylesheet" href="style.css">
        <h2>Comment From User</h2>
        <p>This product is great. I use it all the time.</p>
      </template>
    </my-comment-component>
  </body>

  <script>
    class MyCommentComponent extends HTMLElement {
      connectedCallback() {
        console.log("Shadow Root: ", this.shadowRoot);

        const h = this.shadowRoot.querySelector('h2');
        h.style.color = 'red';
      }
    }

    customElements.define("my-comment-component", MyCommentComponent);
  </script>
```


![Screenshot 2023-07-26 at 8.00.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/47c1a4ef-a8b2-ffdb-c3d0-744feb65ee4a.png)





これのおかげで、スタイルが適応されているコンテンツがWeb Componentでも最初からブラウザで表示されている上、チカっとなることもなく、また、SEO的にも見えているわけなので、Web ComponentsとShadow DOMの便利な機能を使いつつ、何のデメリットもないのです。

# PHPなどでも使えます。

非JavaScript系のバックエンド言語でもこの技術を生かしてSSRを実装できます。例えば、PHPだと以下のように簡単に組み込めます。

```php
<?php

$post_id = $_GET['post-id'];
$comment = get_comment($post_id);

?>

<my-comment-component>
  <template shadowrootmode="open">
    <link rel="stylesheet" href="<?php echo get_comment_stylesheet_uri(); ?>">
    <div id="articles">
      <article>
        <h2><?php echo $comment['title']; ?></h2>
        <p><?php echo $comment['body']; ?></p>
      </article>
    </div>
    <button>Load More Comments</button>
  </template>
</my-comment-component>
<script type="module" defer src="<?php echo get_my_comment_component_script(); ?>"></script>
```

そして、JavaScriptで以下のように`Load More Comments`にイベントリスナーをつけてさらに表示することができます。

```javascript
class MyCommentComponent extends HTMLElement {
  loadMoreButton = /** @type {HTMLButtonElement} */ (this.shadowRoot.querySelector("button"));
  articles = /** @type {HTMLElement} */ (this.shadowRoot.querySelector("#articles"));
  connectedCallback() {
    this.loadMoreButton.addEventListener('click', this.handleLoadMoreClick);
  }

  disconnectedCallback() {
    this.loadMoreButton.removeEventListener('click', this.handleLoadMoreClick);
  }

  handleLoadMoreClick = async () => {
    const comments = await this.fetchMoreComments();
    const commentEls = this.renderMoreComments(comments);
    this.articles.append(...commentEls);
  };

  fetchMoreComments() {
    return fetch("...").then((res) => res.json());
  }

  /**
   * @param comments {any[]}
   */
  renderMoreComments(comments) {
    const commentElements = comments.map((comment) => {
      const article = document.createElement("article");
      const h2 = document.createElement("h2");
      const p = document.createElement("p");

      h2.textContent = comment.title;
      p.textContent = comment.body;

      article.append(h2, p);

      return article;
    });
  }
}

customElements.define("my-comment-component", MyCommentComponent);
```

こうしてとても簡単にSSRとWeb Componentsをどの言語でも使うことができるのです！

# まとめ

Declarative Shadow DOMを使ってWeb ComponentsでSSRをJavaScriptじゃないバックエンド言語でも実現する方法を紹介しましたが、いかがでしょうか？

筆者は最近、WordPress開発でよくこの技術を使っています。Shadow DOMがいらないのなら、簡単にLight DOMに全部入れると、JavaScriptをWeb Componentsで管理することができるから便利です。