HTMXを試してみたいのでWebSocketでSymbolの新規ブロックをリアルタイムで表示してみる

# はじめに

HTMXというものがあることを知りました。

https://htmx.org/

最近急に有名になっているそうで、AjaxをJavaScriptなしで直接HTMLに記述できるものみたいです。

そこで、勉強がてら、Symbolブロックチェーンと無理やり絡めて何かやってみようと思いました。

ネタを何にしようと考えましたが、HTMXはWebSocketも対応しているみたいなので、あまり使われていなそうなSymbolのRESTゲートウェイのWebSocketを使って、新規ブロックのリアルタイム表示なんかをやってみたいと思います。

結論から先に言いますが、HTMXだけでSymbolの新規ブロックをリアルタイム表示をすることはできませんでした。

# インストール

今回はNode.jsなどを使用せず、HTMLを直接編集する形をとります。
その場合は、HTMLに以下の一行を追加するだけです。

```html
<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
```

https://htmx.org/docs/#installing

HTMLはこうなります。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.js"></script>
</body>
</html>
```

# Ajaxでノードの情報を取得してみる

HTMXでAjaxを行うには、以下のように記述すればいいです。

```html
<div hx-get="<url>">
    Click
</div>
```

こうすると、「Click」と表示されたところをクリックすると、`<url>`にリクエストが送信され、その結果が「Click」と置き換えられます。

つまり、`<url>`をRESTゲートウェイの`/node/info`にすれば、結果が表示されます。

ではやってみましょう。HTMLはこうなります。

```diff_html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
+   <div hx-get="https://sym-main-03.opening-line.jp:3001/node/info">
+       Click
+   </div>
    <script src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.js"></script>
</body>
</html>
```

ではブラウザを見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/8b59fbfb-8498-1d5d-072c-29ec06418f67.png)

「Click」と書かれたところをクリックしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/e512e327-9ebf-03ed-70f6-37c881297271.png)

エラーが発生しました。

> Access to XMLHttpRequest at 'https://sym-main-03.opening-line.jp:3001/node/info' from origin 'http://localhost:63342' has been blocked by CORS policy: Request header field hx-request is not allowed by Access-Control-Allow-Headers in preflight response.

`/node/info`にアクセスしようとしたが、クロスオリジン環境で、`hx-request`というヘッダーは許可されていないため通信をブロックしたというエラーです。

どうやら、HTMXではリクエストの際にそういったヘッダーを自動的に付加しているようです。

これを解消するには、どうにかしてレスポンスヘッダーに`Access-Control-Allow-Headers`をつけなければなりません。

この記事の趣旨ではないので省略しますが、CloudFrontを使って解決しました。ブラウザとRESTゲートウェイの間にもう一段CloudFrontを挟んで、レスポンスにヘッダーを付与するよう設定しました。

さて、URLをCloudFrontのものに変更します。

```html
<div hx-get="https://d2jbka2k4ggxp7.cloudfront.net:443/node/info">
    Click
</div>
```

再度ブラウザでやってみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/8b59fbfb-8498-1d5d-072c-29ec06418f67.png)

「Click」と書かれたところをクリックしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/ccf6080e-3260-5a2f-0227-b57b02cfaebd.png)

レスポンスが表示されました！

ここでもうひとネタを。ノード情報を表示するだけなら、わざわざクリックをしなくても表示して欲しいです。そこで、`hx-trigger="load"`という属性を追加します。

```html
<div
    hx-get="https://d2jbka2k4ggxp7.cloudfront.net:443/node/info"
    hx-trigger="load"
></div>
```

こうすることで、HTMLが読み込まれた段階でAjaxを開始することができます。

# JSONのレスポンスをHTMLに変換する

レスポンスが表示されたのはいいのですが、JSONがそのまま表示されています。

これではとても読みにくいので、HTMLとして処理したいところです。

そこで、HTMXの拡張機能に`client-side-templates`というものがあったので利用してみます。

https://htmx.org/extensions/client-side-templates/

これは、JSONやXMLをHTMLとして表現するための機能です。その方法としていくつかのテンプレートエンジンが用意されていますが、今回は`mustache`を利用します。

まずはscriptタグで拡張機能を追加します。

```html
    <script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
    <script src="https://unpkg.com/mustache@latest"></script>
````

そして、`hx-ext="client-side-templates"`属性のついたタグを用意し、その中にAjaxとテンプレートを記述します。

```html
<div hx-ext="client-side-templates">
    この中にAjaxとテンプレートを記述
</div>
```

