更新於 2017/1/10:Laravel 官方已推出 Laravel Echo,可簡化整個建置流程,有興趣可前往參考。
今天在逛 PHPHub 時剛好看到這篇,想著之前也想做類似 Facebook 的通知服務,剛好之前也有碰過一陣子的 socket.io,所以就試著實做看看了。不過推播通知在手機上是相當常見的,但在 Web 上不知為何卻相當少見,也可能是我見識太淺了,看過的網站太少XD。
起手式
首先我們需要先建 Laravel 專案:
1 | $ laravel new notification |
設定你的 .env
,除了資料庫外我們還會使用到隊列(Queue)及廣播(broadcast),看起來會像:
.env
1 |
|
要使用 Redis 必須在 Composer 安裝 predis/predis
:
$ composer require predis/predis
接著執行遷移,跟 5.2 提供的 Auth scaffold(幫我們把 Auth 的部分連 View 都建完):
1 | $ php artisan migrate |
試試看應用程式有沒有正常執行,最後新增兩個使用者,看要在瀏覽器直接建立,或是其他方式也可以。
什麼是隊列
隊列簡單來說就像是 JavaScript 的非同步機制,讓你把一個耗時的工作丟給別人做,你的程式會跳過這部分繼續執行。最常見到的案例就是寄 e-mail 跟簡訊。
什麼是廣播
我們會利用 Laravel 的廣播事件做推送通知的服務,開始之前建議大概瀏覽一下文件,廣播的方式大概如下圖:
流程如下:
- 在 Laravel 執行一個推播通知事件
- 推播通知事件的資訊會推送至 Redis 中
- Node 端會訂閱該 Redis 的頻道,接收到推播通知事件的資訊
- 透過 websocket 將推播通知送給使用者
建立推播通知事件
首先先讓我們建立一個推播通知事件,所有的推播都會透過此事件推送到 Redis:
$ php artisan make:event PushNotification
程式碼如下:
app/Events/PushNotification.php
1 |
|
我們的事件會有兩個屬性,一個是要推播的 message,另一個比較特別的則是 token。token 會作為 socket.io 中 room 的名稱,代表一個使用者。也就是說一個使用者只會有一個 room(token),這麼做可以讓我們指定要推播給哪個使用者。
broadcastOn 則是設定在 Redis 中的頻道名稱,我們會在 socket.io server 端透過這個名稱來訂閱由此事件傳遞的資訊。
若不太明白可以先接著往下看,會有更詳細的說明。
token 的雜湊方式可以隨你喜歡更改,但要確定每次雜湊出來的值都相同,因為我們在 render view 給使用者的時候也會雜湊一組 token 給前端的 JavaScript,以加入 socket.io 中特定的 room。
建立 Socket.io Server
我們的 socket.io 會有兩個任務:
- 接收由 Laravel 的 PushNotification 事件送來的推播資訊
- 將內容透過 websocket 推播給使用者
讓我們先使用 npm 安裝必要的套件:分別是 express(http server)、socket.io(websocket server)及 ioredis(訂閱 redis):
$ npm install express socket.io ioredis --save
接著我們建立 socket.js,先寫 redis 部份的程式碼測試與 Laravel 廣播事件的串接是否有問題:
socket.js
1 | var Redis = require('ioredis'); |
測試與 Laravel 是否正確串接
首先你必須先確認這些東西有沒有執行:
- Laravel Application(Nginx or php artisan serve)
- Redis server
- 隊列監聽器(php artisan queue:listen)
- socket.io server(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
前端
首先我們必須先安裝 socket.io-client,這是 socket.io 在前端所使用的套件,我們會透過 server side 的開發方式,再透過 elixir 的 browserify 轉成前端可執行的 JavaScript。
$ npm install socket.io-client --save
建立 resources/assets/js/app.js
,撰寫以下程式碼:
resources/assets/js/app.js
1 | var io = require('socket.io-client'); |
接著修改 gulpfile.js,然後執行 gulp,他會將編譯結果放在 public/js/app.js
:
gulpfile.js
1 | elixir(function(mix) { |
接著我們希望在 /home
能接收推播(5.2 的 make:auth
預設提供 /home
作為登入後的首頁),所以先在 resources/views/layouts/app.blade.php
下方加上 @yield('scripts')
,看起來會像這樣:
resources/views/layouts/app.blade.php
然後在 resources/views/home.blade.php
下面載入剛剛寫好的 JavaScript:
resources/views/home.blade.php
1 | @section('content') |
後端
修改剛剛的 socket.js,增加 socket.io 及推送通知至前端的程式碼:
socket.js
1 | var app = require('express'); |
接著就可以測試前端是否可以收到通知了!
區分使用者
如果你有開不同瀏覽器登入不同使用者的話會發現,不管你在事件的 User 傳入誰,每個使用者都會收到通知。
因為所有使用者都屬於同一個 channel(notification)。這時就要使用 token 及 socket.io 的 room 來區分使用者。每個 token 代表一個 room,也就是一個使用者,我們就可以由 Laravel 廣播事件內的 token 決定要接推播通知傳給哪個使用者:
前端
我們要做的事情有:
- 在 Controller 產生 token(與事件中的相同),並傳遞至 View
- 前端的 JavaScript 取得 token,並傳給 socket.io server 加入指定的 room
首先,先修改HomeControllr@index
app/Http/Controllers/HomeControllr.php
1 | /** |
接著修改剛剛新增在 resources/views/home.blade.php
的部分,將 token 傳至 JavaScript 中:
resources/views/home.blade.php
1 | ... |
修改 resources/assets/js/app.js
,使用 token 加入使用者的 room:
resources/assets/js/app.js
1 | var io = require('socket.io-client'); |
後端
修改 socket.js,讓使用者加入屬於他的 room,並由 Laravel 廣播事件資訊內的 token 決定要傳給哪個使用者(room):
1 | var app = require('express'); |
Demo
基本上前端收的到通知之後,如何呈現就不是困難的問題了。
後記
實作其實沒那麼困難,不過如果真的要上 Production 的話還是得再思考一下!因為感覺這個 Solution 沒有很透徹XD!
像是 token 的部分這樣安全性不知道會不會不佳,如果想更安全可以用更複雜的演算法,或是在 Laravel 跟 socket.io server 用相同的加密演算法,互相加解密也可以。作法應該還很多種,有厲害的大大還麻煩幫忙補充XD