Xây dựng ứng dụng thêm sửa xóa với Vue, Vuex, Vue Router và Laravel

Giới thiệu

Ai đã từng theo dõi các bài viết cũng mình có lẽ đã từng đọc qua bài viết Laravel 5.5 và React JS, hôm nay mình sẽ viết về một framework khác cũng thường được sử dụng với Laravel, đó là VueJs. Trước khi đọc bài viết này bạn cần có kiến thức cơ bản về LaravelVueJS hoặc có thể tìm hiểu qua tại đây: Laravel - Frontend, VueJS - Get Started.

Chuẩn bị

Tạo seeder cho bảng users:

php artisan make:seeder UsersTableSeeder

Thêm đoạn code này vào run() function trong file database/seeds/UsersTableSeeder:

factory(App\User::class, 50)->create();

Chạy migrate command

php artisan migrate

Chạy seed command chỉ định file seeder chúng ta vừa tạo:

php artisan db:seed --class=UsersTableSeeder

Cài đặt các js packages. Để chạy được command npm bạn cần cài đặt Npm và Nodejs

npm install

Cài đặt package vue-router để routing. Chạy command sau:

npm install --save vue-router

Cài đặt package vuex. Vuex là một thư viện quản lý state cho ứng dụng Vuejs. Nó hoạt động như một centralized store cho tất cả các thành phần trong một ứng dụng, với các quy tắc đảm bảo rằng state chỉ có thể được thay đổi khi có một action cụ thể đoán trước được. Nếu bạn đã từng tìm hiểu về ReactJs thì nó cũng giống như flux hay redux bên ReactJs vậy.

npm install --save vuex

Để recomplile tự động mỗi khi chúng ta thay đổi script ở thư mục assets chạy command sau:

npm run watch

Restful API đơn giản sử dụng Laravel

Restful API

Thêm file app\Http\Controllers\UserController.php với nội dung:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\User;

class UserController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $users = User::all();
        return response()
            ->json($users);
    }

    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        User::create($request->all());
        return response()
            ->json(['message' => 'Success: You have added an user']);
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id)
    {
        $user = User::find($id);
        if (! $user) {
            return response()
            ->json(['error' => 'The user is not exists']);
        }
        return response()
            ->json($user);
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(Request $request, $id)
    {
        $user = User::find($id);
        if (! $user) {
            return response()
            ->json(['error' => 'Error: User not found']);
        }
        $user->update($request->all());
        return response()
            ->json(['message' => 'Success: You have updated the user']);
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        $user = User::find($id);
        if (! $user) {
            return response()
            ->json(['error' => 'Error: User not found']);
        }
        $user->delete();
        return response()
            ->json(['message' => 'Success: You have deleted the user']);
    }
}

Đoạn code trên implement các function với mục đích như sau:

  • index: Lấy tất cả các users để hiển thị ngoại list
  • edit: Lấy user info ra màn edit
  • update: Cập nhật thông tin user
  • destroy: Xoá user

Routes

Chúng ta sửa file routes/web.php với nội dung như sau:

<?php
Route::group(['prefix' => 'api'], function () {
    Route::resource('users', 'UserController');    
});
Route::view('/{any}', 'welcome')
    ->where('any', '.*');

Điều chúng ta cần chú ý nhất ở đây là đoạn route này cần đặt cuối file để bắt tất cả những uri khác các các request trên vào view chứa reactjs.

Route::view('/{any}', 'welcome')
    ->where('any', '.*');

Đoạn routes này thì chỉ đơn giản là nhóm các api vào 1 group có prefix là api mà thôi:

Route::group(['prefix' => 'api'], function () {
    Route::resource('users', 'UserController');    
});

Sử dụng Vuejs làm frontend

Chỉnh sửa file view

Thay đổi file resources/views/welcome.blade.php với nội dung:

