12/08/2018, 16:18

[Laravel] Single Page Application sử dụng Vue, JWTAuth (P1)

Trong loạt bài viết đọc được từ qiita nổi tiếng, tôi xin dịch và chia sẻ lại nội dung trên Viblo bằng tiếng Việt. Loạt bài ngắn này chia sẻ tut kết hợp giữa Laravel 5.4 với Vue.js và JWTAuth. Phần đầu tiên sẽ có những nội dung chính sau : Khái lược Install Tạo model Đầu tiên tôi xin ...

Trong loạt bài viết đọc được từ qiita nổi tiếng, tôi xin dịch và chia sẻ lại nội dung trên Viblo bằng tiếng Việt. Loạt bài ngắn này chia sẻ tut kết hợp giữa Laravel 5.4 với Vue.js và JWTAuth. Phần đầu tiên sẽ có những nội dung chính sau :

  • Khái lược
  • Install
  • Tạo model

Đầu tiên tôi xin chia sẻ lại lại những cái mà tôi đã học được. Q - Tại sao lại làm Single Page Application (SPA) ? A - Đó là vì nó có tính trải nghiệm tốt cho user. Tính tương thích với mobile tốt. Q - Vậy sao lại dùng Laravel ? A - Laravel rất cool ! Xử lý phía frontend đã giao phó cho NodeJS (trên 5.4 là Webpack) nên đơn giản. Nhẹ nhàng mà ít vấn đề xảy ra như là quan hệ phụ thuộc lẫn nhau. Những cái hỗ trợ rất tốt, mạnh mẽ như là Model Factory, Faker, PHPUnit. Q - Vậy còn Vue.js ? A - Tương thích tốt với Laravel, tôi thấy nhiều người khuyên dùng. Mà cài đặt Laravel là Vue.js đã được cài theo. Nó cũng là một JS Framework mới, hiện đại. Dễ dàng xây dựng ứng dụng SPA. Những cái tôi dùng là :

  • vue-router
  • vue-spinner
  • axios

Nếu mà ứng dụng quy mô lớn hơn thì có thể dùng vuex - phương pháo quản lý trạng thái. Q - Thế còn JWTAuth là gì? A - Đây là một phương thức chứng thực đơn giản. Nói một cách khái quát nhất thì như sau :

  • Client : gửi user name và password đến server
  • Server : Tiến hành login, nếu thành công sẽ trả về Json Web Token
  • Client : Nhận lấy Token, với những lần sau thì sẽ thêm Authentication vào Request Header (vd : Authentication: Bearer "yowqeh43gb093fh023.....orhgoerg=" ). Lưu Token local (cookie) và cho đến khi expired thì trạng thái login sẽ được duy trì. Khi logout thì Token đó sẽ bị huỷ.

Note : Tôi đã quyết định dùng JWTAuth theo như nội dung Q&A tôi đã đọc :

OAuth or JWT? Which one to use and why? http://stackoverflow.com/questions/32964774/oauth-or-jwt-which-one-to-use-and-why

If you want simple stateless http authentication to an api, then JWT is just fine and relatively quick to implement, even for a novice developer.

Vue.js cũng tương tự như React là hướng component. Ví dụ như tôi tạo file có đuôi .vue như dưới và kết hợp trên Webpack, rồi truyền cho trình duyệt.

<template>
  <h1>Hello, {{ name }}</h1>
</template>

<script>
  export default {
    data () {
      return {
        name: 'HuongNV'
      }
    },
  }
</script>

<style>
  .h1 {
    border-bottom: 1px;
  }
</style>

↓ Output

<h1>Hello, HuongNV</h1>

Tôi thấy rất dễ dàng làm quen và sử dụng.

Các bước cài đặt của Laravel :

composer create-project --prefer-dist laravel/laravel spa-todo 5.4
cd spa-todo

Kế đến là Database, do để demo thôi nên tôi sẽ dùng sqlite.

perl -i -pe 's/DB_.+
//g' .env
echo 'DB_CONNECTION=sqlite' >> .env
touch database/database.sqlite

Thực hiện migrate :

php artisan migrate
# => Migrated: 2017_10_12_000000_create_users_table
# => Migrated: 2017_10_12_100000_create_password_resets_table

Thêm vào modules cần thiết cho ứng dụng SPA.

// Cài đặt yarn
npm install -g yarn
// Thêm vue-router
// tương đương với npm install --save-dev vue-router
yarn add vue-router vue-spinner --dev

Chỉnh sửa file package.json. Khi không gắn dist trước bin/ thì sẽ không hoạt động được.

package.json
{
  ...,
  "scripts": {
    "dev": "node_modules/cross-env/dist/bin/cross-env.js ...",
    "watch": "node_modules/cross-env/dist/bin/cross-env.js ...",
    "hot": "node_modules/cross-env/dist/bin/cross-env.js ...",
    "production": "node_modules/cross-env/dist/bin/cross-env.js ...",
  },
  ...
}

