1 of 87

使用 Laravel & Vue.js�打造即時資訊看板

走向現代化的 PHP 開發�

PHPConf Taiwan 2016

2 of 87

大澤木小鐵

目前任職 KKBOX�@jaceju

3 of 87

我們將會談論到

  • 何謂即時資訊?
  • 如何獲取即時資訊?
  • 如何將資訊即時傳送給用戶?
  • 如何不重整頁面就更新資訊?

4 of 87

我們將不會談論到

  • Laravel 的基礎
  • Vue.js 的基礎

5 of 87

何謂即時資訊?

6 of 87

當系統狀態更新時,�不需要重整頁面即可自動呈現。

7 of 87

browser

application

system

system

system

update

send data

1sec < time ≤ 1min

8 of 87

常見的即時資訊

  • Twitter 新推 / 轉推 / 按讚
  • Facebook 新動態消息 / 按讚 / 新訊息
  • Google Analytics 即時面板
  • HackPad / Google Docs

9 of 87

即時資訊看板實際應用

  • Google Calendar 活動列表
  • 網站每分鐘上線人數
  • 手機應用每日 crash 數
  • 手機應用每日評價星數比例圖表
  • 專案測試涵蓋率走勢圖表

10 of 87

EXAMPLE

11 of 87

如何獲取即時資訊?

12 of 87

不是它戳你,就是你戳它

13 of 87

它戳你

  • 方法
    • 服務的 Web Hooks 掛你提供的 API
  • 速度
    • 取決網路速度 (數秒鐘以內)

14 of 87

你戳它

  • 方法
    • 服務提供的 SDK 或 API
  • 速度
    • 排程取得 (最快每分鐘)
    • 自行寫 Daemon (數秒鐘)

15 of 87

在 Laravel 上怎麼做?

16 of 87

它戳你:�建立 token based API

17 of 87

Queue

token based

API

System

create job

send data

respoding

18 of 87

先寫驗證程式

19 of 87

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;

});

}

20 of 87

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();

// ...

});

}

// ...

21 of 87

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),

];

});

22 of 87

產生更新電池狀態的 Job 類別

$ php artisan make:job UpdateBatteryState

23 of 87

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;

}

// ...

24 of 87

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');

25 of 87

實際測試 API

26 of 87

Queue Driver 使用 redis

$ composer require predis/predis

27 of 87

建立測試使用者

$ 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"�>>>

28 of 87

用 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}"

29 of 87

你戳它:�利用 Commands 及 Schedule

30 of 87

以 Google Calendar 為例

$ composer require spatie/laravel-google-calendar(安裝細節請參考套件說明)

$ php artisan make:command FetchGoogleCalendarEvents

31 of 87

app/Console/Commands/FetchGoogleCalendarEvents.php

namespace App\Console\Commands;

// ...

class FetchGoogleCalendarEvents extends Command

{

protected $signature = 'dashboard:calendar';

protected $description = 'Fetch Google Calendar events.';

}

32 of 87

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();

// ...

33 of 87

app/Console/Kernel.php

class Kernel extends ConsoleKernel

{

protected $commands = [

Commands\FetchGoogleCalendarEvents::class,

];

protected function schedule(Schedule $schedule)

{

$schedule->command('dashboard:calendar')

->everyMinute(); // 每分鐘抓取一次活動資訊

}

34 of 87

如何將資訊即時傳送給用戶?

35 of 87

你訂閱,我推播

36 of 87

Pub/Sub

Channel

Publisher

Subscriber

Subscriber

Subscriber

PUBLISH

SUBSCRIBE

37 of 87

Laravel 本身並不提供�Pub/Sub 與 Subscriber

38 of 87

官方推薦: Pusher

39 of 87

但 Pusher 要錢錢

40 of 87

Open Source 就是能�找到別人自幹出來分享的玩意

41 of 87

Redis Pub/Sub

+�Laravel Echo Server

42 of 87

Redis Pub/Sub

Private Channel

"dashboard"

Laravel Event

Laravel Echo Server

PUBLISH

SUBSCRIBE

43 of 87

我們還需要 Event

44 of 87

以 Google Calendar 為例

$ php artisan make:event DashboardEvent

$ php artisan make:event GoogleCalendarEventsFetched

45 of 87

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');

}

}

