使用 Laravel & Vue.js�打造即時資訊看板
走向現代化的 PHP 開發�
PHPConf Taiwan 2016
大澤木小鐵
目前任職 KKBOX�@jaceju
我們將會談論到
我們將不會談論到
何謂即時資訊?
當系統狀態更新時,�不需要重整頁面即可自動呈現。
browser
application
system
system
system
update
send data
1sec < time ≤ 1min
常見的即時資訊
即時資訊看板實際應用
EXAMPLE
如何獲取即時資訊?
不是它戳你,就是你戳它
它戳你
你戳它
在 Laravel 上怎麼做?
它戳你:�建立 token based API
Queue
token based
API
System
create job
send data
respoding
先寫驗證程式
tests/ApiTest.php
use App\Jobs\UpdateBatteryState;
public function 用_post_呼叫更新電池狀態的_api_後應建立_UpdateBatteryState_的工作佇列()
{
Queue::fake();
$payload = ['percent' => 23, 'charging' => true];
$this->post('/api/battery-state?api_token=' . $this->user->api_token,
$payload, ['Accept' => 'application/json'])
->assertResponseOk();
Queue::assertPushed(UpdateBatteryState::class, function ($job)� use ($payload) {
return $job->payload === $payload;
});
}
database/migrations/2014_10_12_000000_create_users_table.php
class CreateUsersTable extends Migration
{
// ...
public function up()
{
Schema::create('users', function (Blueprint $table) {
// ...� $table->string('api_token', 60)->unique()->nullable();
// ...
});
}
� // ...
database/factories/ModelFactory.php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\User::class, function (Faker\Generator $faker) {
// ...
return [
// ...
'api_token' => str_random(60),
];
});
產生更新電池狀態的 Job 類別
$ php artisan make:job UpdateBatteryState
app/Jobs/UpdateBatteryState.php
namespace App\Jobs;
�// ...
class UpdateBatteryState implements ShouldQueue
{
use InteractsWithQueue, Queueable, SerializesModels;
public $payload;
public function __construct(array $payload)
{
$this->payload = $payload;
}
� // ...
routes/api.php
use App\Jobs\UpdateBatteryState;
Route::post('/battery-state', function (Request $request) {
$payload = $request->except('api_token');
dispatch(app(UpdateBatteryState::class, [$payload]));
})->middleware('auth:api');
實際測試 API
Queue Driver 使用 redis
$ composer require predis/predis
建立測試使用者
$ php artisan tinker�Psy Shell v0.7.2 (PHP 7.0.12 — cli) by Justin Hileman�>>> $user = factory(App\User::class)->create();�=> App\User {#718� name: "Miss Leanna Hintz V",� email: "hackett.roberta@example.net",� updated_at: "2016-10-24 11:45:13",� created_at: "2016-10-24 11:45:13",� id: 2,�}�>>> $user->api_token;�=> "yoZhZpeafddn6yepIV85JTtWcw3AbUbt4IEtUZjDwVYVMWg2vJyVsMWPPFlZ"�>>>
用 cURL 呼叫 API 並以 redis-cli 驗證
$ curl -X POST 'http://example-dashboard.dev/api/battery-state?api_token=...' -F percent=87 -F charging=0
$ redis-cli lrange queues:default 0 -1�1) "{\"job\":\"Illuminate\\\\Queue\\\\CallQueuedHandler@call\",\"data\":�{\"commandName\":\"App\\\\Jobs\\\\UpdateBatteryState\",\"command\":\"O:27:�\\\"App\\\\Jobs\\\\UpdateBatteryState\\\":5:{s:7:\\\"payload\\\";a:2:{s:7:�\\\"percent\\\";s:2:\\\"87\\\";s:8:\\\"charging\\\";s:1:\\\"0\\\";}s:6:�\\\"\\u0000*\\u0000job\\\";N;s:10:\\\"connection\\\";N;s:5:\\\"queue\\\";�N;s:5:\\\"delay\\\";N;}\"},\"id\":\"2GGUr21dGjLkFT3PNs5DHfwmJ3pZADPW\",�\"attempts\":1}"
你戳它:�利用 Commands 及 Schedule
以 Google Calendar 為例
$ composer require spatie/laravel-google-calendar� (安裝細節請參考套件說明)
$ php artisan make:command FetchGoogleCalendarEvents
app/Console/Commands/FetchGoogleCalendarEvents.php
namespace App\Console\Commands;
// ...
class FetchGoogleCalendarEvents extends Command
{
protected $signature = 'dashboard:calendar';
protected $description = 'Fetch Google Calendar events.';
}
app/Console/Commands/FetchGoogleCalendarEvents.php
use Spatie\GoogleCalendar\Event;
// ...
public function handle()
{
$events = Event::get() // 從 Google API 取得所有 Calendar 活動
->map(function (Event $event) {
return [
'name' => $event->name,
'date' => Carbon::createFromFormat('Y-m-d H:i:s',
$event->getSortDate())
->format(DateTime::ATOM),
];
})
->unique('name')
->toArray();
// ...
app/Console/Kernel.php
class Kernel extends ConsoleKernel
{
protected $commands = [
Commands\FetchGoogleCalendarEvents::class,
];
protected function schedule(Schedule $schedule)
{
$schedule->command('dashboard:calendar')
->everyMinute(); // 每分鐘抓取一次活動資訊
}
如何將資訊即時傳送給用戶?
你訂閱,我推播
Pub/Sub
Channel
Publisher
Subscriber
Subscriber
Subscriber
PUBLISH
SUBSCRIBE
Laravel 本身並不提供�Pub/Sub 與 Subscriber
官方推薦: Pusher
但 Pusher 要錢錢
Open Source 就是能�找到別人自幹出來分享的玩意
Redis Pub/Sub
Redis Pub/Sub
Private Channel
"dashboard"
Laravel Event
Laravel Echo Server
PUBLISH
SUBSCRIBE
我們還需要 Event
以 Google Calendar 為例
$ php artisan make:event DashboardEvent
$ php artisan make:event GoogleCalendarEventsFetched
app/Events/DashboardEvent.php
namespace App\Events;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
abstract class DashboardEvent implements ShouldBroadcast
{
/**
* Get the channels the event should broadcast on.
*/
public function broadcastOn()
{
// 對私有頻道發送事件,避免不相干的人收到事件
return new PrivateChannel('dashboard');
}
}
app/Events/GoogleCalendarEventsFetched.php
namespace App\Events;
class GoogleCalendarEventsFetched extends DashboardEvent
{
/**
* @var array
*/
public $events;
public function __construct(array $events)
{
$this->events = $events;
}
}
app/Events/GoogleCalendarEventsFetched.php
use App\Events\GoogleCalendarEventsFetched;
class FetchGoogleCalendarEvents extends Command
{
// ...
public function handle()
{
// ...
event(new GoogleCalendarEventsFetched($events));
}
}
透過 laravel-echo-server 接收事件
安裝與初始化 laravel-echo-server
$ npm install -g laravel-echo-server� (我很想用 yarn ,但用它裝好後無法執行 laravel-echo-server 指令。)
$ laravel-echo-server init�? Enter the host for the server. example-dashboard.dev�? Which port would you like to serve from? 6001�? Which database would you like to use to store presence channel members? redis�? Will you be authenticating users from a different host? Yes�? Enter the host of your authentication server. http://example-dashboard.dev�? Is this the right endpoint for authentication /broadcasting/auth? Yes�? Will you be serving on http or https? http�Configuration file saved. Run laravel-echo-server start to run server.
啟動 laravel-echo-server
$ laravel-echo-server start --dev�L A R A V E L E C H O S E R V E R
Starting server in DEV mode...
✔ Dev mode activated.
✔ Running at example-dashboard.dev on port 6001
✔ Channels are ready.
✔ Listening for redis events...
✔ Listening for http events...
Server ready!�
如何不重整頁面就更新資訊?
一切的關鍵
Web Socket
Laravel Application
laravel-echo-server
Browser
Redis Pub/Sub
Vue Renderer
Web Socket
Publish
Subcribe
抽象化 Web Socket 的官方套件
Laravel Echo
Laravel Echo
透過 Socket.io 操作 Web Socket
Laravel Echo
laravel-echo-server
Socket.io
Channel
Laravel Event
Redis
Channel
Laravel Echo 範例
Echo.channel('orders')
.listen('OrderShipped', (e) => {
console.log(e.order.name);
})
.listen('EmailSent', (e) => {
console.log(e.message);� });
安裝 Laravel Echo
$ yarn add laravel-echo pusher-js socket-io.client
resources/assets/js/app.js
import Echo from 'laravel-echo';
import SocketIO from 'socket.io-client';
// ...
window.io = SocketIO;
window.Echo = new Echo({
broadcaster: 'socket.io',
host: 'http://example-dashboard.dev:6001'
});�
// ...
讓事件透過私人頻道
廣播給前端畫面
Laravel Echo
laravel-echo-server
/broadcasting/auth
private
channel
Is user logged in?
Laravel Event
private
channel
routes/web.php
Route::get('/', function () {
return view('dashboard');
})->middleware('auth.basic');
怎麼建立
/broadcasting/auth ?
app/Providers/BroadcastServiceProvider.php
class BroadcastServiceProvider extends ServiceProvider
{
public function boot()
{
Broadcast::routes();
Broadcast::channel('dashboard', function () {
return true;
});
}
}
建立以 Vue 構成的版面
把 Vue.js 2.0 整合到 Laravel 中
結果在準備題目時,�官方已經整合好了...
版面樣式參考:�dashboard.spatie.be
resources/views/layouts/master.blade.php
<!-- Laravel Echo 會需要它 -->
<meta name="csrf-token" content="{{ csrf_token() }}">
resources/views/dashboard.blade.php
@extends('layouts/master')
@section('content')
<current-time grid="a1" dateformat="MM月DD日 ddd"></current-time>
<google-calendar grid="a2"></google-calendar>
<battery-state grid="a3"></battery-state>
<code-coverage grid="b1:d1" title="Project 1"></code-coverage>
<code-coverage grid="b2:d2" title="Project 2"></code-coverage>
<code-coverage grid="b3:d3" title="Project 3"></code-coverage>
@endsection
resources/assets/js/app.js
import Vue from 'vue';
import GoogleCalendar from './components/GoogleCalendar.vue';
import BatteryState from './components/BatteryState.vue';
import CurrentTime from './components/CurrentTime.vue';
import CodeCoverage from './components/CodeCoverage.vue';
new Vue({
el: '#app',
components: {
GoogleCalendar,
BatteryState,
CurrentTime,
CodeCoverage
}
});
Vue component 整合 Laravel Echo
Vue component
Laravel Echo
Socket.io
receive event & payload
update data
resources/assets/js/components/GoogleCalendar.vue
<script>
export default {
data() {
return {
events: [],
};
},
created () {
window.Echo.private('dashboard')
.listen('GoogleCalendarEventsFetched', (e) => {
this.events = e.events;
});
},
};
</script>
總結整個流程
Vue component
Laravel Echo
System State
Third-Party�Service
Scheduled�Commands
Token-based�API
Laravel Echo Server
Laravel Event
Queued Job
Redis Pub/Sub
Socket.io
加碼:如何設置正式環境
如何把 Laravel Worker �與 Larave Echo Server
設置為系統服務?
QUESTION
Laravel Worker
# /etc/supervisor/conf.d/laravel-worker.conf�[program:laravel-worker]�process_name=%(program_name)s_%(process_num)02d�command=php /path/to/example-dashboard/artisan queue:work redis --sleep=3 --tries=3 --daemon�autostart=true�autorestart=true�user=ubuntu�numprocs=8�redirect_stderr=true�stdout_logfile=/path/to/example-dashboard/shared/storage/logs/worker.log�
Laravel Echo Server
# /etc/supervisor/conf.d/laravel-echo-server.conf�[program:laravel-echo-server]�process_name=%(program_name)s_%(process_num)02d�command=/usr/local/bin/node /path/to/bin/laravel-echo-server start�autostart=true�autorestart=true�user=ubuntu�numprocs=1�redirect_stderr=true�stdout_logfile=/path/to/ci-dashboard/shared/storage/logs/laravel-echo-server.log�
啟動 supervisor 服務
# 重讀設定檔�$ sudo supervisorctl reread�$ sudo supervisorctl update��# 執行 services�$ sudo supervisorctl start laravel-worker:*�$ sudo supervisorctl start laravel-echo-server:*�# 或是�$ sudo supervisorctl start all�
如何讓 Laravel Echo Server
共用 https 通道?
QUESTION
Laravel Application
laravel-echo-server
browser
https (443)
http (6001)
Nginx
Laravel Application
laravel-echo-server
browser
https (443)
proxy (6001)
Nginx
Nginx 設定
location /socket.io/ {� proxy_pass http://127.0.0.1:6001;� proxy_set_header Upgrade $http_upgrade;� proxy_set_header Connection "upgrade";� proxy_http_version 1.1;� proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;� proxy_set_header Host $host;�}�
Q & A