Note : nếu mà sử như dưới thì path nó sẽ nhìn dễ chịu hơn.

// Cần thêm
yarn add cross-env --dev
// Sửa lại
package.json
{
  ...,
  "scripts": {
    "dev": "cross-env.js ...",
    "watch": "cross-env.js ...",
    "hot": "cross-env.js ...",
    "production": "cross-env.js ...",
  },
  ...
}

Cấu trúc của thư mục Laravel :

  • Frontend : resources/
  • Server side : app
  • Routing : routes/
.
|-- package.json
|-- resources
|   |-- views
|   |   `-- app.blade.php
|   `-- assets
|       `-- js
|           `-- app.js
`-- routes
    `-- web.php

Trước khi đi vào bắt đầu ứng dụng, hãy thử truyền View vào, routing của trình duyệt sẽ do Vue.js nó làm :

// routes/web.php
<?php
Route::get('/{any}', function () {
    return view('app');
})->where('any', '.*');

Tôi sẽ tạo View làm điểm đầu vào cho SPA :

// resources/views/app.blade.php
<!DOCTYPE html>
<html lang="{{ config('app.locale') }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="awidth=device-awidth, initial-scale=1">

        <title>Vue TODO</title>

        <link rel="stylesheet" href="css/app.css">

        <script>
            window.Laravel = {};
            window.Laravel.csrfToken = "{{ csrf_token() }}";
        </script>
    </head>
    <body>
        <div id="app">
          <example></example>
        </div>
    </body>

    <script src="js/app.js"></script>
</html>

Phần <div id="app"> ... </div> sẽ do thằng Vue.js thao tác.

// resources/assets/js/app.js
require('./bootstrap');

Vue.component('example', require('./components/Example.vue'));

const app = new Vue({
    el: '#app'
});

Khởi tạo xong bạn hãy thử vào localhost:8000 sẽ thấy Vue.js hiển thị :

yarn run dev
php artisan serve

Note : nếu mà bạn dùng yarn run watch thì mỗi khi có thay đổi nó sẽ tìm và build JS cho bạn.

Phần chính sẽ làm ở bước này sẽ là implement API server sử dụng chức năng REST API của Laravel. Do là ứng dụng SPA TODO nên tôi sẽ tạo bảng tasks và định nghĩa RESTful routing đến nó. Và tôi cũng sẽ tạo cả unit test.

Migration

Đầu tiên tôi tiến hành mirgration tạo ra bảng tasks.

php artisan make:migration create_tasks_table
# => Created Migration: 2017_10_12_140557_create_tasks_table

Rồi sửa bên trong nó.

// database/migrations/2017_03_16_140557_create_tasks_table.php
<?php

use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;

