Forum dạng SPA với Laravel và Vue.js – Phần 4: Hiển thị dữ liệu từ API

Trong phần trước chúng ta đã xây dựng các API thao tác với dữ liệu các đối tượng trong ứng dụng Forum, như trong mô hình phân lớp Fontend và Backend, các API là cách thức để các tầng ứng dụng giao tiếp với nhau. Bài viết này chúng ta sẽ tập trung vào vấn đề lấy dữ liệu từ API và hiển thị dữ liệu này lên giao diện.

Mô hình ứng dụng đa tầng sử dụng Laravel + Vuejs

Như vậy, trong bài viết này chúng ta sẽ tập trung vào Vue.js thực hiện lấy dữ liệu và hiển thị dữ liệu trên giao diện người dùng.

1. Hiển thị dữ liệu Category

Dữ liệu chúng ta đã thực hiện thông qua API http://spa-forum.dev/categories, các công việc để hiển thị dữ liệu Category cần làm như sau:

  • Lấy dữ liệu từ API và lưu vào các mảng.
  • Tạo ra hai Vue component, một để hiển thị danh sách các danh mục và component còn lại để hiển thị danh sách chủ đề trong một danh mục.

Chúng ta sẽ thực hiện xây dựng danh sách danh mục trong diễn đàn trước, để lấy dữ liệu từ máy chủ chúng ta phải thực hiện các request dạng Ajax đến các api được tạo ra từ phần xây dựng API cho category, topic và comment. Trước đây, Vue.js có một thư viện mở rộng vue-resources để xây dựng các request và response web, tuy nhiên hiện Vue-resource đã không còn được duy trì, thay vào đó Vue.js khuyến cáo sử dụng các gói thư viện khác làm rất tốt việc này, ví dụ axios. Laravel 5.4 cũng đưa Axios mặc định vào trong cấu hình Laravel Mix, do vậy trong phần thiết lập khung dự án chúng ta đã cài đặt axios.

Diễn đàn sẽ hiển thị các danh mục ngay tại trang chủ, do đó chúng ta sẽ thực hiện thay đổi component HomeView.vue. Dữ liệu được lấy từ API sẽ lưu vào một mảng, thực hiện khai báo trong phần thẻ <script> trong HomeView.vue:

<script>
export default {
  data () {
    return {
      categories: []
    }
  }
}
</script>

Câu hỏi quan trọng nhất là ở đâu trong <script> sẽ thực hiện yêu cầu Ajax để lấy dữ liệu về danh mục? Để trả lời câu hỏi này bạn cần hiểu hơn về vòng đời thực thể trong Vue.js, chúng ta sẽ sử dụng hook mounted().

<script>
export default {

  data () {
    return {
      categories: []
    }
  },

  mounted () {
    this.categories = ['xin chào','Allaravel']
  }
}
</script>

và nếu bạn kiểm tra trên Vue Devtool bạn sẽ thấy:

Vue DevTools

 

Nhưng ở đây, chúng ta không chỉ muốn đưa vào categories mảng [‘xin chào’,’Allaravel’] mà muốn lấy dữ liệu từ server từ API http://spa-forum.dev/api/categories như đã thiết lập ở bài trước. Chúng ta có thể sử dụng axios là các HTTP client giúp thực hiện các HTTP request. Hiện tại vue-resources đã không còn được duy trì bởi Vue.js do có nhiều gói thư viện ngoài làm tốt hơn, ví dụ như axios chẳng hạn. Axios cũng được Laravel sử dụng làm HTTP client mặc định trong các dự án, do vậy khi thực hiện npm install ở bài Thiết lập môi trường, chúng ta đã cài đặt sẵn axios. Trong ứng dụng mẫu của Laravel cũng đã cài đặt sẵn axios, bạn xem file bootstrap.js nằm trong thư mục resources/assets/js

window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
    window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
    console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}

Bây giờ chúng ta sẽ sử dụng axios để lấy dữ liệu từ API http://spa-forum.dev/api/categories. Thay đổi hook mounted() trong Home.vue:

mounted () {
	axios.get('api/categories').then((response) => {
        this.categories = response.data;
    })
}

Kiểm tra lại Vue DevTools xem thế nào?

Kết quả trên Vue DevTools khi sử dụng axios

Ok, vậy là chúng ta thấy categories đã chứa mảng các dữ liệu danh mục lấy ra từ hệ thống API. Như vậy, công việc lấy dữ liệu từ các API đã hoàn thành, tiếp theo chúng ta sẽ thực hiện hiển thị dữ liệu này lên trang.