<!doctype html>
<html lang="{{ app()->getLocale() }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <title>Laravel 5.5 - ReactJS Example</title>
        <link rel="stylesheet" type="text/css" href="{{ asset('css/app.css') }}">
        <script type="text/javascript">
            window.Laravel = {!! json_encode([
                'baseUrl' => url('/'),
                'csrfToken' => csrf_token(),
            ]) !!};
        </script>
    </head>
    <body>
        <div id="app"></div>
        <script type="text/javascript" src="{{ asset('js/app.js') }}"></script>
    </body>
</html>

File view trên có thêm biến javascript window.Laravel sẽ chứa những giá trị động lấy từ Laravel sử dụng trong javascript. Chúng ta sẽ fill nội dung Vuejs vào thẻ div có id là app.

Tạo sẵn các file component và sửa file app.js

Đây là cấu trúc thư mục phần Vuejs sau khi hoàn thành:

Đầu tiên tạo sẵn các file App.vue, UserList.vue, UserRow.vue, CreateUser.vue, EditUser.vue trong resources\js\components.

File resources\js\api.js chứa URI của các APIs, sau này nếu muốn sửa chỉ cần sửa một nơi:

const RESOURCE_USER = 'api/users';

export {
    RESOURCE_USER
};

File resources\js\store\usersStore.js với nội dung:

import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
import {RESOURCE_USER} from '../api';

Vue.use(Vuex);

const usersStore = new Vuex.Store({
    state: {
        users: [],
        user: {},
    },
    mutations: {
        FETCH(state, users) {
            state.users = users;
        },
        FETCH_ONE(state, user) {
            state.user = user;
        },
    },
    actions: {
        fetch({ commit }) {
            return axios.get(RESOURCE_USER)
                .then(response => commit('FETCH', response.data))
                .catch();
        },
        fetchOne({ commit }, id) {
            axios.get(`${RESOURCE_USER}/${id}/edit`)
                .then(response => commit('FETCH_ONE', response.data))
                .catch();
        },
        deleteUser({}, id) {
            axios.delete(`${RESOURCE_USER}/${id}`)
                .then(() => this.dispatch('fetch'))
                .catch();
        },
        editUser({}, user) {
            axios.put(`${RESOURCE_USER}/${user.id}`, {
                name: user.name,
                email: user.email,
                password: user.password,
            })
                .then(() => this.dispatch('fetch'));
        },
        addUser({}, user) {
            axios.post(`${RESOURCE_USER}`, {
                name: user.name,
                email: user.email,
                password: user.password,
            });
        }
    }
});

export default usersStore;

Trung tâm của các ứng dụng Vuex chính là store, chứa toàn bộ các state của ứng dụng. Khi các components nhận được state từ nó, mỗi khi nó thay đổi sẽ được cập nhật một cách hiệu quả, và nó không thể bị thay đổi trực tiếp. Store bao gồm:

  • State: Chứa tất cả trạng thái của ứng dụng.
  • Mutations: Bạn chỉ có thể thay đổi store khi commit một mutation. Nó gần giống như event.
  • Actions: Giống với mutations, nhưng khác là:
  • Thay vì mutate trực tiếp state, nó commit qua các mutations .
  • Nó có thể chứa các các hành động bất đồng bộ. Ngoài ra còn có Getters, và Modules, các bạn có thể thao khảo ở trang tài liệu của Vuex: https://vuex.vuejs.org/guide/

Sửa nội dung file resources\js\app.js:

// resources\js\app.js

require('./bootstrap');

window.Vue = require('vue');

import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './components/App.vue';
import routes from './routes';
import usersStore from './store/usersStore';

Vue.use(VueRouter);

const router = new VueRouter({
    routes,
    // mode: 'history',
});
window.events = new Vue();


new Vue({
    el: '#app',
    render: h => h(App),
    router,
    store: usersStore,
});

Đầu tiên để có thể sử dụng được các thuộc tính của VueRouter chúng ta cần use nó

Vue.use(VueRouter);

Sau đó khởi tạo router

const router = new VueRouter({
    routes,
    // mode: 'history',
});

