Xây dựng Vue SPA (Single Page App) với Laravel – Phần 2

Lần này, chúng ta sẽ Xây dựng một ứng dụng Vue SPA với Laravel bằng cách load dữ liệu bất động bộ (async) từ API bên trong một component Vue. Chúng ta cũng sẽ quan sát cách xử lý lỗi khi API trả về lỗi và cách show error trong giao diện.

Nếu bạn chưa đọc Phần 1 của bài viết này, chúng tôi đã giới thiệu cho các bạn về cách sử dụng một Vue SPA với Vue Router và Laravel backend. Nếu muốn làm theo dễ dàng hơn, hãy đọc qua phần một.

Để cho dữ liệu của server-side đơn giản, API sẽ sử dụng data mẫu. Trong Phần 3, chúng ta sẽ convert API sang controller với data test từ database.

API Route

Các ứng dụng Vue SPA là “stateless*”, có nghĩa là chúng ta thực hiện call API tới route của Laravel được định nghĩa trong file  routes/api.php. Các API route không sử dụng session state, nghĩa là ứng dụng của chúng ta thực sự là “stateless” ở back end

Note*: Stateless là thiết kế không lưu dữ liệu của client trên server. Có nghĩa là sau khi client gửi dữ liệu lên server, server thực thi xong, trả kết quả thì “quan hệ” giữa client và server bị “cắt đứt” – server không lưu bất cứ dữ liệu gì của client. Như vậy, khái niệm “trạng thái” ở đây được hiểu là dữ liệu.

Trong ví dụ này, giả sử chúng ta muốn có danh sách user mà có thể sử dụng để xem một request bất đồng bộ cho phần backend từ ứng dụng Vue:

Route::get('/users', function () {
    return factory('App\User', 10)->make();
});

Các route tạm sẽ sử dụng model factories để tạo ra một collection của các model Eloquent mà nó có thể sinh ra lượng lớn các dữ liệu vào trong database một cách tiện lợi. Chúng ta sử dụng phương thức make(), nó sẽ không insert dữ liệu test lên databaser mà là trả về một tập các trường hợp (instance*) App\User mới vốn chưa tồn tại.

Note*: Instance đơn thuần là thuật ngữ chỉ một trường hợp, một ví dụ cụ thể. Trong lĩnh vực phần mềm, instance trước đây được định nghĩa như là một dạng cấu trúc dữ liệu, một chương trình máy tính hoặc một phương thức được triển khai, xử lý và chạy được trên bộ nhớ ( memory). Đặc biệt, trong lập trình hướng đối tượng, dựa trên định nghĩa về class, instance được hiểu là một đối tượng được triển khai dưới dạng tập hợp dữ liệu trên bộ nhớ. Class như một dạng văn phòng phẩm, và trong đó có rất nhiều instance mang nội dung khác nhau.

Define route trong routes/api.php có nghĩa là các request của chúng ta tiền tố /api, vì tiền tố này cũng được xác định trong class RouteServiceProvider của ứng dụng:

protected function mapApiRoutes()
{
    Route::prefix('api')
         ->middleware('api')
         ->namespace($this->namespace)
         ->group(base_path('routes/api.php'));
}

Resoure của user lấy từ /api/users

[
   {
      "name":"Nguyen Huu Binh",
      "email":"[email protected]"
   },
   {
      "name":"Nguyen Huu Minh Nhat",
      "email":"[email protected]"
   },
   {
      "name":"Le Dang Hieu",
      "email":"[email protected]"
   },
   ...
]

Route ở client

Phần 1 chúng ta đã build 1 mớ các route trong resources/assets/js/app.js để điều hướng cho SPA. Bất cứ khi nào chúng ta muốm thêm route mới, chúng ta tạo object mới trong mảng routes mà nó define đường dẫn, tên và component của mỗi route. Route mới tạo là /users

import UsersIndex from './views/UsersIndex';

const router = new VueRouter({
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'home',
            component: Home
        },
        {
            path: '/hello',
            name: 'hello',
            component: Hello,
        },
        {
            path: '/users',
            name: 'users.index',
            component: UsersIndex,
        },
    ],
});

UsersIndex Component

Router define 1 route bằng cách sử dụng component UsersIndex . Hãy cùng xem resources/assets/js/views/UsersIndex.vue

<template>
    <div class="users">
        <div class="loading" v-if="loading">
            Loading...
        </div>

        <div v-if="error" class="error">
            {{ error }}
        </div>

        <ul v-if="users">
            <li v-for="{ name, email } in users">
                <strong>Name:</strong> {{ name }},
                <strong>Email:</strong> {{ email }}
            </li>
        </ul>
    </div>
