laravel (+inertia.js + react) でwebsocketを本気でヤる(準備編)

websocketというトピックに限ればinertia.jsは正直あんま関係ないんだけど、ただまあreactコンポーネントを使うという意味では従来のbladeのドキュメントは使えねえからなあ…

これ読んでわかる奴いるんか?って話じゃん。

概要

websocketは

      Sender                                      Server                                      Receiver
        |                                            |                                            |
        | ------- WebSocket Handshake ------>        |                                            |
        |                                            |                                            |
        | <------ Handshake Response -------         |                                            |
        |                                            |                                            |
        | ------ Encoded Data Frame ------>          |                                            |
        |                                            | ------- Forward Data Frame ------>         |
        |                                            |                                            |
        |                                            | <------ Acknowledgement --------          |
        |                                            |                                            |
        | ------- Close Request --------->           |                                            |
        |                                            |                                            |
        | <-------- Close Response --------          |                                            |
        |                                            |                                            |

まあこんな感じの構図になっている。わからんな。

何がいいたいかというと、送信側と受信側は別に同じじゃなくてもいい(大抵は同じになるとは思うが)から、セットアップに関しては単体で行った方がトラブルが少ない、つまり送信側はまずwebsocketサーバーにリクエストを送信し、それが着弾した事を確認できないとその先には進めない。これはpusherのデバッグ画面が有益になる。、であるからまずは送信側からwebsocketサーバーまでの経路を確立する所から初める。

pusherの垢作る

自前のwebsocketサーバーとかでやるのはいきなりは難しいから特にwebsocketの概念も何にもわかってねえなら絶対にpusherの垢作りましょう そもそも、以下のような雛形を作ってくれるから楽でしょ。

送信用のlaravelプロジェクトを作成する

既存のでもいいけど、

% curl -s "https://laravel.build/moge?with=mysql"  | bash

こうするとmysqlが付いてきてしまうが、実際にはこんなミニマムなdocker-compose.ymlでいい。つまりmysqlすら必要ない(でもlaravel.buildは最低限なんか付いてきちゃうんだよなー)

version: "3"
services:
    laravel.test:
        build:
            context: ./vendor/laravel/sail/runtimes/8.2
            dockerfile: Dockerfile
            args:
                WWWGROUP: '${WWWGROUP}'
        image: sail-8.2/app
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:80'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            WWWUSER: '${WWWUSER}'
            LARAVEL_SAIL: 1
            XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
            XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
            IGNITION_LOCAL_SITES_PATH: '${PWD}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
networks:
    sail:
        driver: bridge

というか実際にはVITEすら必要ない。portの開放も必要なかったりするが、とはいえ一応これは開放する

.env.exampleから.envは作る

cp .env.example .env

sail upしておいて

 % ./vendor/bin/sail up

key:genする。

% ./vendor/bin/sail artisan key:gen

   INFO  Application key set successfully.

.envに値を記載する

.env 

PUSHER_APP_ID=**
PUSHER_APP_KEY=**
PUSHER_APP_SECRET=**

この辺の値をコピる

config/broadcasting.php 

'options' => [
  'cluster' => 'ap3',
  'useTLS' => true
],

この案内に関しては

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
                'port' => env('PUSHER_PORT', 443),
                'scheme' => env('PUSHER_SCHEME', 'https'),
                'encrypted' => true,
                'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
            ],
            'client_options' => [
                // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
            ],
        ],

元がこうなっているので.envに

PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=ap3

こうやって書いておいても同じ。

さらに重要な点として

BROADCAST_DRIVER=pusher

これを追加する。これは忘れがちになりがちであるが、これを行わないと一生pusherしない

artisan config:clear

念のため、↑をしておくこと。

eventを作る

artisan make:event MyEvent

   INFO  Event [app/Events/MyEvent.php] created successfully.

そうするとこんなファイルができあがってくる

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MyEvent
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct()
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('channel-name'),
        ];
    }
}

そうしたら

class MyEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

このようにShouldBroadcastimplementsする。

tinkerで確認する

一応 

composer dump-autoload

してからtinkerを起動する

Psy Shell v0.11.22 (PHP 8.2.12cli) by Justin Hileman
> event(new App\Events\MyEvent());

   Error  Class "Pusher\Pusher" not found.

このようにPusher/Pusherが無いといわれればokだ。これが正常

pusher/pusher-php-serverをinstallしてもう一度実行する

composer require pusher/pusher-php-server

してもう一度トライすると

Psy Shell v0.11.22 (PHP 8.2.12 — cli) by Justin Hileman
> event(new App\Events\MyEvent());
= []

このようになる。この際デバッグ画面に

このように出ていればokだ。逆にこれが出てないとこの先絶対に進んではならない

(受信専用アプリだったら別だけど…)

client側で受信確認する

bladeの解説は沢山あると思うからここでは作らないよ〜ん。inertia.jsとreactで確認してみる。inertia.jsとreactなんて使わねーよって人はここから先読んでも無駄なので他あたった方がいいすよ

breezeとreactでview作る

% ./vendor/bin/sail composer require laravel/breeze --dev
% ./vendor/bin/sail artisan breeze:install react

ごりごり作っていく。面倒なのでviteは設定しないでもいい。どうせそんな大袈裟な事はしない。vite使わない場合はviewいじったらnpm run buildしていく必要があるよ!

% ./vendor/bin/sail npm run build

welcomeページをコピってViewerコンポーネントを作る

まず routes/web.php に

Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

とかなってると思うがこれをコピーしてこんなのを作ってみよう

