2

说明:本文主要来源于real-time-apps-laravel-5-1-event-broadcasting

本文主要基于Laravel的Model Event介绍该框架的实时通信功能,Laravel模型的生命周期中包含事件:createdcreatingsavedsavingupdatedupdatingdeleteddeletingrestoredrestoring,同时结合了Pusher包,有关Pusher的注册和使用相关信息可以参考:基于 Pusher 驱动的 Laravel 事件广播(上)。同时,作者会将开发过程中的一些截图和代码黏上去,提高阅读效率。
备注:Laravel对Model的CRUD操作都会触发对应的事件,如create操作会在创建前触发creating事件,创建后触发created事件,即Model Event。

Non Real-time App

Laravel程序安装

先全局安装composer:

    curl -sS https://getcomposer.org/installer | php
    mv composer.phar /usr/local/bin/composer

新建一个空文件夹,在文件夹下,再使用composer安装Laravel项目:

composer create-project laravel/laravel mylaravelapp --prefer-dist

写一个TODO APP

写路由Route

在app/Http/routes.php中写上资源型路由:

Route::get('/', function () {
    return view('index');
});
Route::resource('items', 'ItemController', ['except' => ['create', 'edit']]);//排除掉create和edit操作

写个Model

先建个迁移文件:

php artisan make:migration create_items_table --create=items