46 of 87

app/Events/GoogleCalendarEventsFetched.php

namespace App\Events;

class GoogleCalendarEventsFetched extends DashboardEvent

{

/**

* @var array

*/

public $events;

public function __construct(array $events)

{

$this->events = $events;

}

}

47 of 87

app/Events/GoogleCalendarEventsFetched.php

use App\Events\GoogleCalendarEventsFetched;

class FetchGoogleCalendarEvents extends Command

{

// ...

public function handle()

{

// ...

event(new GoogleCalendarEventsFetched($events));

}

}

48 of 87

透過 laravel-echo-server 接收事件

49 of 87

安裝與初始化 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.

50 of 87

啟動 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!

51 of 87

如何不重整頁面就更新資訊?

52 of 87

一切的關鍵

Web Socket

53 of 87

Laravel Application

laravel-echo-server

Browser

Redis Pub/Sub

Vue Renderer

Web Socket

Publish

Subcribe

54 of 87

抽象化 Web Socket 的官方套件

Laravel Echo

55 of 87

Laravel Echo

透過 Socket.io 操作 Web Socket

56 of 87

Laravel Echo

laravel-echo-server

Socket.io

Channel

Laravel Event

Redis

Channel

57 of 87

Laravel Echo 範例

Echo.channel('orders')

.listen('OrderShipped', (e) => {

console.log(e.order.name);

})

.listen('EmailSent', (e) => {

console.log(e.message);� });

58 of 87

安裝 Laravel Echo

$ yarn add laravel-echo pusher-js socket-io.client

59 of 87

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'

});�

// ...

60 of 87

讓事件透過私人頻道

廣播給前端畫面

61 of 87

Laravel Echo

laravel-echo-server

/broadcasting/auth

private

channel

Is user logged in?

Laravel Event

private

channel

62 of 87

routes/web.php

Route::get('/', function () {

return view('dashboard');

})->middleware('auth.basic');

63 of 87

怎麼建立

/broadcasting/auth ?

64 of 87

app/Providers/BroadcastServiceProvider.php

class BroadcastServiceProvider extends ServiceProvider

{

public function boot()

{

Broadcast::routes();

Broadcast::channel('dashboard', function () {

return true;

});

}

}

65 of 87

66 of 87

建立以 Vue 構成的版面

67 of 87

把 Vue.js 2.0 整合到 Laravel 中

68 of 87

結果在準備題目時,�官方已經整合好了...

69 of 87

版面樣式參考:�dashboard.spatie.be

70 of 87

resources/views/layouts/master.blade.php

<!-- Laravel Echo 會需要它 -->

<meta name="csrf-token" content="{{ csrf_token() }}">

71 of 87

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

72 of 87

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

}

});

73 of 87

Vue component 整合 Laravel Echo

74 of 87

Vue component

Laravel Echo

Socket.io

receive event & payload

update data

75 of 87

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>

76 of 87

總結整個流程

77 of 87

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

78 of 87

加碼:如何設置正式環境

79 of 87

如何把 Laravel Worker �與 Larave Echo Server

設置為系統服務?

QUESTION

80 of 87

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�

81 of 87

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�

82 of 87

啟動 supervisor 服務

# 重讀設定檔�$ sudo supervisorctl reread�$ sudo supervisorctl update��# 執行 services�$ sudo supervisorctl start laravel-worker:*�$ sudo supervisorctl start laravel-echo-server:*�# 或是�$ sudo supervisorctl start all�

83 of 87

如何讓 Laravel Echo Server

共用 https 通道?

QUESTION

84 of 87

Laravel Application

laravel-echo-server

browser

https (443)

http (6001)

Nginx

85 of 87

Laravel Application

laravel-echo-server

browser

https (443)

proxy (6001)

Nginx

86 of 87

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;�}�

87 of 87

Q & A