[Laravel] Single Page Application sử dụng Vue, JWTAuth (P1)
Bài đăng này đã không được cập nhật trong 3 năm
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
Khái lược
Đầ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.
Install
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_.+\n//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="width=device-width, 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.
Tạo Model
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 Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
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 Illuminate\Database\Eloquent\Model;
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 App\Http\Controllers;
use Illuminate\Http\Request;
use App\Task;
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 Illuminate\Http\Request;
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 \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\Task::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'is_done' => mt_rand(0, 1),
];
});
database/seeds/DatabaseSeeder.php
<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Chạy database seeds.
*
* @return void
*/
public function run()
{
factory(App\Task::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 Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
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.
All rights reserved