Khởi tạo vuejs với router, và store, chúng ta sẽ sử dụng component App làm layout:

new Vue({
    el: '#app',
    render: h => h(App),
    router,
    store: usersStore,
});

File resources/routes.js định nghĩa các routes trong ứng dụng Vuejs:

import CreateUser from './components/CreateUser'
import EditUser from './components/EditUser'
import UserList from './components/UserList'

const routes = [
    {
        path: '/',
        component: UserList,
        name: 'users.index',
    },
    {
        path: '/users/create',
        component: CreateUser,
        name: 'users.create',
    },
    {
        path: '/users/edit/:id',
        component: EditUser,
        name: 'users.edit',
    },
];
export default routes;

Path là đường dẫn hiển thị trên thanh địa chỉ, component là component sẽ được render vào layout, và name dùng để sử dụng để định danh route, giúp cho chúng ta có thể sử dụng một cách thuận tiện hơn. Ngoài ra còn có rất nhiều thuộc tính khác, các bạn có thể tham khảo ở link tài liệu https://router.vuejs.org/guide/essentials/named-routes.html

Tạo layout

Tạo file resources/js/components/App.vue với nội dung:

// resources/js/components/App.vue

<template>
    <div class="container">
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <a class="navbar-brand" href="#">Laravel 5.5 - ReactJS Example</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav mr-auto">
                    <li :class="['nav-item', {active: $route.name === 'users.index'}]">
                        <router-link class="nav-link" :to="{name: 'users.index'}">Users</router-link>
                    </li>
                    <li :class="['nav-item', {active: $route.name === 'users.create'}]">
                        <router-link class="nav-link" :to="{name: 'users.create'}">Add User</router-link>
                    </li>
                </ul>

            </div>
        </nav>
        <router-view></router-view>
    </div>
</template>

<script>
    export default {
    }
</script>

Ở đoạn code trên có component mới là <router-link> có sẵn trong VueRouter, tác dụng của nó là tạo ra thẻ <a/>, nhưng khác là nó có thể sử dụng được object $route, và nhiều hỗ trợ khác nữa bạn có thể tìm hiểu rõ hơn tại đây: https://router.vuejs.org/api/#router-link-props .

Ngoài ra còn có component khác là <router-view> chính là nơi các component UserList, CreateUser, EditUser được định nghĩa ở file routes.js được render.

Tạo component hiển thị danh sách users

User List

Tạo file resources/js/components/UserList.vue với nội dung:

// resources/js/components/UserList.vue

<template>
    <div>
        <h1>Users</h1>
        <div class="clearfix">
            <router-link class="btn btn-success pull-righ" :to="{name: 'users.create'}">Add User</router-link>
        </div><br />
        <table class='table table-hover'>
            <thead>
            <tr>
                <td>ID</td>
                <td>Name</td>
                <td>Email</td>
                <td>Actions</td>
            </tr>
            </thead>
            <tbody>
            <user-row v-for="user in users" :user="user" :key="user.id"></user-row>
            </tbody>
        </table>
    </div>
</template>

<script>
    import { mapState } from 'vuex';
    import UserRow from "./UserRow";
    export default {
        components: {UserRow},
        computed: {
            ...mapState(['users']),
        },
        created: function () {
            this.$store.dispatch('fetch');
        }
    }
</script>

Ở đoạn code trên, trong method created() chúng ta dispatch action fetch để lấy ra danh sách users.

Trong computed, map state users trong store vào component UserList. Tài liệu về helper mapState của Vuex: https://vuex.vuejs.org/guide/state.html#the-mapstate-helper.

User Row

Nội dung file resources/js/components/UserRow.vue:

// resources/js/components/UserRow.vue

<template>
    <tr>
        <td>
            {{ user.id }}
        </td>
        <td>
            {{ user.name }}
        </td>
        <td>
            {{ user.email }}
        </td>
        <td>
            <router-link class="btn btn-primary" :to="`/users/edit/${user.id}`">Edit</router-link>
        </td>
        <td>
            <button class="btn btn-danger" @click="deleteUser">Delete</button>
        </td>
    </tr>