Route::get('/viewer', function () {
    return Inertia::render('Viewer', [
    ]);
});

当然viewもコピーする

% cp resources/js/Pages/Welcome.jsx resources/js/Pages/Viewer.jsx

最低限の内容まで省略する

resources/js/Pages/Viewer.jsx 

import { Head } from '@inertiajs/react';

export default function Welcome({ auth }) {
  return (
    <>
      <Head title="Viewer" />
    </>
  );
}


最低限のview

一々run buildするのが面倒だけど、まあこんな感じやね。

pusher.jsのinstall

これはフロントエンドのライブラリーだからphpとか関係ない。installはnpmでヤる

% ./vendor/bin/sail npm install pusher-js

で、reactの場合useEffectを使いmountされたときに初期化しちゃうのが一般的な使い方となる

import React, { useEffect } from 'react';
import Pusher from 'pusher-js';
import { Head } from '@inertiajs/react';

export default function Welcome({ auth }) {
  useEffect(() => {
    /*
    // Pusherのインスタンスを初期化
    const pusher = new Pusher('YOUR_APP_KEY', {
      cluster: 'YOUR_APP_CLUSTER'
    });

    // チャンネルに購読
    const channel = pusher.subscribe('my-channel');

    // イベントをリッスン
    channel.bind('my-event', function(data) {
      alert(JSON.stringify(data));
    });
    */
  }, []);



  return (
    <>
      <Head title="Viewer" />
      <h1>Viewer</h1>
    </>
  );
}

ここで、

  • YOUR_APP_KEY

  • YOUR_APP_CLUSTER

の2つが必須になる。これはinertiajsの場合こういう風にbackendから渡してやったっていい

Route::get('/viewer', function () {
    $pusherKey = config('broadcasting.connections.pusher.key');
    $pusherCluster = config('broadcasting.connections.pusher.options.cluster');

    return Inertia::render('Viewer', [
        'pusherKey' => $pusherKey,
        'pusherCluster' => $pusherCluster,
    ]);
});

このpropを使ってViewer.jsxを更新する

import React, { useEffect } from 'react';
import Pusher from 'pusher-js';
import { Head } from '@inertiajs/react';

export default function Welcome({ auth, pusherKey, pusherCluster }) {
  useEffect(() => {
    const pusher = new Pusher(pusherKey, {
      cluster: pusherCluster,
      forceTLS: true
    });

    // チャンネルに購読
    const channel = pusher.subscribe('channel-name');

    // イベントをリッスン
    channel.bind('my-event', function(data) {
      alert(JSON.stringify(data));
    });
  }, []);

  return (
    <>
      <Head title="Viewer" />
      <h1>Viewer</h1>
    </>
  );
}

ここでは

  • channel-name

  • my-event

という2つのキーワードでもって検知を試みている

このようにdebugコンソールに接続された旨が表示される。

何度も言うように接続されてないのにこの先に進んでもだめよ

debugコンソールから送信する

まずはpusherのデバッグコンソールからこの2つをあわせて送信する

channelとeventの名前を合わせる。Dataは何でもいい

そうすと、待機しているブラウザーは何も操作していなくてもpusherのコンソールからテスト送信した奴が

何も操作しないのが重要

alertされてくるだろう。ここまでで受信準備は整った。ここまで駄目なら次には進めない。データー連携っていうのは得てしてそういうもんだ。

いよいよeventを更新する

debugコンソールではなくphp側からこれを更新できるようにする

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('channel-name'),
        ];
    }

このようになっている。実はこれでは動作しない。これはもうちょい認証とかをしっかりした奴なので今回はもっと緩くやる。

これは単純に

    public function broadcastOn(): array
    {
        return [
            new Channel('channel-name'),
        ];
    }

Privateを抜けば Illuminate\Broadcasting\Channel が使われるのでokだ。
あと、eventをカスタマイズする必要がある

    public function broadcastAs(): string
    {
        return 'my-event';
    }

これで channel-name というチャンネルで my-event というイベント名で送信されるため

    // チャンネルに購読
    const channel = pusher.subscribe('channel-name');

    // イベントをリッスン
    channel.bind('my-event', function(data) {
      alert(JSON.stringify(data));
    });

この辺と合致するだろう。

チャンネル登録解除

  useEffect(() => {
    const pusher = new Pusher(pusherKey, {
      cluster: pusherCluster,
      forceTLS: true
    });

    // チャンネルに購読
    const channel = pusher.subscribe('channel-name');

    // イベントをリッスン
    channel.bind('my-event', function(data) {
      alert(JSON.stringify(data));
    });

    // コンポーネントのアンマウント時にリソースをクリーンアップ
    return () => {
      channel.unbind('my-event');
      pusher.unsubscribe('channel-name');
    };

  }, []);

コンポーネントが解除された場合はunbindとunsubscribeするのが作法である。

以上まとめ

再三申しあげるが、わかってないなら最初は絶対pusherでやる事。debugコンソールが便利だからだ。そして何をしたらいいのかわからない場合はとりあえず手順にしたがって上から順々にやる事だ。自分のwebsocketサーバーに移すのは後からでもやれる。

そしてsenderからwebsocketへの接続、websocketからreceiverの接続をそれぞれ独立して正しく確立できる状態にある事を保証していくこと。いっきにつきすすむと何がエラーになってるのかわからないことがある。

このようにwebsocketを使ったシステムは非常に複雑さを伴ういわば上級テクニックなので、productionに投入する際は十分テストと学習を行ってからにして欲しい。ここで書いている事はまだ実践ですらないのだ。





この記事が気に入ったらサポートをしてみませんか?