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ượngGET /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ớiPUT /posts/1— Cập nhật toàn bộ nội dung bài viếtDELETE /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
}
{}