Tích hợp RxJS với Vue.js để Tương tác với API REST

Giới thiệu dịch vụ mẫu JSONPlaceholder

JSONPlaceholder (https://jsonplaceholder.typicode.com/) là một nền tảng API REST miễn phí, dùng để thử nghiệm và phát triển frontend. Trong bài viết này, chúng ta sẽ xây dựng một ứng dụng Vue.js (với TypeScript) sử dụng RxJS 6 để thực hiện các thao tác CRUD cơ bản trên tài nguyên /posts.

Các endpoint được sử dụng

  • GET /posts/1 — Lấy bài viết duy nhất dưới dạng chuỗi hoặc đối tượng
  • GET /posts — Lấy danh sách bài viết (giới hạn số lượng)
  • POST /posts — Tạo bài viết mới
  • PUT /posts/1 — Cập nhật toàn bộ nội dung bài viết
  • DELETE /posts/1 — Xóa bài viết

Cấu trúc dữ liệu trả về từ tất cả các endpoint GET tuân theo lược đồ sau:

{
  "userId": 1,
  "id": 1,
  "title": "string",
  "body": "string"
}

Khởi tạo dự án Vue

Sử dụng Vue CLI để tạo ứng dụng mới với hỗ trợ TypeScript:

npm install -g @vue/cli
vue create vue-rx-demo
# Chọn: Manually select features → Babel + TypeScript
cd vue-rx-demo
npm run serve

Sau khi chạy, ứng dụng sẽ sẵn sàng tại http://localhost:8080.

Mô hình dữ liệu Post

Tạo file src/models/Post.ts:

export class Post {
  userId: number = 0;
  id: number = 0;
  title: string = '';
  body: string = '';

  toString(): string {
    const escapedBody = this.body.replace(/\n/g, '\\n');
    return `Post {userId=${this.userId}, id=${this.id}, title="${this.title}", body="${escapedBody}"}`;
  }
}

Cài đặt DI và phản xạ metadata

Để hỗ trợ tiêm phụ thuộc trong Vue với TypeScript, cài đặt hai gói:

npm install vue-typescript-inject reflect-metadata

Cập nhật tsconfig.json để bật decorator:

"compilerOptions": {
  "emitDecoratorMetadata": true,
  "experimentalDecorators": true,
  // ... các tùy chọn khác
}

Chỉnh sửa src/main.ts:

import 'reflect-metadata';
import Vue from 'vue';
import App from './App.vue';
import VueTypeScriptInject from 'vue-typescript-inject';

Vue.config.productionTip = false;
Vue.use(VueTypeScriptInject);

new Vue({
  render: h => h(App),
}).$mount('#app');

Xây dựng lớp HTTP reactive dựa trên Axios + RxJS

Vì thư viện rxios gốc chưa tương thích với RxJS 6, ta tự triển khai một phiên bản đơn giản hơn trong src/utils/HttpService.ts:

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { Observable, from } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

export class HttpService {
  private client = axios.create({ baseURL: 'https://jsonplaceholder.typicode.com/' });

  get<T>(url: string, config?: AxiosRequestConfig): Observable<T> {
    return from(this.client.get<T>(url, config)).pipe(
      map((res: AxiosResponse<T>) => res.data),
      catchError(err => {
        console.error('HTTP GET error:', err);
        throw err;
      })
    );
  }

  post<T>(url: string, data: any, config?: AxiosRequestConfig): Observable<T> {
    return from(this.client.post<T>(url, data, config)).pipe(
      map((res: AxiosResponse<T>) => res.data),
      catchError(err => {
        console.error('HTTP POST error:', err);
        throw err;
      })
    );
  }

  put<T>(url: string, data: any, config?: AxiosRequestConfig): Observable<T> {
    return from(this.client.put<T>(url, data, config)).pipe(
      map((res: AxiosResponse<T>) => res.data),
      catchError(err => {
        console.error('HTTP PUT error:', err);
        throw err;
      })
    );
  }

  delete<T>(url: string, config?: AxiosRequestConfig): Observable<T> {
    return from(this.client.delete<T>(url, config)).pipe(
      map((res: AxiosResponse<T>) => res.data),
      catchError(err => {
        console.error('HTTP DELETE error:', err);
        throw err;
      })
    );
  }
}

Dịch vụ quản lý bài viết

Tạo src/services/PostService.ts với logic xử lý bất đồng bộ:

import { Injectable } from 'vue-typescript-inject';
import { Observable, of } from 'rxjs';
import { map, tap, take, mergeMap } from 'rxjs/operators';
import { Post } from '../models/Post';
import { HttpService } from '../utils/HttpService';

@Injectable()
export class PostService {
  private http = new HttpService();

  constructor() {
    this.fetchSinglePostAsString().subscribe();
    this.fetchSinglePostAsObject().subscribe();
    this.fetchTopPosts(2).subscribe();
    this.submitNewPost().subscribe();
    this.modifyExistingPost().subscribe();
    this.removePost().subscribe();
  }

  fetchSinglePostAsString(): Observable<string> {
    return this.http.get<string>('/posts/1', { responseType: 'text' as 'json' })
      .pipe(tap(data => console.log('Raw response:', data)));
  }

  fetchSinglePostAsObject(): Observable<Post> {
    return this.http.get<Post>('/posts/1')
      .pipe(
        map(res => Object.assign(new Post(), res)),
        tap(post => console.log('Mapped Post:', post.toString()))
      );
  }

  fetchTopPosts(count: number): Observable<Post> {
    return this.http.get('/posts')
      .pipe(
        mergeMap(posts => posts.slice(0, count)),
        map(raw => Object.assign(new Post(), raw)),
        tap(post => console.log('Fetched Post:', post.toString())),
        take(count)
      );
  }

  submitNewPost(): Observable<any> {
    const payload = { userId: 101, title: 'Tiêu đề mẫu', body: 'Nội dung mẫu' };
    return this.http.post('/posts', payload)
      .pipe(
        map(res => JSON.stringify(res, null, 2)),
        tap(json => console.log('Created:', json))
      );
  }

  modifyExistingPost(): Observable<any> {
    const payload = { userId: 101, title: 'Tiêu đề cập nhật', body: 'Nội dung cập nhật' };
    return this.http.put('/posts/1', payload)
      .pipe(
        map(res => JSON.stringify(res, null, 2)),
        tap(json => console.log('Updated:', json))
      );
  }

  removePost(): Observable<any> {
    return this.http.delete('/posts/1')
      .pipe(
        map(() => '{}'),
        tap(json => console.log('Deleted:', json))
      );
  }
}

Cấu hình thành phần chính

Xóa src/components/HelloWorld.vue, sau đó cập nhật src/App.vue:

<template>
  <div id="app"></div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { PostService } from './services/PostService';
import { inject } from 'vue-typescript-inject';

@Component({
  providers: [PostService]
})
export default class App extends Vue {
  @inject() private postService!: PostService;
}
</script>

Kết quả thực thi mẫu

Khi chạy ứng dụng, bảng điều khiển sẽ in ra các kết quả tương tự như sau:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"
}
Post {userId=1, id=1, title="sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body="quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"}
Post {userId=1, id=2, title="qui est esse", body="est rerum tempore vitae\\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\\nqui aperiam non debitis possimus qui neque nisi nulla"}
Post {userId=1, id=1, title="sunt aut facere repellat provident occaecati excepturi optio reprehenderit", body="quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"}
{
  "userId": 101,
  "title": "Tiêu đề mẫu",
  "body": "Nội dung mẫu",
  "id": 101
}
{
  "userId": 101,
  "title": "Tiêu đề cập nhật",
  "body": "Nội dung cập nhật",
  "id": 1
}
{}

Thẻ: RxJS Vue.js typescript REST http

Đăng vào ngày 24 tháng 5 lúc 10:33