例えばこのようになります。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div hx-ext="client-side-templates">
        <div
            hx-get="https://d2jbka2k4ggxp7.cloudfront.net:443/node/info"
            hx-trigger="load"
            mustache-template="node-info"
        ></div>
        <template id="node-info">
            <div class="grid-container">
                <div>networkGenerationHashSeed</div>
                <div>{{ networkGenerationHashSeed }}</div>
                <div>networkIdentifier</div>
                <div>{{ networkIdentifier }}</div>
                <div>port</div>
                <div>{{ port }}</div>
                <div>roles</div>
                <div>{{ roles }}</div>
                <div>version</div>
                <div>{{ version }}</div>
            </div>
        </template>
    </div>

    <script src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.js"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
    <script src="https://unpkg.com/mustache@latest"></script>
</body>
</html>
```

実際にはCSSが書いてあるのですが、長くなるのでこの記事の最後に表示しています。

それではブラウザを見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/7d1f1095-7cfc-ff90-87ed-72f3aa5f42d7.png)

テンプレートに書いた形で表示されました。

# WebSocketで新規ブロックを取得する

さて次のステップに進みましょう。WebSocketです。

RESTゲートウェイのWebSocketのドキュメントどこだっけな…

見つけられなかったのでSymbolエクスプローラーを参考にします。

https://symbol.fyi

F12開発者ツールでトップページを見てみましょう。

新規ブロック情報の受信は以下の流れで行われているようです。

1. アクセスするとすぐにWebSocketが接続され、`{"uid":"一意のID"}`という情報を受信しています。
1. その後、ブラウザから`{"uid":"一意のID","subscribe":"block"}`みたいな情報を送信しています（赤線）。
1. そしてそこから新規ブロックができる度に情報を受信している感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/aa4c634f-81c5-63a0-00df-5f06602ac6bf.png)

まずはWebSocketに接続してみましょう。そのためにはまず、以下のようにWebSocketの拡張機能を追加します。

```html
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
```

そして次のようなHTMLを追加します。

```html
<div hx-ext="ws" ws-connect="<url>">
</div>
```

こうすると自動的に接続してくれます。接続すると`uid`を受信します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/1cebbd06-d671-9a5f-1fae-30efc366481e.png)

接続ができたので、次は`subscribe`と`uid`の情報を送信したいです。そこで、WebSocketで送信を行うには、`ws-send`属性をつけた`form`を利用します。

https://htmx.org/extensions/web-sockets/#sending-messages-to-a-websocket

例えば、次のようにします。これで、`subscribe`と`uid`をキーに、インプットフィールドを値にしたJSONを送信することができるようになります。

```html
<div hx-ext="ws" ws-connect="<url>">
    <form id="form" ws-send>
        <input name="subscribe">
        <input name="uid">
    </form>
</div>
```

ひと工夫。`subscribe`の値は固定で`block`なので、次のようにします。

```diff_html
<div hx-ext="ws" ws-connect="<url>">
    <form id="form" ws-send>
-       <input name="subscribe">
+       <input name="subscribe" value="block" hidden>
        <input name="uid">
    </form>
</div>
```

さらにひと工夫。ボタンクリックで送信できるように、次のようにします。

```diff_html
<div hx-ext="ws" ws-connect="<url>">
-   <form id="form" ws-send>
+   <form id="form" ws-send hx-trigger="submit">
        <input name="subscribe" value="block" hidden>
        <input name="uid">
+       <button type="submit">submit</button>
    </form>
</div>
```

というわけで、以下のようになりました。


```html
    <div hx-ext="ws" ws-connect="wss://d2jbka2k4ggxp7.cloudfront.net:443/ws">
        <form id="form" ws-send hx-trigger="submit">
            <input name="subscribe" value="block" hidden>
            <input name="uid">
            <button type="submit">submit</button>
        </form>
    </div>

    
    <script src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.js"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
    <script src="https://unpkg.com/mustache@latest"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
```

ではブラウザに移りましょう。まずWebSocketに接続し、`uid`のメッセージを受信します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/1cebbd06-d671-9a5f-1fae-30efc366481e.png)

この`uid`をコピペして、インプットフィールドに入れて、ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/197bda34-3a28-f858-2065-695ca9b10243.png)

しばらくすると、新規ブロックの情報が送られてきました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/ead26308-7a48-41d7-f092-3e65d3ac6381.png)

しかし、毎回F12開発者ツールを開いて`uid`をコピペするのは面倒です。なのでここでちょっとJavaScriptを持ち出して、新規ブロック情報を取得するまでのやりとりを自動化してみます。

```html

    <div hx-ext="ws" ws-connect="wss://d2jbka2k4ggxp7.cloudfront.net:443/ws">
    </div>

    <script src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.js"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
    <script src="https://unpkg.com/mustache@latest"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
    <script>
        document.body.addEventListener('htmx:wsBeforeMessage', function (event) {
            try {
                const message = JSON.parse(event.detail.message)
                if (message.uid) {
                    event.detail.socketWrapper.send(JSON.stringify({
                        uid: message.uid,
                        subscribe: 'block'
                    }), event.detail.elt);
                }
            } catch (e) {
                console.error(e);
            }
        });
    </script>
