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;
このようにShouldBroadcastをimplementsする。
tinkerで確認する
一応
composer dump-autoload
してからtinkerを起動する
Psy Shell v0.11.22 (PHP 8.2.12 — cli) 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" />
</>
);
}
一々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つをあわせて送信する
そうすと、待機しているブラウザーは何も操作していなくても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に投入する際は十分テストと学習を行ってからにして欲しい。ここで書いている事はまだ実践ですらないのだ。
この記事が気に入ったらサポートをしてみませんか?