Chúng ta có một danh sách các danh mục và chúng ta sẽ sử dụng v-for để chạy qua danh sách này và hiển thị lên các danh mục bằng template <category>. Tạo một file Category.vue để chứa component hiển thị cho một danh mục.

<template>
    <div class="panel panel-success">
        <div class="panel-heading">
            <router-link :to="{ name: 'Category', params: { categoryId: category.id } }" v-text="category.name"></router-link>
        </div>
        <div class="panel-body">
            {{ category.description }}
        </div>
        <div class="panel-footer">
            <span>{{ category.numberOfPosts }}</span>
            <small>Chủ đề</small>
        </div>
    </div>  
</template>
<script>
export default {
    props: ['category']
}
</script>

Trong Category.vue chúng ta cần để ý đến thuộc tính props là cách thức truyền dữ liệu từ component cha sang component con. Thay đổi lại nội dung HomeView.vue:

<template>
	<div>
		<category v-for="category in categories" :category="category" :key="category.id"></category>
	</div>
</template>
<script>
	import Category from '../Category.vue';
	export default {
		
		components: { Category },

		data () {
			return {
				categories: []
			}
		},
		mounted () {
			axios.get('http://spa-forum.dev/api/categories').then((response) => {
		        this.categories = response.data
		    })
		}
	}
</script>

Ok, giờ quay lại http://spa-forum.dev chúng ta sẽ thấy rằng các danh mục được lấy từ api đã được hiển thị.

Hiển thị Category trong SPA Forum

2. Hiển thị các Topic

Tương tự với danh mục, khi click vào một danh mục ứng dụng sẽ chuyển đến danh sách các chủ đề liên quan đến danh mục này. Chúng ta sẽ thay đổi CategoryView.vue để hiển thị các chủ đề liên quan đến một danh mục:

<template>
	<div>
		<h2>Các chủ đề trong danh mục {{ categoryName }}</h2>
		<topic v-for="topic in topics" :topic="topic" :key="topic.id"></topic>
	</div>
</template>

<script>
import Topic from '../Topic.vue';

export default {

	components: { Topic },

	data () {
		return {
			topics: [],
			categoryId: this.$route.params.categoryId,
			categoryName: this.$route.params.categoryName
		}
	},
	mounted () {
		axios.get('/api/categories/' + this.$route.params.categoryId + '/topics').then((response) => {
			this.topics = response.data.data;
		})
	}
}
</script>

Trong CategoryView.vue có v-for xử lý danh sách các chủ đề lấy từ API http://spa-forum.dev/api/category/{id}/topics, chúng ta xây dựng component cho <topic>, tạo file Topic.vue trong components với nội dung như sau:

<template>
    <div class="panel panel-success">
        <div class="panel-heading">
            <router-link :to="{ name: 'Topic', params: { topicId: topic.id } }" v-text="topic.title"></router-link>
        </div>
        <div class="panel-body">
            <span>by {{ topic.user.name }}</span>
            <span>{{ topic.time }}</span>
        </div>
        <div class="panel-footer">
            <span>{{ topic.views }}</span>
            <small>Lượt xem</small>
        </div>
    </div>  
</template>

<script>
export default {

  props: ['topic']
}
</script>

Component Topic sẽ nhận được dữ liệu từ component cha là CategoryView thông qua props. Như vậy đã xong, chúng ta thử vào ứng dụng từ đầu xem sao:

  • Vào http://spa-forum.dev, tiếp theo click vào một danh mục bất kỳ để vào tiếp danh sách các chủ đề của danh mục, kết quả như sau.

Các chủ đền liên quan đến một danh mục trong SPA Forum

Có một số điểm các code chúng ta viết chưa đáp ứng được:

  • Thời gian vẫn ở dạng Unix timestamp là một số được tính bằng số giây tính từ mốc thời gian 1/1/1970 đến hiện tại, chúng ta cần định dạng lại theo định dạng có thể đọc được kiểu như dd/mm/yyyy.
  • Lượt xem phải định dạng lại, nếu lượt xem là 1,000 hiển thị là 1K, 1,000,000 sẽ hiển thị là 1M …

3. Sử dụng Vue filters định dạng lại dữ liệu trước khi hiển thị

Vue.js cung cấp các filter giúp xử lý dữ liệu trước khi thực hiện render.

