更新於 2017/1/10:Laravel 官方已推出 Laravel Echo,可簡化整個建置流程,有興趣可前往參考。
今天在逛 PHPHub 時剛好看到這篇,想著之前也想做類似 Facebook 的通知服務,剛好之前也有碰過一陣子的 socket.io,所以就試著實做看看了。不過推播通知在手機上是相當常見的,但在 Web 上不知為何卻相當少見,也可能是我見識太淺了,看過的網站太少 XD。
首先我們需要先建 Laravel 專案:
$ laravel new notification$ cd notification$ composer install$ npm install
設定你的 .env,除了資料庫外我們還會使用到隊列(Queue)及廣播(broadcast),看起來會像:
...QUEUE_DRIVER=redisBROADCAST_DRIVER=redis...
要使用 Redis 必須在 Composer 安裝 predis/predis:
$ composer require predis/predis
接著執行遷移,跟 5.2 提供的 Auth scaffold(幫我們把 Auth 的部分連 View 都建完):
$ php artisan migrate$ php artisan make:auth
試試看應用程式有沒有正常執行,最後新增兩個使用者,看要在瀏覽器直接建立,或是其他方式也可以。
隊列簡單來說就像是 JavaScript 的非同步機制,讓你把一個耗時的工作丟給別人做,你的程式會跳過這部分繼續執行。最常見到的案例就是寄 e-mail 跟簡訊。
我們會利用 Laravel 的廣播事件做推送通知的服務,開始之前建議大概瀏覽一下文件,廣播的方式大概如下圖:

流程如下:
首先先讓我們建立一個推播通知事件,所有的推播都會透過此事件推送到 Redis:
$ php artisan make:event PushNotification
程式碼如下:
<?phpnamespace App\Events;use App\Events\Event;use App\User;use Illuminate\Queue\SerializesModels;use Illuminate\Contracts\Broadcasting\ShouldBroadcast;class PushNotification extends Event implements ShouldBroadcast{use SerializesModels;/*** @var string*/public $token;/*** @var string*/public $message;/*** Create a new event instance.** @param User $user* @param $message*/public function __construct(User $user, $message){$this->token = sha1($user->id . '|' . $user->email);$this->message = $message;}/*** Get the channels the event should be broadcast on.** @return array*/public function broadcastOn(){return ['notification'];}}
我們的事件會有兩個屬性,一個是要推播的 message,另一個比較特別的則是 token。token 會作為 socket.io 中 room 的名稱,代表一個使用者。也就是說一個使用者只會有一個 room(token),這麼做可以讓我們指定要推播給哪個使用者。
broadcastOn 則是設定在 Redis 中的頻道名稱,我們會在 socket.io server 端透過這個名稱來訂閱由此事件傳遞的資訊。
若不太明白可以先接著往下看,會有更詳細的說明。
token的雜湊方式可以隨你喜歡更改,但要確定每次雜湊出來的值都相同,因為我們在 render view 給使用者的時候也會雜湊一組 token 給前端的 JavaScript,以加入 socket.io 中特定的 room。
我們的 socket.io 會有兩個任務:
讓我們先使用 npm 安裝必要的套件:分別是 express(http server)、socket.io(websocket server)及 ioredis(訂閱 redis):
$ npm install express socket.io ioredis --save
接著我們建立 socket.js,先寫 redis 部份的程式碼測試與 Laravel 廣播事件的串接是否有問題:
var Redis = require('ioredis');var redis = new Redis();// 訂閱 redis 的 notification 頻道,也就是我們在事件中 broadcastOn 所設定的redis.subscribe('notification', function (err, count) {console.log('connect!');});// 當該頻道接收到訊息時就列在 terminal 上redis.on('message', function (channel, notification) {console.log(notification);});
首先你必須先確認這些東西有沒有執行:
php artisan serve)php artisan queue:listen)node socket.js)確認完畢後,我們進入 Laravel 的 Tinker 做測試:
$ php artisan tinker

