基於Model Event模型事件的Laravel實時APP

說明:本文主要來源於real-time-apps-laravel-5-1-event-broadcastingphp

本文主要基於Laravel的Model Event介紹該框架的實時通訊功能,Laravel模型的生命週期中包含事件:createdcreatingsavedsavingupdatedupdatingdeleteddeletingrestoredrestoring,同時結合了Pusher包,有關Pusher的註冊和使用相關信息能夠參考:基於 Pusher 驅動的 Laravel 事件廣播(上)。同時,做者會將開發過程當中的一些截圖和代碼黏上去,提升閱讀效率。
備註:Laravel對Model的CRUD操做都會觸發對應的事件,如create操做會在建立前觸發creating事件,建立後觸發created事件,即Model Event。css

Non Real-time App

Laravel程序安裝

先全局安裝composer:html

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

新建一個空文件夾,在文件夾下,再使用composer安裝Laravel項目:html5

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

寫一個TODO APP

寫路由Route

在app/Http/routes.php中寫上資源型路由:mysql

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

寫個Model

先建個遷移文件:jquery

php artisan make:migration create_items_table --create=items

在遷移文件database/migrations/*_create_items_table.php中寫上:laravel

/**
     * 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:ajax

php artisan make:model Item

別忘了配置下數據庫,我用的是MAMP集成環境,數據庫服務是MySQL。數據庫配置主要在config/database.php和.env文件中,在.env文件中寫上對應的host,database,user,password:sql

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

相關文章
相關標籤/搜索