[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.