我們直接觸發事件:
event(new App\Events\PushNotification(App\User::first(), 'banana!'))
你應該在 node 的 terminal 看到:
{"event":"App\\Events\\PushNotification","data":{"token":"long-hash-string","message":"banana!"}}
首先我們必須先安裝 socket.io-client,這是 socket.io 在前端所使用的套件,我們會透過 server side 的開發方式,再透過 elixir 的 browserify 轉成前端可執行的 JavaScript。
$ npm install socket.io-client --save
建立 resources/assets/js/app.js,撰寫以下程式碼:
var io = require('socket.io-client');// 建立 socket.io 的連線var notification = io.connect('http://localhost:3000');// 當從 socket.io server 收到 notification 時將訊息印在 console 上notification.on('notification', function (message) {console.log(message);});
接著修改 gulpfile.js,然後執行 gulp,他會將編譯結果放在 public/js/app.js:
elixir(function (mix) {mix.browserify('app.js');});
接著我們希望在 /home 能接收推播(5.2 的 make:auth 預設提供 /home 作為登入後的首頁),所以先在 resources/views/layouts/app.blade.php 下方加上 @yield('scripts') ,看起來會像這樣:
...</div></nav>@yield('content')@yield('scripts')<!-- JavaScripts -->{{-- <script src="{{ elixir('js/app.js') }}"></script> --}}...
然後在 resources/views/home.blade.php 下面載入剛剛寫好的 JavaScript:
@section('content')<script src="/js/app.js"></script>@endsection
修改剛剛的 socket.js,增加 socket.io 及推送通知至前端的程式碼:
var app = require('express');var http = require('http').Server(app);var io = require('socket.io')(http);var Redis = require('ioredis');var redis = new Redis();redis.subscribe('notification', function (err, count) {console.log('connect!');});redis.on('message', function (channel, notification) {console.log(notification);notification = JSON.parse(notification);// 將訊息推播給使用者io.emit('notification', notification.data.message);});// 監聽 3000 porthttp.listen(3000, function () {console.log('Listening on Port 3000');});
接著就可以測試前端是否可以收到通知了!

如果你有開不同瀏覽器登入不同使用者的話會發現,不管你在事件的 User 傳入誰,每個使用者都會收到通知。
因為所有使用者都屬於同一個 channel(notification)。這時就要使用 token 及 socket.io 的 room 來區分使用者。每個 token 代表一個 room,也就是一個使用者,我們就可以由 Laravel 廣播事件內的 token 決定要接推播通知傳給哪個使用者:

我們要做的事情有:
token(與事件中的相同),並傳遞至 Viewtoken,並傳給 socket.io server 加入指定的 room首先,先修改 HomeControllr@index
/*** Show the application dashboard.** @return Response*/public function index(Request $request){$user = $request->user();$token = sha1($user->id . '|' . $user->email);return view('home', compact('token'));}
接著修改剛剛新增在 resources/views/home.blade.php 的部分,將 token 傳至 JavaScript 中:
... @section('content')<script>Notification.TOKEN = '{{ $token or null }}';</script><script src="/js/app.js"></script>@endsection
修改 resources/assets/js/app.js,使用 token 加入使用者的 room:
var io = require('socket.io-client');var notification = io.connect('http://localhost:3000');// 當連接到 socket.io server 時觸發 set-token 設定使用者的 roomnotification.on('connect', function () {notification.emit('set-token', Notification.TOKEN);});notification.on('notification', function (message) {console.log(message);});
修改 socket.js,讓使用者加入屬於他的 room,並由 Laravel 廣播事件資訊內的 token 決定要傳給哪個使用者(room):
var app = require('express');var http = require('http').Server(app);var io = require('socket.io')(http);var Redis = require('ioredis');var redis = new Redis();redis.subscribe('notification', function (err, count) {console.log('connect!');});io.on('connection', function (socket) {// 當使用者觸發 set-token 時將他加入屬於他的 roomsocket.on('set-token', function (token) {console.log(token);socket.join('token:' + token);});});redis.on('message', function (channel, notification) {console.log(notification);notification = JSON.parse(notification);// 使用 to() 指定傳送的 room,也就是傳遞給指定的使用者io.to('token:' + notification.data.token).emit('notification',notification.data.message);});// 監聽 3000 porthttp.listen(3000, function () {console.log('Listening on Port 3000');});

基本上前端收的到通知之後,如何呈現就不是困難的問題了。
實作其實沒那麼困難,不過如果真的要上 Production 的話還是得再思考一下!因為感覺這個 Solution 沒有很透徹 XD!
像是 token 的部分這樣安全性不知道會不會不佳,如果想更安全可以用更複雜的演算法,或是在 Laravel 跟 socket.io server 用相同的加密演算法,互相加解密也可以。作法應該還很多種,有厲害的大大還麻煩幫忙補充 XD
← Back to Home