</template>

<script>
    export default {
        props: {
            user: Object,
            handleDelete: Function,
        },
        methods: {
            deleteUser: function () {
                this.$store.dispatch('deleteUser', this.user.id);
            },
        }
    }
</script><template>
    <tr>
        <td>
            {{ user.id }}
        </td>
        <td>
            {{ user.name }}
        </td>
        <td>
            {{ user.email }}
        </td>
        <td>
            <router-link class="btn btn-primary" :to="{name: 'users.edit', params: {id: user.id}}">Edit</router-link>
            <button class="btn btn-danger" @click="deleteUser">Delete</button>
        </td>
    </tr>
</template>

<script>
    export default {
        props: {
            user: Object,
            handleDelete: Function,
        },
        methods: {
            deleteUser: function () {
                this.$store.dispatch('deleteUser', this.user.id);
            },
        }
    }
</script>

Component này chỉ đảm nhận việc hiển thị bản ghi ra và có một method deleteUser, sẽ dispatch action deleteUser dùng để xóa bản ghi đó.

Đây là màn hình user list:

Create user

Nội dung file resources/js/components/CreateUser.vue:

// resources/js/components/CreateUser.vue

<template>
    <div>
        <h1>Create An User</h1>
        <form @submit.prevent="add">
            <div class="form-group">
                <label for="name">Name</label>
                <input type="text" class="form-control" id="name" placeholder="Name"
                       v-model="name" required />
            </div>
            <div class="form-group">
                <label for="email">Email</label>
                <input type="email" class="form-control" id="email" placeholder="Email"
                       v-model="email" required />
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" class="form-control" id="password" placeholder="Password"
                       v-model="password" required />
            </div>
            <button type="submit" class="btn btn-primary">Add User</button>
        </form>
    </div>
</template>

<script>
    export default {
        data: function () {
            return {
                name: null,
                email: null,
                password: null,
            }
        },
        methods: {
            add: function () {
                this.$store.dispatch('addUser', {
                    name: this.name,
                    email: this.email,
                    password: this.password,
                });
                this.$router.push({name: 'users.index'});
            }
        }
    }
</script>

Component CreateUser bao gồm method add sẽ dispatch action addUser, sau đó push route users.index để quay về màn hình user list.

Màn hình create user:

Edit user

Nội dung file resources/js/components/EditUser.vue:

// resources/js/components/EditUser.vue

<template>
    <div>
        <h1>Edit The User</h1>
        <form @submit.prevent="edit">
            <div class="form-group">
                <label for="name">Name</label>
                <input type="text" class="form-control" id="name" placeholder="Name"
                       v-model="user.name" required />
            </div>
            <div class="form-group">
                <label for="email">Email</label>
                <input type="email" class="form-control" id="email" placeholder="Email"
                       v-model="user.email" required />
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" class="form-control" id="password" placeholder="Password"
                       v-model="user.password" required />
            </div>
            <button type="submit" class="btn btn-primary">Save User</button>
        </form>
    </div>
</template>

<script>
    import { mapState } from 'vuex';
    export default {
        methods: {
            edit: function () {
                this.$store.dispatch('editUser', this.user);
                this.$router.push({name: 'users.index'});
            }
        },
        beforeCreate: function () {
            this.$store.dispatch('fetchOne', this.$route.params.id);
        },
        computed: {
            ...mapState(['user']),
        }
    }
</script>

Component EditUser chỉ khác CreateUser ở method beforeCreate dispatch đến action fetchOne để lấy thông tin user và disptach tới action editUser để lưu thông tin user.

Màn hình edit user đã được fill data:

Kết

Trên đây là các bước cơ bản để làm chức năng thêm sửa xoá đơn giản với Laravel và Vuejs, bạn có thể tham khảo source code tại đây: https://github.com/thinhhung/LaravelVueJsTutorial