在迁移文件database/migrations/*_create_items_table.php中写上:

/**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('items', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->boolean('isCompleted')->default(false);
            $table->timestamps();
        });
    }

新建一个Eloquent Model:

php artisan make:model Item

别忘了配置下数据库,我用的是MAMP集成环境,数据库服务是MySQL。数据库配置主要在config/database.php和.env文件中,在.env文件中写上对应的host,database,user,password:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_DATABASE=model_event
DB_USERNAME=root
DB_PASSWORD=model_event

写控制器Controller

首先在项目根目录下输入artisan命令创建个ItemController:

php artisan make:controller ItemController

在ItemController中写上增删改查:

class ItemController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return Response
     */
    public function index()
    {
        $uncompletedItems = Item::where('isCompleted', 0)->get();
        $completedItems = Item::where('isCompleted', 1)->get();

        $data = ['uncompletedItems' => $uncompletedItems,
            'completedItems' => $completedItems];

        return view('item.index', $data);
    }

    /**
     * Store a newly created resource in storage.
     *
     * @return Response
     */
    public function store(Request $request)
    {
        $item = new Item;
        $item->title = $request->title;
        $item->save();
        return response()->json(['id' => $item->id]);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return Response
     */
    public function show($id)
    {
        $item = Item::find($id);
        return view('item.show', ['item' => $item]);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function update(Request $request, $id)
    {
        $item = Item::find($id);
        $item->isCompleted = (bool) $request->isCompleted;
        $item->save();
        return;
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return Response
     */
    public function destroy($id)
    {
        $item = Item::find($id);
        $item->delete();
        return;
    }
}

写个View视图

建个reources/views/index.php:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Todo App</title>

    <!-- Bootstrap -->
    {{--<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">--}}
      <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <!--<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>-->
      {{--<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>--}}
    {{--<![endif]-->--}}
  </head>
  <body>
    <div class="container">
        <div class="row">
            <div class="col-sm-offset-4 col-sm-4">
                <h1 class="text-center">Todo App</h1>
                <form id="addFrm" role="form">
                    <div class="form-group">
                        <input type="text" class="form-control" name="title"  id="title" required="required" placeholder="Enter title">
                    </div>
                    <div class="form-group">
                        <input type="submit" class="btn btn-default" name="submit" value="Add">
                    </div>
                </form>
                <hr>
                <div id="itemsList">
                </div>                
            </div>
        </div>
    </div>

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <!-- 新 Bootstrap 核心 CSS 文件 -->
    <!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
    <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
    {{--<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>--}}
    {{--<script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>--}}
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    {{--<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>--}}
    <script>
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });
    </script>
    <script>
        //renders item's new state to the page
        function addItem(id, isCompleted) {//根据状态添加item
            $.get("/items/" + id, function(data) {
                if (isCompleted) {
                    $("#completedItemsList").append(data);
                } else {
                    $("#uncompletedItemsList").append(data);
                }
            });
        }

        //removes item's old state from the page
        function removeItem(id) {
            $('li[data-id="' + id + '"').remove();
        }

        (function($, addItem, removeItem) {
            $.get( "/items", function( data ) {//DOM加载后,AJAX请求数据,进入ItemController::index()
                $( "#itemsList" ).html( data );
            });

            $( "#addFrm" ).submit(function() {//回车或点击提交按钮时,AJAX post到ItemController::store()方法,json返回保存的'id'=>$item->id
                console.log($(this).serialize());
                $.post( "/items", $(this).serialize(), function( data ) {
                    addItem(data.id, false);
                    $( "#title" ).val('');
                });
                return false;
            });

            $(document).on("change", ".isCompleted", function() {
                var id = $(this).closest('li').data('id');
                var isCompleted = $(this).prop("checked") ? 1 : 0;//获取该item的完成状态
                $.ajax('/items/' + id, {//进入ItemController::update(),更细下item状态
                    data: {"isCompleted": isCompleted},
                    method: 'PATCH',
                    success: function() {//根据状态变化删除增加item
                        removeItem(id);
                        addItem(id, isCompleted);
                    }
                });
            });

            $(document).on("click", ".deleteItem", function() {
                var id = $(this).closest('li').data('id');
                $.ajax('/items/' + id, {//进入ItemController::destroy()删除数据库中item
                    method: 'DELETE',
                    success: function() {//UI删除该item
                        removeItem(id);
                    }
                });
            });
        })(jQuery, addItem, removeItem);

    </script>
  </body>
</html>

ItemController控制器中返回两个子视图item.index、item.show,在resources/views/item中建两个:

//item.index
<legend>未完成的Items</legend>
<ul id="uncompletedItemsList" class="list-group">
    @foreach ($uncompletedItems as $item)
        @include('item.show')
    @endforeach
</ul>
<hr>
<legend>完成的Items</legend>
<ul id="completedItemsList" class="list-group">
    @foreach ($completedItems as $item)
        @include('item.show')
    @endforeach
</ul>
//item.show
<li class="list-group-item {{ ($item->isCompleted) ? 'text-muted' : '' }}" data-id="{{ $item->id }}">
    <span class="badge">
        <span class="deleteItem glyphicon glyphicon-remove" aria-hidden="true"></span>
    </span>
    <span class="checkbox-inline">
        <label>
            <input type="checkbox" class="isCompleted" value="1" {{ ($item->isCompleted) ? 'checked="checked"' : '' }}>
            {{ $item->title }}
        </lable>
    </span>
</li>

一切准备就OK了,我的在MAMP环境输入路由:http://laravelmodelevent.app:...,新开AB两个页面,然后在输入框里提交文本后:
图片描述
A页面输入后B页面只有刷新才能看到最新输入的文本,不能实时显示,当然,输入的文本已经保存在model_event.items表里了:
图片描述

页面里改变每一个item的checkbox后,该item的状态将会互换,在UI上显示也是上下位置互换,具体逻辑可以看views/index.blade.php的JS逻辑,这不是本文的重点,故不详述。

重点是:在A页面写入新文本,B页面不能实时显示。这还不是个实时APP。

Real-time App

创建三个广播事件

创建三个广播事件:

  • ItemCreated:当新建一个item完成时触发

  • ItemUpdated:当更新一个item完成时触发(isCompleted=0或1)

  • ItemDeleted:当删除一个item完成时触发

在项目根目录依次输入:

php artisan make:event ItemCreated
php artisan make:event ItemUpdated
php artisan make:event ItemDeleted

Laravel事件广播需要实现ShouldBroadcast接口并且在broadcastOn()方法中写上广播频道:

class ItemCreated extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $id;
    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Item $item)
    {
        $this->id = $item->id;
    }

    /**
     * Get the channels the event should be broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return ['itemAction'];
    }
}
class ItemDeleted extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $id;
    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Item $item)
    {
        $this->id = $item->id;
    }

    /**
     * Get the channels the event should be broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return ['itemAction'];
    }
}
class ItemUpdated extends Event implements ShouldBroadcast
{
    use SerializesModels;

    public $id;
    public $isCompleted;
    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct(Item $item)
    {
        $this->id          = $item->id;
        $this->isCompleted = (bool)$item->isCompleted;
    }

    /**
     * Get the channels the event should be broadcast on.
     *
     * @return array
     */
    public function broadcastOn()
    {
        return ['itemAction'];
    }
}

