VueJS là một JS framework rất được các anh em developer yêu mến, và ngày càng phổ biến. Với VueJS, chúng ta có đầy đủ công cụ xây dựng một ứng dụng web, đặc biệt là dạng ứng dụng SPA – Single page Application.
Với các ứng dụng SPA thì việc phải tương tác với server thông qua các APIs là điều thường xuyên, từ dữ liệu thời gian thực tới tài nguyên tĩnh như ảnh, video… Trong hầu hết dự án dạng SPA nói riêng, HTTP client là phần mà chúng ta sử dụng các thư viện như axios, apollo… để kết nối tới các Rest API của server.
Khi việc kết nối này trở nên thường xuyên và quan trọng thì bạn càng cần phải tổ chức mã nguồn phần HTTP client thật tốt. Nếu không, dự án của bạn sẽ rất khó bảo trì, cũng khó mở rộng về sau này.
Bài viết này, chúng ta sẽ cùng nhau tìm hiểu một best practices tổ chức phần HTTP Client nhé.
Vì sở thích cá nhân, mình vẫn ưu ái sử dụng thư viện Axios vì tính dễ dùng, cộng đồng lớn… và đơn giản là mình thích, thế thôi J
Nội dung chính bài viết:
- Tạo HTTP Clients sử dụng axios instances
- Tổ chức các API endpoints
- Tạo các network requests trong Vuex
- Xử lý các thông tin xác thực bằng cách sử dụng interceptors
- Xử lý lỗi và ghi log
- Caching
Mình bắt đầu thôi nhỉ!
Nội dung chính của bài viết
Tạo HTTP Clients sử dụng axios instances
Axios hỗ trợ tạo request tới API sử dụng axios instances. Đọc tên là chúng ta liên tưởng ngay tới việc Axios nó có khả năng giúp có thể kết nối tới nhiều server API trong cùng một dự án. Ví dụ, ứng dụng của bạn có thể cần kết nối tới nhiều server: abc.com/api
để chuyên xác thực, còn xyz.com/api
để chuyên lấy dữ liệu…
Thôi không dài dòng nữa, bạn cần phải cài đặt axios đã:
$ npm install --save axios
Bước tiếp theo là import thư viện axios vừa cài đặt vào dự án. Một kinh nghiệm của mình đó là BASE_URL
nên để trong file enviroment .env
, lý do cơ bản nhất là để chúng ta có thể tách được URL giữa 2 môi trường dev và product.
Trong ứng dụng VueJS, để có thể truy cập được các biến env, chúng ta cần đặt tiền tố cho nó là VUE_APP_
Vì vậy, nếu bạn muốn lưu BASE_URL
, hãy tạo tệp .env trong thư mục gốc của dự án và thêm dòng sau:
VUE_APP_BASE_URL=https://myApiServerUrl.com
OK, khi đã tạo xong biến môi trường, bạn hoàn toàn có thể truy cập được biến này trong bất kỳ đâu trong dự án,
Giờ chúng ta sẽ tạo một axios instances, bạn tạo một tệp httpClient.js trong thư mục api và có nội dung như sau:
import axios from axios; const httpClient = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, headers: { "Content-Type": "application/json", // anything you want to add to the headers } }); export default httpClient;
Có một điểm cần lưu ý, mặc định axios để giá trị timeout = 0
, nghĩa là không có timeout gì hết, nếu kết nối có vấn đề thì nó cứ treo kết nối mãi mãi. Đây không phải là một trải nghiệm tốt. Do đó, mình nên đặt một giá trị timeout cho nó hợp lý. Chúng ta cập nhật đoạn mã trên để thêm giá trị timeout như sau:
const httpClient = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 1000, // indicates, 1000ms ie. 1 second headers: { "Content-Type": "application/json", } });
Vậy là xong bước đầu, mời bạn đọc tiếp.
Cấu trúc tổ chức API endpoints
Theo nguyên tắc thiết kết REST API phổ biến, hầu hết chúng ta đều để các thao tác CRUD cùng một endpoint. Ví dụ nhé:
GET “/users”
: Lấy danh sách toàn bộ người dùngGET “/users/:id”
: Lấy thông tin chi tiết một người dùng theo IDPOST “/users”
: Thêm mới một người dùngPUT “/users”
: Cập nhật thông tin một người dùngDELETE “/users/:id”
: Xóa một người dùng
Vì vậy, các tổ chức mã nguồn tốt nhất là nên gom chúng vào một tệp duy nhất. Như dưới đây:
import httpClient from './httpClient'; const END_POINT = '/users'; const getAllUsers = () => httpClient.get(END_POINT); // you can pass arguments to use as request parameters/data const getUser = (user_id) => httpClient.get(END_POINT, { user_id }); // maybe more than one.. const createUser = (username, password) => httpClient.post(END_POINT, { username, password }); export { getAllUsers, getUser, createUser }
Chúng ta sẽ có một cấu trúc thư mục như sau:
api/ ├── httpClient.js --> HTTP Client với cấu hình kết nối đã tạo ở trên. ├── users.api.js ├── posts.api.js └── comments.api.js
Và khi muốn sử dụng để gọi API, đơn giản là bạn import thôi, như này nhé:
import { getAllUsers, getUser } from '@/api/users.api';
Tạo request bên trong Vuex
Chúng ta hoàn toàn có thể gọi các request tới API ngay trong các component giao diện. Tuy nhiên, đây không phải là cách viết code tốt, vì nó sẽ trộn lẫn giữa code nghiệp vụ với code giao diện, đây là điều nên tránh nếu bạn muốn dễ unit test hơn. Cái này không hẳn là ý kiến chủ quan của mình đâu nhé, đến tác giả của Vuex họ cũng khuyên bạn làm như vậy, hãy nhìn hình dưới nhé.
Bạn thấy không, họ cũng khuyên là gọi các backend API trong actions của Vuex thay vì gọi trong component.
Tóm lại, chúng ta sẽ chuyển tất cả các thao tác xử lý nghiệp vụ vào Vuex, bao gồm cả những request network.
Mình sẽ không giải thích chi tiết cơ chế hoạt động của Vuex trong bài viết này, nếu bạn muốn, mời bạn đọc lại bài viết này nhé 😋: Sử dụng thư viện Vuex để quản lý State trong VueJS
Dưới đây là một ví dụ một module trong Vuex mà mình dùng để lấy dữ liệu danh sách người dùng, rồi commit vào store.
/* * store/modules/users.module.js */ // import the api endpoints import { getAllUsers } from "@/api/users.api" const state = { users: [] } const getters = { getUsers(state) { return state.users; } } const actions = { async fetchUsers({ commit }) { try { const response = await getAllUsers(); commit('SET_USERS', response.data); } catch (error) { // handle the error here } }); } } const mutations = { SET_USERS(state, data) { state.users = data; } } export default { namespaced: true, state, getters, actions, mutations }
Trong Vue component, giờ đây để gọi API, chúng ta sẽ gọi thông qua action của Vuex.
<template> <!-- Your template here --> </template> <script> import { mapActions, mapGetters } from "vuex"; export default { data() { return { isLoading: false; } }, computed: { ...mapGetters('Users', ['getUsers']) }, methods: { ...mapActions('Users', ['fetchUsers']) }, async mounted(): { // Make network request if the data is empty if ( this.getUsers.length === 0 ) { // set loading screen this.isLoading = true; await this.fetchUsers(); this.isLoading = false; } } } </script>
Xử lý chứng chỉ xác thực bằng cách sử dụng interceptors
Với các APIs mà cần phải đính kèm mã token (ví dụ mã JWT token sau khi đăng nhập) vào tất cả các request khi gửi lên server. Bạn sẽ làm thế nào? Mỗi lần gọi là một lần đính mã token?
Không ai làm thế cả, mình sẽ chỉ cho bạn một cách. Đó là sử dụng interceptors để đính mã token vào tất cả các request.
Có thể hiểu đơn giản, interceptors là một middleware nằm giữa, được gọi trước mỗi request hoặc ngay khi nhận được response mà server trả về trước khi thực sự return kết quả cho bạn.
OK, giờ quay trở lại tệp httpClient.js và thêm đoạn mã sau:
import axios from axios; const httpClient = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 5000 }); const getAuthToken = () => localStorage.getItem('token'); const authInterceptor = (config) => { config.headers['Authorization'] = getAuthToken(); return config; } httpClient.interceptors.request.use(authInterceptor); export default httpClient;
Xử lý Error trong HTTP Client
Xử lý error là một phần vô cùng quan trọng, nhưng lại rất hay bị bỏ qua bởi các bạn developer “bận rộn”.
Đã là các request network thì không ai biết trước được là nó có chắc thành công hay không? Mà nếu có lỗi xảy ra thì ít nhất ứng dụng cũng phải thông báo cho người dùng biết, chứ chỉ thông báo chung chung như “Đã có lỗi”, thậm chí để ứng dụng bị crash thì thật quá chán đúng không?
Như mình đã đề cập ở trên, interceptors của Axios cung cấp một callback luôn được gọi ngay khi có response từ server trước khi thực sự return kết quả cho bạn. Cụ thể là interceptors.response.use(...)
. Bạn có thể tham khảo cách xử lý lỗi của mình như dưới đây:
// interceptor to catch errors const errorInterceptor = error => { // check if it's a server error if (!error.response) { notify.warn('Network/Server error'); return Promise.reject(error); } // all the other error responses switch(error.response.status) { case 400: console.error(error.response.status, error.message); notify.warn('Nothing to display','Data Not Found'); break; case 401: // authentication error, logout the user notify.warn( 'Please login again', 'Session Expired'); localStorage.removeItem('token'); router.push('/auth'); break; default: console.error(error.response.status, error.message); notify.error('Server Error'); } return Promise.reject(error); } // Interceptor for responses const responseInterceptor = response => { switch(response.status) { case 200: // yay! break; // any other cases default: // default case } return response; } httpClient.interceptors.response.use(responseInterceptor, errorInterceptor);
Cache
Cache là giải pháp đầu tiên và cũng là hiệu quả nhất mỗi khi bạn nghĩ tới việc tối ưu hiệu năng, tăng tốc độ ứng dụng.
Để sử dụng cache cho các request, chúng ta sẽ sử dụng thêm một extension của axios
Cài đặt:
$ npm install --save axios-extensions
Cách thêm cache cho request:
import axios from 'axios'; import { cacheAdapterEnhancer } from 'axios-extensions'; const cacheConfig = { enabledByDefault: false, cacheFlag: 'useCache' } const httpClient = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, headers: { 'Cache-Control': 'no-cache' }, adapter: cacheAdapterEnhancer(axios.defaults.adapter, cacheConfig); })
Giờ đây, với request nào mà bạn cần cache thì chỉ cần thêm option cache vào là được. Ví dụ:
const getUsers = () => httpClient.get('/users', { useCahe: true });
Tạm kết
Mình xin tạm kết thúc bài viết về http client tại đây, dù hơi dài nhưng mình nghĩ nó sẽ rất có ích cho dự án của bạn. Đây chỉ là một trong những cách tổ chức mã nguồn sau cho clean hơn mà thôi. Nếu bạn có cách nào hay hơn thì đừng ngại chia sẻ nhé.
🔥 Hẹn gặp lại các bạn ở bài viết tiếp theo, chờ nhé.
Bình luận. Cùng nhau thảo luận nhé!