LoginSignup
3
0

More than 1 year has passed since last update.

【Deno】ReadableStreamでFizz Buzzして無限Fizz Buzzサーバーを建てる

Last updated at Posted at 2022-06-03

ReadableStream(Webストリーム)でFizz Buzzする

ReadableStreamは細切れのデータを送受信できるAPIです。
今回はこのAPIを使ってFizz Buzzしていきたいと思います。

また、Denoを使うとストリーミングするサーバーを簡単に書くことができます。今回はDenoを使って無限FizzBuzzストリーミングサーバーを書いてみます。

(何を言っているかわからない方は https://dash.deno.com/playground/fizzbuzz にアクセスして実例を見てください。)

実装方法

WebストリームAPIには以下の3種類が存在します。

  • ReadableStream - 細切れのデータを読み込み
  • TransformStream - 細切れのデータを変換
  • WritableStream - 細切れのデータを書き込み

これらは、以下のコードのようにメソッドチェーンで組み合わせて使います。

new ReadableStream()
  .pipeThrough(new TransformStream())
  // ...(中略) 
  .pipeThrough(new TransformStream())
  .pipeTo(new WritableStream())

このようにメソッドチェーンできる理由は、.pipeThrough()の返り値がReadableStreamになっているからです。
上のコードが細切れのデータを読み書きする様子を図で表すと、下の画像のようになります。

image.png

図に書いた通り、Webストリームはpullストリームであり、ジェネレーターのように遅延評価されます。
WritableStreamの書き込み準備が整うとバケツリレーのように遡ってReadableStreamにデータが要求され、次にReadableStreamがデータを書き込むとまたバケツリレーされてWritableStreamへデータが届きます。

このことを利用すると、無限のデータを生成するReadableStreamを作ることができます。

無限ストリームの作り方

上記のように、「データを要求された時にデータを書き込む」というやり方で実装します。これをするにはReadableStreamの作成時にpullメソッドを定義します。

new ReadableStream({
  pull(controller) { // データが要求された時の処理
    // ここにデータを送信する処理を書く
  }
})

pullメソッドにはcontrollerという引数が渡されます。このcontrollerを使ってデータを書き込みます。

new ReadableStream({
  pull(controller) { // データが要求された時に
    controller.enqueue(data); // データを書き込み
  }
})

今回はFizzBuzzを送信したいので、FizzBuzzをcontroller.enqueueします。

let i = 0;
const readable = new ReadableStream<string>({
  pull(controller) {
    const val = i % 15 === 0
      ? "FizzBuzz"
      : i % 3 === 0
      ? "Fizz"
      : i % 5 === 0
      ? "Buzz"
      : i;
    controller.enqueue(`${val}`);
    i++;
  },
});

これで「無限にFizzBuzzが流れてくるストリーム」の完成です。

試しにストリームを読んでみましょう。実行にはDeno1.22を使います。
ストリームを読むにはfor-await-of文を使うことができます。

for await (const data of readable) {
  console.log(data);
}

これをdeno run <ファイル名>コマンドで動かすと、無限にFizzBuzzが表示されます。

for-await-of文を使ったやり方は、DenoとNode.jsのみで使えます。ブラウザでは絶賛実装中です。
ブラウザの場合は.getReader()を使います。

const reader = readable.getReader();
while (true) {
  console.log(await reader.read());
}

なお無限に表示されるので、気が済んだらctrl-cで止めましょう。

無限Fizz Buzzサーバー

DenoはNode.jsの後継の新しいサーバーサイドJavaScriptランタイムです。

Denoを使うと、サーバーとブラウザの間でReadableStreamをやり取りすることができます。(DenoとブラウザはどちらもWeb標準APIを使えるため)

実装方法

まずは無限にFizz Buzzが流れてくるサーバーを作ります。
http://localhost:8000/fizzbuzzにアクセスしたときにfizzbuzzをストリーミングするサーバーを書いていきます。

serve.ts
import {
  serve,
  Status,
  STATUS_TEXT,
} from "https://deno.land/std@0.142.0/http/mod.ts";

serve((request) => {
  const { pathname } = new URL(request.url);
  if (pathname === "/fizzbuzz") { // "/fizzbuzz"にアクセスしたら
    // fizzbuzzストリームを作成
    let i = 0;
    const readable = new ReadableStream<string>({
      pull(controller) {
        const val = i % 15 === 0
          ? "FizzBuzz"
          : i % 3 === 0
          ? "Fizz"
          : i % 5 === 0
          ? "Buzz"
          : i;
        controller.enqueue(`${val}`);
        i++;
      },
    });
    // 文字列のストリームをUint8Arrayのストリームに変換
    const uint8ArrayStream = readable.pipeThrough(new TextEncoderStream());
    // Response作成時に1番目の引数にストリームを渡す
    return new Response(uint8ArrayStream);
  }

  // それ以外の場合は404 Not Foundを表示
  return new Response(STATUS_TEXT.get(Status.NotFound), {
    status: Status.NotFound,
  });
});

基本的にはif (pathname === "/fizzbuzz") { return new Reaponse(fizzbuzzストリーム) }という処理の流れになります。

一つ注意点として、レスポンスに渡す前に文字列をUint8Arrayに変換するというポイントがあります。
文字列のストリームをUint8Arrayのストリームに変換するには組み込みで用意されている変換ストリームであるTextEncoderStreamを利用します。
TextEncoderStreamはTransformStreamの一種なので、readable.pipeThrough(new TextEncoderStream())と書くことでパイプすることができます。

この状態でサーバーを起動し、http://localhost:8000/fizzbuzzにアクセスすると、fizzbuzzのデータが(無限に)どんどん流れてくるのがわかると思います。

deno run --allow-net ./serve.ts

image.png

クライアント側でReadableStreamを受け取る

サーバーで送信したReadableStreamを、ブラウザで受け取る処理を書いていきます。

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script type="module">
      // body はUint8Arrayのストリーム
      const { body } = await fetch("/fizzbuzz");
      // Uint8Arrayのストリームを文字列のストリームに変換
      const fizzbuzzStream = body.pipeThrough(new TextDecoderStream());
      // 読み出してconsole.logで表示 
      const reader = fizzbuzzStream.getReader();
      while (true) {
        console.log(await reader.read());
      }
    </script>
  </head>
  <body></body>
</html>

ブラウザでもReadableStreamの基本的な操作は一緒です。

fetch()/fizzbuzzというURLにリクエストすると、Responseオブジェクトが返ってきます。このResponseオブジェクトには先ほどサーバーで作成したResponseオブジェクトと同じ情報が入っています。
その中のbodyというプロパティを使うと、ReadableStreamでデータを読み出すことができます。(const { body } = await fetch("/fizzbuzz");の行。)

サーバー側では「文字列のストリームをUint8Arrayのストリームに変換する」という処理を書きましたが、ブラウザ側では「Uint8Arrayを文字列のストリームのストリームに変換する」という処理を書きます。これにはTextDecoderStreamを使います。(TextEncoderStreamの逆ですね。)

これらの処理が書かれたindex.htmlをサーバーから配信すれば、FizzBuzzストリームをブラウザで受け取ることができます。

import {
  serve,
  Status,
  STATUS_TEXT,
} from "https://deno.land/std@0.142.0/http/mod.ts";
import { contentType } from "https://deno.land/std@0.142.0/media_types/mod.ts";
import { delay } from "https://deno.land/std@0.142.0/async/mod.ts";

serve(async (request) => {
  const { pathname } = new URL(request.url);

  if (pathname === "/") { // "/"というパスにアクセスされた場合は
    // index.htmlを読み込み
    const { body } = await fetch(new URL("./index.html", import.meta.url));
    // index.htmlのデータをレスポンスで返す
    return new Response(body, {
      headers: { "Content-Type": contentType("html")! },
    });
  }

  if (pathname === "/fizzbuzz") {
    let i = 0;
    const readable = new ReadableStream<string>({
      async pull(controller) {
        // 送信スピードが速すぎてブラウザコンソールが埋まってしまうので、1秒間待機してから送信する
        await delay(1000);

        const val = i % 15 === 0
          ? "FizzBuzz"
          : i % 3 === 0
          ? "Fizz"
          : i % 5 === 0
          ? "Buzz"
          : i;
        controller.enqueue(`${val}`);
        i++;
      },
    });
    return new Response(readable.pipeThrough(new TextEncoderStream()));
  }

  return new Response(STATUS_TEXT.get(Status.NotFound), {
    status: Status.NotFound,
  });
});

ここまで実装すると、

FizzBuzzストリーム

TextEncoderStream(文字列→Uint8Array変換)

(サーバーからブラウザへ送信)

TextDecoderStream(Uint8Array→文字列変換)

while文で読み出し

という処理の流れが完成します。

このサーバーを起動した状態で http://localhost:8000/ にアクセスすれば、ブラウザコンソールにfizzbuzzが流れてきます。

image.png

無事FizzBuzzがブラウザ側で読めていることが確認できました。

※なお、送信スピードが速すぎてブラウザコンソールが埋まってしまうので、サーバー側からデータを送信する時に、delay関数を使って1秒待機するようにしています。

まとめ

  • ReadableStreamは遅延評価なので無限にデータを流すことができる
  • これを利用して無限FizzBuzzストリームを作ることができる
  • Denoを使うとサーバー側で作成したReadableStreamをブラウザ側に渡すことができる

以上、無限Fizz Buzzストリームを配信するサーバーの作り方でした。

外部ライブラリを一切使わずにここまでできるので、Denoはすごいぞという感じです。

なお本記事で作成したサーバーはdeno deployというサービスを使って公開しています。急にFizzBuzzストリームが必要になったときはhttps://fizzbuzz.deno.dev にアクセスしてお使いください。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0