```

# 新規ブロックの情報をHTMLにしたいがJavaScriptを書かないと無理なようだ

さて、新規ブロックの情報をJSON形式で受け取ることができました。

さて、これをHTMLにしようと調べてみましたが、どうも現状そのような機能はないみたいです。

以下の記述にあるように、受信したメッセージはHTMLとしてパースされる作りになっているみたいです。なので、WebSocketから受信する情報をHTMLにしないとどうにもなりませんでした。

> Content that is sent down from the websocket will be parsed as HTML and swapped in by the id property, using the same logic as Out of Band Swaps.

https://htmx.org/extensions/web-sockets/#receiving-messages-from-a-websocket

JavaScriptを使えば、以下のようにしてWebSocketのメッセージを取得することができます。

```diff_html

    <div hx-ext="ws" ws-connect="wss://d2jbka2k4ggxp7.cloudfront.net:443/ws">
    </div>

    <script src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.js"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
    <script src="https://unpkg.com/mustache@latest"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
    <script>
        document.body.addEventListener('htmx:wsBeforeMessage', function (event) {
            try {
                const message = JSON.parse(event.detail.message)
                if (message.uid) {
                    event.detail.socketWrapper.send(JSON.stringify({
                        uid: message.uid,
                        subscribe: 'block'
                    }), event.detail.elt);
-               }
+               } else if (message.topic === 'block') {
+                   console.log(message);
+               }
            } catch (e) {
                console.error(e);
            }
        });
    </script>
```

こうすると、F12開発者ツールのConsoleには以下のように新規ブロック情報が次々と表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/0ca86359-b9ae-2a72-b044-eea2c3f48437.png)

JavaScriptで扱えるとなれば、あとはどうにでもすることができると思います。しかし、今回はHTMXを使っていくことが趣旨なので、いったんここでお開きとしたいと思います。

# おわりに

HTMXを使ってSymbolブロックチェーンの新規ブロック情報をWebSocketで受信することをやってみました。目指すものはできなかったものの、HTMXについて勉強することができたかなと思います。もしブロック情報がHTMLとして表示できたなら、トランジションとかもやってみたかったのですが、それはまた別の機会にネタを考えてやってみたいと思いました。

# 最終的なHTML

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .grid-container {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 10px; /* セル間の間隔を調整 */
            font-size: 16px; /* フォントサイズを調整 */
            padding: 10px; /* パディングを追加 */
            border: 1px solid #ccc; /* 枠線を追加 */
            border-radius: 5px; /* 角丸を適用 */
            background-color: #f0f0f0; /* 背景色を設定 */
        }

        .grid-container > div {
            padding: 5px; /* セル内のパディングを調整 */
            border-bottom: 1px solid #ccc; /* セルの下部に線を追加 */
            white-space: nowrap; /* テキストを折り返さないように */
            overflow: hidden; /* オーバーフローした部分を非表示にする */
            text-overflow: ellipsis; /* 省略記号を表示 */
        }

        /* 奇数行の背景色を変更して交互に表示 */
        .grid-container > div:nth-child(odd) {
            background-color: #e0e0e0;
        }
    </style>
</head>
<body>
    <div hx-ext="client-side-templates">
        <div
            hx-get="https://d2jbka2k4ggxp7.cloudfront.net:443/node/info"
            hx-trigger="load"
            mustache-template="node-info"
        ></div>
        <template id="node-info">
            <div class="grid-container">
                <div>networkGenerationHashSeed</div>
                <div>{{ networkGenerationHashSeed }}</div>
                <div>networkIdentifier</div>
                <div>{{ networkIdentifier }}</div>
                <div>port</div>
                <div>{{ port }}</div>
                <div>roles</div>
                <div>{{ roles }}</div>
                <div>version</div>
                <div>{{ version }}</div>
            </div>
        </template>
    </div>

    <div hx-ext="ws" ws-connect="wss://d2jbka2k4ggxp7.cloudfront.net:443/ws">
    </div>

    <script src="https://unpkg.com/htmx.org@1.9.10/dist/htmx.js"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
    <script src="https://unpkg.com/mustache@latest"></script>
    <script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
    <script>
        document.body.addEventListener('htmx:wsBeforeMessage', function (event) {
            try {
                const message = JSON.parse(event.detail.message)
                if (message.uid) {
                    event.detail.socketWrapper.send(JSON.stringify({
                        uid: message.uid,
                        subscribe: 'block'
                    }), event.detail.elt);
                } else if (message.topic === 'block') {
                    console.log(message);
                }
            } catch (e) {
                console.error(e);
            }
        });
    </script>
</body>
</html>
```