WebSocketではないTurbo Streamsのsourceを作って遊ぶ

2021/01/17に公開

2020年の年末にBasecampからHotwireが発表されました。

https://twitter.com/dhh/status/1341420143239450624

そのなかのTurbo Streamsですが、sourceはWebSocketである必要はなく、自分でsourceを作ることもできます。以下では簡単なsourceを作って遊んでみます。

以下で使っているバージョンは@hotwired/turbo7.0.0-beta.3です。

Turbo Streamsが要求していることは何なのか

特にドキュメントで明示されているわけではないですが、ActionCableをsourceとして利用しているturobo-railsのTurboCableStreamSourceElementが参考になります。

sourceは@hotwired/turboconnectStreamSourceで利用することができますが、きちんと型の情報も定義されています。

connectStreamSource(source: StreamSource): void;

StreamSourceはといういと、

type StreamSource = {
  addEventListener(type: "message", listener: (event: MessageEvent) => void, options?: boolean | AddEventListenerOptions): void
  removeEventListener(type: "message", listener: (event: MessageEvent) => void, options?: boolean | EventListenerOptions): void
}

MessageEventをdispatchするEventTargetのようなものであることを要求しているようです。

MessageEventの中身としてはdataturbo-streamを入れるとよさそうです。turbo-streamはほぼHTMLのようなものです。

時刻を更新するだけのsourceを作ってみる。

では実際に作っていきましょう。
div#timerを作って、それを毎秒更新するためのsourceを作ってみます。

EventTarget的なものを作ってもいいですが、turbo-railsを参考にcustom elementsを作ってサボります。

Turbo Streamsのactionにはappend, prepend, replace, removeありますが、ここではreplaceを使って、ただ書き換えるだけにします。なのでMessageEventdataとしては

<turbo-stream action="replace" target="timer">
  <template>
    <div id="timer">
      ${new Date().toISOString()}
    </div>
  </template>
</turbo-stream>

を吐き出します。

以上のことを素直に実装すると、

import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"

class TickElement extends HTMLElement {
  timerId?: number;

  connectedCallback() {
    connectStreamSource(this);
    this.timerId = setInterval(this.dispatchMessageEvent.bind(this), 1000);
  }

  disconnectedCallback() {
    disconnectStreamSource(this);
    if (this.timerId) clearInterval(this.timerId);
  }

  dispatchMessageEvent() {
    const data = `
      <turbo-stream action="replace" target="timer">
        <template>
          <div id="timer">
            ${new Date().toISOString()}
          </div>
        </template>
      </turbo-stream>
    `
    const event = new MessageEvent("message", { data });
    this.dispatchEvent(event);
  }
}

customElements.define("tick-source", TickElement);

といった感じになります。これをHTMLから

<div id="timer"></div>
<tick-source />

と利用すれば…

実際の動き

自分で作ったsourceで更新することができました!

最後に

一応成果物は
https://github.com/en30/hotwire-stream-source-playground
にあります。

Hotwireの旨みはサーバサイドのテンプレートを使い回すことで楽ができることなので、今回作ったsourceは実用的とは言えません。しかしsourceを書きさえすれば、Server Sent EventsでもWeb Pushでも、ポーリングだろうとTurbo Streamsで利用することができるということです。意外と簡単に書けるので色々やりようがありそうですね。

Discussion