Laravel 11 Reverb 构建实时通知案例
文档:
reverb:https://laravel.com/docs/11.x/reverb
广播:https://laravel.com/docs/11.x/broadcasting
在本篇文章中,将演示如何在 Laravel 11 应用中使用 Reverb 发送实时通知。本案例中,将实现一个“普通用户发布文章时管理员将收到通知的案例。”
实现介绍
- 使用 Laravel UI 创建身份验证(auth)脚手架
- 设置 2 种用户:管理员和普通用户,
is_admin
用于区分是否为管理员 - 创建一个文章表
- 允许用户发布文章
- 创建事件监听发送实时通知
- 普通用户发送文章之后,管理员使用 Reverb 收到实时通知
创建 Laravel 11 项目
$ laravel new reverb-noticle
┌ Would you like to install a starter kit? ────────────────────┐
│ No starter kit │
└──────────────────────────────────────────────────────────────┘
┌ Which testing framework do you prefer? ──────────────────────┐
│ Pest │
└──────────────────────────────────────────────────────────────┘
┌ Would you like to initialize a Git repository? ──────────────┐
│ No │
└─────────────────
$ cd reverb-noticle
使用 bootstrap auth
# 引入 UI 包
$ composer require laravel/ui
# 生成 auth
$ php artisan ui bootstrap --auth
$ pnpm install
$ pnpm run build
创建迁移文章
创建文章迁移表
$ php artisan make:migration create_posts_table
为 posts 表添加字段:database/migrations/2024_09_15_041935_create_posts_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('body');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
为 users 表添加 is_admin
字段:database/migrations/0001_01_01_000000_create_users_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
// 是否为管理员
$table->tinyInteger('is_admin')->default(0);
$table->timestamps();
});
...
}
...
};
执行迁移文件
$ php artisan migrate
创建和修改模型
生成文章模型和控制器
$ php artisan make:model Post -c
修改 Post 模型:app/Models/Post.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = ['title', 'body', 'user_id'];
public function user()
{
return $this->belongsTo(User::class);
}
}
修改 User 模型:app/Models/User.php
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use HasFactory, Notifiable;
protected $fillable = [
'name',
'email',
'password',
'is_admin' // 添加
];
protected $hidden = [
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
安装 Reverb & Echo 服务器
1)安装广播。注意,Laravel 11 默认没有启动广播,执行如下命令之后,请会出现是否安装 Reverb
,选择 Yes
安装。
$ php artisan install:broadcasting
2)接下来安装 Laravel echo 服务
$ pnpm install --save-dev laravel-echo
echo
服务安装之后,下面一起来看看都有什么内容:resources/js/echo.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb', // 这个很重要,制定了广播驱动使用 reverb
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
再来看看.env
中的配置内容:
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=729547
REVERB_APP_KEY=iwlrwqmzcka4zknocycw
REVERB_APP_SECRET=c45icidgb9gw79695s1a
REVERB_HOST="localhost"
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
3)再次构建 JS
$ pnpm run build
创建添加文章事件
$ php artisan make:event PostCreate
编辑广播事件代码:app/Events/PostCreate.php
<?php
namespace App\Events;
use App\Models\Post;
use App\Models\User;
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;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class PostCreate implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* 创建一个新的事件实例
*
* @param Post $post 创建的文章实例
* @param User $user 创建文章的用户实例
*/
public function __construct(public Post $post, private readonly User $user)
{
}
/**
* 定义事件广播的频道
*
* @return Channel
*/
public function broadcastOn(): Channel
{
// 返回事件广播的频道,这里是一个公共频道 posts
return new Channel('posts');
}
/**
* 指定广播事件的名称
*
* @return string
*/
public function broadcastAs(): string
{
return 'create';
}
/**
* 获取要广播的数据
*
* @return array
*/
public function broadcastWith(): array
{
return [
'message' => "{$this->user->name} 于 [{$this->post->created_at}] 创建新文章:'{$this->post->title}'."
];
}
}
PostCreate
事件用于广播新文章创建的通知。
- 通过
broadcastOn
方法指定了广播的频道('posts'
), - 通过
broadcastAs
方法定义了广播事件的名称('create'
), - 通过
broadcastWith
方法提供了广播的数据(包括用户的名字、文章创建时间和标题)。
这个事件会立即广播,确保前端能够实时接收到新文章创建的消息。
创建路由
routes/web.php
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
// 文章路由
Route::get('posts', [\App\Http\Controllers\PostController::class, 'index'])->name('posts.index');
Route::post('posts', [\App\Http\Controllers\PostController::class, 'store'])->name('posts.store');
编写控制器方法
app/Http/Controllers/PostController.php
<?php
namespace App\Http\Controllers;
use App\Events\PostCreate;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$posts = Post::query()->latest()->get();
return view('posts', compact('posts'));
}
public function store(Request $request)
{
$this->validate($request, [
'title' => 'required',
'body' => 'required'
]);
$post = Post::create([
'user_id' => auth()->id(),
'title' => $request->title,
'body' => $request->body
]);
// 触发 PostCreate 事件,将新创建的文章和当前用户作为参数传递
event(new PostCreate($post, auth()->user()));
return back()->with('success','发布文章成功');
}
}
编写视图文件
1)修改 app 模板文件:resources/views/layouts/app.blade.php
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<!-- Scripts -->
@vite(['resources/sass/app.scss', 'resources/js/app.js'])
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
@yield('script')
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">
Laravel 使用 Reverb 发送实时通知
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<!-- 导航栏左侧-->
<ul class="navbar-nav me-auto">
</ul>
<!-- 导航栏右侧 -->
<ul class="navbar-nav ms-auto">
<!-- Authentication Links -->
<!-- 用户未登录 -->
@guest
@if (Route::has('login'))
<li class="nav-item">
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
</li>
@endif
@if (Route::has('register'))
<li class="nav-item">
<a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
</li>
@endif
@else
<!-- 用户已登录 -->
<li class="nav-item">
<a class="nav-link" href="{{ route('posts.index') }}">{{ __('Posts') }}</a>
</li>
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::user()->name }}
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
@csrf
</form>
</div>
</li>
@endguest
</ul>
</div>
</div>
</nav>
<main class="py-4">
@yield('content')
</main>
</div>
</body>
</html>
2)添加文章视图:resources/views/posts.blade.php
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<div class="card-header"><i class="fa fa-list"></i> {{ __('Posts List') }}</div>
<div class="card-body">
@session('success')
<div class="alert alert-success" role="alert">
{{ $value }}
</div>
@endsession
<div id="notification">
</div>
@if(!auth()->user()->is_admin)
<p><strong>创建文章</strong></p>
<form method="post" action="{{ route('posts.store') }}" enctype="multipart/form-data">
@csrf
<div class="form-group">
<label>标题:</label>
<input type="text" name="title" class="form-control" />
@error('title')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label>内容:</label>
<textarea class="form-control" name="body"></textarea>
@error('body')
<div class="text-danger">{{ $message }}</div>
@enderror
</div>
<div class="form-group mt-2">
<button type="submit" class="btn btn-success btn-block"><i class="fa fa-save"></i> 发布</button>
</div>
</form>
@endif
<p class="mt-4"><strong>文章列表:</strong></p>
<table class="table table-bordered data-table">
<thead>
<tr>
<th width="70px">ID</th>
<th>标题</th>
<th>内容</th>
</tr>
</thead>
<tbody>
@forelse($posts as $post)
<tr>
<td>{{ $post->id }}</td>
<td>{{ $post->title }}</td>
<td>{{ $post->body }}</td>
</tr>
@empty
<tr>
<td colspan="5">There are no posts.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('script')
@if(auth()->user()->is_admin)
<script type="module">
// 重点看之类,收到服务器返回的数据后
// 在页面中进行展示
window.Echo.channel('posts')
.listen('.create', (data) => {
console.log('收到服务器返回的数据: ', data);
let d1 = document.getElementById('notification');
d1.insertAdjacentHTML('beforeend', '<div class="alert alert-success alert-dismissible fade show"><span><i class="fa fa-circle-check"></i> '+data.message+'</span></div>');
});
</script>
@endif
@endsection
-
Echo.channel('posts')
对应PostCreate
中broadcastOn
中定义的频道名称 -
listen('.create',)
对应PostCreate
中broadcastAs
中定义的广告事件名称
填充管理员用户
生成填充文件
$ php artisan make:seeder CreateAdminUser
编写填充代码:database/seeders/CreateAdminUser.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\User;
class CreateAdminUser extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
User::create([
'name' => '管理员',
'email' => 'admin@qq.com',
'password' => bcrypt('123456'),
'is_admin' => 1
]);
}
}
执行填充
$ php artisan db:seed --class=CreateAdminUser
启动应用
启动项目应用
$ php artisan serve
启动 reverb 服务
$ php artisan reverb:start
测试实时通知
测试时,需要打开两个不同的客户端。
- 注册一个普通用户
- 登录管理员用户和普通用户,并打开文章列表
- 管理员界面中打开 F12,观察控制台输出
- 普通用户发布一篇文章,管理员即可收到通知
看到如图信息:测试成功
^_^