创建Model Event

Laravel的Eloquent每一CRUD操作都会触发Model事件,可以在service provider里监听这些事件从而触发新建的三个广播事件,在AppServiceProvider中:

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Item::created(function($item){
            event(new ItemCreated($item));
        });
        Item::deleted(function($item){
            event(new ItemDeleted($item));
        });
        Item::updated(function($item){
            event(new ItemUpdated($item));
        });
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

使用Pusher

Pusher的作用、注册和安装可参考:基于 Pusher 驱动的 Laravel 事件广播(上)
注册安装也比较简单,总之使用Pusher能做个实时APP。
更新resources/views/index.blade.php文件:

...
    <title>Todo App</title>
    <!-- Bootstrap -->  
    <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <script src="//js.pusher.com/3.0/pusher.min.js"></script>//引入pusherJS文件
           
...
        $.post( "/items", $(this).serialize(), function( data ) {
//                    addItem(data.id, false);//注销掉
                    $( "#title" ).val('');
                });
...
       $.ajax('/items/' + id, {//进入ItemController::update(),更细下item状态
                    data: {"isCompleted": isCompleted},
                    method: 'PATCH',
                    success: function() {//根据状态变化删除增加item
//                        removeItem(id);//注销掉
//                        addItem(id, isCompleted);//注销掉
                    }
                });
...                
        $(document).on("click", ".deleteItem", function() {
                var id = $(this).closest('li').data('id');
                $.ajax('/items/' + id, {//进入ItemController::destroy()删除数据库中item
                    method: 'DELETE',
                    success: function() {//UI删除该item
//                        removeItem(id);//注销掉
                    }
                });
            });
        })(jQuery, addItem, removeItem);

    //新加代码
        var pusher            = new Pusher("{{env("PUSHER_KEY")}}");
        var itemActionChannel = pusher.subscribe('itemAction');
        itemActionChannel.bind('App\\Events\\ItemCreated', function (data) {
            console.log(data.id);
           addItem(data.id, false);
        });
        itemActionChannel.bind('App\\Events\\ItemDeleted', function (data) {
            console.log(data.id);
           removeItem(data.id);
        });
        itemActionChannel.bind('App\\Events\\ItemUpdated', function (data) {
            removeItem(data.id);
            addItem(data.id, data.isCompleted);
        });     
      
      

新加代码主要用pusher对象注册三个事件广播的频道'itemAction',并分别绑定三个事件,成功后回调执行对应的UI操作。想要了解更多可以参考这篇文章:基于 Pusher 驱动的 Laravel 事件广播(下)

测试实时功能

刷新AB页面,并观察数据库model_event.items。

测试实时创建功能。A页面输入文本后发现B页面不用刷新就实时显示对应内容,且数据库已经保存刚刚创建的文本:
图片描述
图片描述

测试实时更新功能。B页面点击状态更新checkbox后,A页面该item状态也实时更新,且数据库isCompleted字段变为1:
图片描述
图片描述

测试实时删除功能。A页面点击删除按钮后,B页面也实时删除对应的item,且数据库该item也删除:
图片描述
图片描述

OK,It is working!!!

总结:本节主要利用Laravel的Model Event来创建一个实时WEB APP,挺好玩的,可以玩一玩哦。有问题可留言。嘛,过两天还想结合Laravel的Container Event容器事件新开篇文章,到时见。

欢迎关注Laravel-China

RightCapital招聘Laravel DevOps


lx1036
3.1k 声望923 粉丝

为五斗米折腰