3.1 fromNow Filter

Chúng ta sẽ tạo ra một thư mục filters để chứa các Vue filter tiện cho quản lý mã nguồn. Trong thư mục này tạo timeFilter.js với nội dung sau:

import moment from 'moment';

export function fromNow (time) {
  return moment(time, 'X').fromNow();
}

Filter này sử dụng gói thư viện moment do đó chúng ta cần cài đặt gói này bằng câu lệnh npm install moment –save

Để sử dụng filer fromNow trong file app.js chúng ta import và khai báo filter này như sau:

import fromNow from './filters/timeFilter';
//… 

Vue.filter('fromNow', fromNow);

Và trong Topic.vue chúng ta sử dụng filter này như sau:

<span>{{ topic.time | fromNow }}</span>

3.2 largeNumber Filter

Filter này giúp xử lý các số lớn thành dạng rút gọn: 1K, 1M… Chúng ta tạo tiếp largeNumber.js trong filters với nội dung:

export function largeNumber (number) {
    if (number > 999999) {
        return (number / 1000000).toFixed(1) + 'M';
    }
    
    if (number > 999) {
        return (number / 1000).toFixed(1) + 'k';
    }

    return number;
}

Filter này không dùng thư viện ngoài nào, do đó không cần cài đặt gì thêm. Tiếp tục, để sử dụng được filter này chúng ta import và đăng ký với Vue trong app.js

import largeNumber from './filters/largeNumber';
//… 

Vue.filter('largeNumber', largeNumber);

Và sử dụng filter này trong Topic.vue như sau:

<span>{{ topic.views | largeNumber }}</span>

Ok, chúng ta quay lại màn hình danh sách chủ đề trong một danh mục xem thế nào?

Sử dụng Vue Filter trong xu ly dữ liệu

Như vậy thời gian đã ở định dạng đọc được và số lượt view nhìn dễ hơn nhiều rồi.

4. Phần tiếp theo

Phần công việc trong bài viết này có lẽ là một trong những phần quan trọng nhất trong các công việc xây dựng Forum dạng ứng dung đơn trang bằng Laravel + Vue.js. Trong bài viết này chúng ta đã có được một số kiến thức xử lý quan trọng trong các xây dựng ứng dụng:

  • Lấy dữ liệu từ các API thông qua Axios hoặc Vue-router hiển thị thông qua Vue component.
  • Sử dụng Vue filter xử lý dữ liệu.

Trong phần tiếp theo chúng ta thực hiện các vấn đề liên quan nâng cao giao diện người dùng như phân trang, hiển thị đường dẫn và hiển thị trạng thái tải dữ liệu.

7 thoughts on “Forum dạng SPA với Laravel và Vue.js – Phần 4: Hiển thị dữ liệu từ API

  1. có ai bị lỗi như em không ạ. Em reload lại trang spa-forum.dev/category thi bị mất dữ liệu và báo lỗi, và categoryId và cateogryName undefined mong mọi nguời hỗ trợ. em cảm ơn ạ

  2. ở chỗ topic.user.name làm thế nào để gọi ra quan hệ của topic va user ạ. E đang bị lỗi can not read property name.

      1. Điều em băn khoan có phải là api trong laravel khi call nó nó sẻ trả về dữ liệu và dữ liệu quan hệ của nó luôn không? ví dụ như anh gọi router api/cateogories em thấy anh viết trong CategoryController@index chỉ trả về là return Category::all() mà nó lại trả về được danh sách topic và từ topic sang cả user e đang không hiểu chỗ ý mong anh giải thích cho em. Em cảm ơn

  3. [Vue warn]: Unknown custom element: – did you register the component correctly? For recursive components, make sure to provide the “name” option. Mình làm đến lấy danh sách từn category thì nó bị lỗi thế này , ai gặp chưa giúp với ạ

  4. Thực ra chô topic.user.name của chủ bài viết hơi bị sai nhé, API không trả ra dữ liệu quan hệ. Các bạn muốn lấy trường name của user tạo topic thì xem các lấy trường numberOfTopics trong model Category nha.
    Ngoài ra Vue sẽ báo thiếu cateogryName, vì trong phần router-link, phần data ko truyền vào cateogryName nên nó ko lấy được 🙂 …
    Đánh giá là bài viết hay, nhưng còn quá nhiều sai sót. Chủ thớt nên test kỹ trước khi post hihi

Add Comment