class CreateTasksTable extends Migration
{
    /**
     * Chạy migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tasks', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name')->nullable(false);
            $table->boolean('is_done')->default(false);

            $table->timestamps();
        });
    }

    /**
     * Bỏ migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('tasks');
    }
}
php artisan migrate
# => Migrated: 2017_10_12_140557_create_tasks_table

Model

Tiếp theo sẽ tạo Model

php artisan make:model Task
# => Model created successfully.

Cũng tiến hành sửa nội dung :

// app/Task.php
<?php

namespace App;

use IlluminateDatabaseEloquentModel;

class Task extends Model
{

    protected $fillable = ['name', 'is_done'];

    protected $casts = [
        'is_done' => 'boolean',
    ];
}

Biến $casts ở đây là setting để biến đổi 1 => true khi API Server gửi JSON.

Controller

Tôi cần tạo ra REST Controller liên quan đến bảng tasks.

php artisan make:controller TaskController
# => Controller created successfully.

Rồi cũng sửa như sau :

app/Http/Controllers/TaskController.php
<?php

namespace AppHttpControllers;

use IlluminateHttpRequest;
use AppTask;

class TaskController extends Controller
{
    public function index()
    {
        return Task::take(5)->get()->keyBy('id');
    }

    public function store(Request $request)
    {
        return Task::create($request->only('name'))->save()->fresh();
    }

    public function destroy($id)
    {
        return Task::destroy($id);
    }

    public function update($id, Request $request)
    {
        return Task::find($id)->fill($request->only('is_done'))
               ->save()->fresh();
    }
}

Những xử lý lỗi, validation, policy, status code còn thiếu nhiều nhưng lần này tôi chưa thêm vào.

Routing

Từ bản Laravel 5.4 thì Web và API được chia ra từ đầu nên rất tiện.

// routes/api.php
<?php

use IlluminateHttpRequest;

Route::group(['middleware' => 'api'], function () {
    Route::resource('tasks',  'TaskController');
});

Vậy đã xong, routing của REST Controller đã được thiết lập.

// Chưa có gì
curl -XGET localhost:8000/api/tasks
[]
// POST thử
curl -XPOST localhost:8000/api/tasks -d 'name=Learn Vue.js'
{
  "1": {
    "id": 1,
    "name": "Learn Vue.js",
    "is_done": false,
    "created_at": "2017-10-16 22:09:13",
    "updated_at": "2017-10-16 22:09:13"
  }
}
// Cập nhật thử
curl -XPUT localhost:8000/api/tasks/1 -d 'is_done=true'
{
  "id": 1,
  "name": "Learn Vue.js",
  "is_done": true,
  "created_at": "2017-10-16 22:09:13",
  "updated_at": "2017-10-16 22:12:56"
}
// Đã check ok, xoá đi và kiểm tra lại
curl -XDELETE localhost:8000/api/tasks/1
curl -XGET localhost:8000/api/tasks
[]

Seeding

Mỗi lần test lại tạo dữ liệu thì sẽ tốn công nên tôi sẽ dùng seeding để tự động hóa nó.

// database/factories/ModelFactory.php
<?php

/** @var IlluminateDatabaseEloquentFactory $factory */

$factory->define(AppTask::class, function (FakerGenerator $faker) {
    return [
        'name' => $faker->name,
        'is_done' => mt_rand(0, 1),
    ];
});
database/seeds/DatabaseSeeder.php
<?php

use IlluminateDatabaseSeeder;

class DatabaseSeeder extends Seeder
{
    /**
     *  Chạy database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(AppTask::class, 50)->create();
    }
}
// Chạy artisan
php artisan db:seed

Xác nhận

curl -XGET localhost:8000/api/tasks

{
  "1": {
    "id": 1,
    "name": "Thora Strosin",
    "is_done": false,
    "created_at": "2017-10-16 22:39:49",
    "updated_at": "2017-10-16 22:39:49"
  },
  "2": {
# ...
  },
  "5": {
    "id": 5,
    "name": "August Denesik",
    "is_done": true,
    "created_at": "2017-10-16 22:39:49",
    "updated_at": "2017-10-16 22:39:49"
  }
}

Khi mà muốn reset cơ sở dữ liệu thì chỉ cần chạy lệnh artisan :

php artisan migrate:refresh --seed

Unittest

Tôi sẽ viết cả Unit Test. Đầu tiên là tôi sẽ dùng SQLite memory trên môi trường test.

phpunit.xml
        <!-- Thêm vào -->
        <env name="DB_DATABASE" value=":memory:"/>

Thực hiện đọc tên DB từ biến môi trường :

// config/database.php
<?php
    // ...

    'connections' => [

        'sqlite' => [

            // ...
            'database' => env('DB_DATABASE', database_path('database.sqlite')),
            // ...

Cuối cùng sửa để Laravel sẽ trỏ đến mỗi trường test.

// tests/CreatesApplication.php
<?php

    // ...

    public function createApplication()
    {
        if (file_exists(__DIR__.'/../bootstrap/cache/config.php'))
            unlink(__DIR__.'/../bootstrap/cache/config.php');
        // ...

Tới đây việc chuẩn bị đã xong, giờ là đến việc viết test :

tests/Feature/TaskTest.php
<?php
namespace TestsFeature;

use TestsTestCase;
use IlluminateFoundationTestingDatabaseMigrations;
use IlluminateFoundationTestingDatabaseTransactions;

class TaskTest extends TestCase
{

    use DatabaseMigrations;
    use DatabaseTransactions;

    public function testCrudTask()
    {
        $this->json('POST', '/api/tasks', ['name' => 'Learn Vue.js'])
             ->assertStatus(200)
             ->assertJson([
                'id'   => 1,
                'name' => 'Learn Vue.js',
                'is_done' => false,
            ]);

        $this->assertDatabaseHas('tasks', [
                'id'   => 1,
                'name' => 'Learn Vue.js',
                'is_done' => false,
            ]);

        $this->json('GET', '/api/tasks')
             ->assertStatus(200)
             ->assertJson([
                1 => [
                    'id'   => 1,
                    'name' => 'Learn Vue.js',
                    'is_done' => false,
                ]
            ]);

        $this->json('PUT', '/api/tasks/1', ['is_done' => true])
             ->assertStatus(200)
             ->assertJson([
                'id'   => 1,
                'name' => 'Learn Vue.js',
                'is_done' => true,
            ]);

        $this->assertDatabaseHas('tasks', [
                'id'   => 1,
                'name' => 'Learn Vue.js',
                'is_done' => true,
            ]);

        $this->json('DELETE', '/api/tasks/1')
             ->assertStatus(200);

        $this->assertDatabaseMissing('tasks', [
                'id'   => 1,
            ]);
    }
}

Khi mà tôi thử chạy thì kết quả thấy ok - passed :

./vendor/bin/phpunit 

PHPUnit 5.7.16 by Sebastian Bergmann and contributors.

..                                                      2 / 2 (100%)

Time: 107 ms, Memory: 14.00MB

OK (2 tests, 11 assertions)

Vậy là xong phía Server, trong bài tiếp theo tôi sẽ tiếp tục công việc bên Front end như về root component, child component, sử dụng router, axios ... và kết thúc bằng việc dùng JWTAuth.

0