</template>
<script>
import axios from 'axios';
export default {
    data() {
        return {
            loading: false,
            users: null,
            error: null,
        };
    },
    created() {
        this.fetchData();
    },
    methods: {
        fetchData() {
            this.error = this.users = null;
            this.loading = true;
            axios
                .get('/api/users')
                .then(response => {
                    console.log(response);
                });
        }
    }
}
</script>

Nếu bạn mới bắt đầu tiếp cận Vue, thì sẽ thấy 1 số concept không quen dùng ở đây. Tôi recommend bạn đọc tài liệu về Vue components và làm quen với Vue lifecycle hooks (created, mounted, etc.).

Trong component này, chúng ta nạp dữ liệu bất đồng bộ trong khi component tạo hook. Chúng ta define phương thức fechData() để reset các lỗi và các thuộc tính của user thành null, set loading thành true.

Dòng cuối cùng của phương thức fetchData() sử dùng thư viện Axios để tạo HTTP request tới API. Axios là gì? Nó là một thư viện HTTP client dựa trên promise, chúng ta sử dụng nó để kết nối then() callback nơi mà ta log các phản hồi và set nó vào thuộc tính của data users.

Đây là những gì trong console, data sẽ trông giống như thế này khi bạn load /users trong ứng dụng (ở client page, không phải API)

Hoàn tất Route Component

Chúng ta có route và component cho /users, bây giờ hãy móc các link điều hướng trong App component và sau đó thiết lập data user từ các response. Trong resources/assets/js/views/App.vue:

<template>
    <div>
        <h1>Vue Router Demo App</h1>

        <p>
            <router-link :to="{ name: 'home' }">Home</router-link> |
            <router-link :to="{ name: 'hello' }">Hello World</router-link> |
            <router-link :to="{ name: 'users.index' }">Users</router-link>
        </p>

        <div class="container">
            <router-view></router-view>
        </div>
    </div>
</template>
<script>
    export default {}
</script>

Bước tiếp theo hãy update UsersIndex.vue để thiết lập user data:

fetchData() {
    this.error = this.users = null;
    this.loading = true;
    axios
        .get('/api/users')
        .then(response => {
            this.loading = false;
            this.users = response.data;
        });
}

Giờ refresh lại page trên trình duyệt, bạn sẽ thấy:

Tham khảo thêm các vị trí tuyển lập trình Laravel cho bạn

Xử lý lỗi

Nói chung là cũng mong các component của chúng ta nên hoạt động tốt, tuy nhiên cũng nên phòng hờ có sự cố xảy ra và ta phải có những action để handle nó cũng như thông báo với người dùng. Bây giờ ta thử add một error giả lập từ server cho API:

Route::get('/users', function () {
    if (rand(1, 10) < 3) {
        abort(500, 'We could not retrieve the users');
    }

    return factory('App\User', 10)->make();
});

Chúng ta sử dụng rand() để tử chối request nếu số nhỏ hơn 3. Nếu bạn refresh page vài lần thì bạn nên thấy “Loading…”, và nếu bạn inspect phần developer tools của browser thì sẽ thấy 1 uncaught exception từ request của Axios:

Chúng ta handle request thất bại bằng cách kết hợp 1 catch() callback vào Axios Promise:

fetchData() {
    this.error = this.users = null;
    this.loading = true;
    axios
        .get('/api/users')
        .then(response => {
            this.loading = false;
            this.users = response.data;
        }).catch(error => {
            this.loading = false;
            this.error = error.response.data.message || error.message;
        });
}

Chúng ta thiết lập việc loading thuộc tính của data thành false và sử dụng error exception để cố gắng thiết lập một message key từ phản hồi. Cái message trả về đặc tả thuộc tính exception.message.

Để thân thiện với người dùng, bạn nên để cho user một nút “Try Again” cho điều kiện này trong UsersIndex.vue template, để họ dễ dàng call phương thức fetchData để làm mới users property*:

(Giải thích cơ bản cho phần property: Attribute là thuộc tính của phần tử DOM. Property là thuộc tính của đối tượng Javascript.)

<div v-if="error" class="error">
    <p>{{ error }}</p>

    <p>
        <button @click.prevent="fetchData">
            Try Again
        </button>
    </p>
</div>

Bây giờ check xem, nếu fail thì giao diện sẽ thế nào?

TopDev lược dịch từ Laravel-News