如何使用 Rx Observable 存取 API ?
前端的難點之一是讀取 API 時,回傳為 Promise,這是 非同步
行為;若使用 RxJS,可視為 Observable,以 stream 方式處理 API 回傳資料。
Version
Vue 2.5.21
Vue-rx 6.1.0
RxJS 6.3.3
Promise
API Function
api/books.js
import axios from 'axios'; const API = process.env.VUE_APP_API; const URI = process.env.VUE_APP_BOOKS; export const fetchBooks = () => axios.get(`${API}${URI}`);
傳統 Vue 會使用 API function,搭配 Axios 回傳 Promise。
Component
components/HelloWorld.vue
<template> <div> <ul> <li v-for="(item, index) in books" :key="index"> Title : {{ item.title }}, Price : {{ item.price }} </li> </ul> </div> </template> <script> import { fetchBooks } from '@/api/books'; const mounted = function() { fetchBooks() .then(res => this.books = res.data.books); }; export default { name: 'HelloWorld', data: () => ({ books: [], }), mounted, } </script>
14 行
const mounted = function() { fetchBooks() .then(res => this.books = res.data.books); };
在 mounted
hook 呼叫 fetchBooks()
,由於回傳的是 Promise,必須使用 then()
與 callback function 將資料寫進 books
data,才能顯示在 HTML template。
由於要寫進 data
,必須使用 this
,因此 mounted()
只能使用 Function Expression,而不能使用 Arrow Function。
第 4 行
<li v-for="(item, index) in books" :key="index"> Title : {{ item.title }}, Price : {{ item.price }} </li>
v-for
directive 支援 books
data,可順理顯示在 HTML template。
Rx Observable
API Function
api/books.js
import axios from 'axios'; import { from } from 'rxjs'; const API = process.env.VUE_APP_API; const URI = process.env.VUE_APP_BOOKS; export const fetchBooks = () => from(axios.get(`${API}${URI}`));
Axios 原本是回傳 Promise,為了要使 API function 回傳 Rx Observable,必須使用 RxJS 的 from()
將 Promise 轉成 Observable。
Component
components/HelloWorld.vue
<template> <div> <ul> <li v-for="(item, index) in books$" :key="index"> Title : {{ item.title }}, Price : {{ item.price }} </li> </ul> </div> </template> <script> import { fetchBooks } from '@/api/books'; import { pluck } from 'rxjs/operators'; const books$ = fetchBooks().pipe( pluck('data', 'books'), ); export default { name: 'HelloWorld', subscriptions: () => ({ books$ }), } </script>
21 行
subscriptions: () => ({ books$ }),
在 subscriptions
內宣告 books$
Observable。
15 行
const books$ = fetchBooks().pipe( pluck('data', 'books'), );
定義 books$
Observable,由於 fetchBooks()
回傳的就是 Observable
,因此可以使用 pipe()
整合其他 operator。
pipe()
pipe(...fns: Array<UnaryFunction<any, any>>): UnaryFunction<any, any>
將多個 unary function 組合成新的 function
由於資料都放在 Promise 的 Response.data
下,而我們真正想要的資料又放在 books
下,因此使用 pluck()
從 data.books
擷取出想顯示的資料。
pluck()
pluck<T, R>(...properties: string[]) : OperatorFunction<T, R>
從 object 中擷取指定巢狀 property,並回傳新的 function
由於沒有使用到 this
,因此 Observable 可大膽使用 Arrow Function,不必如 Promise 為了遷就 this
而使用 Function Expression。
第 4 行
<li v-for="(item, index) in books$" :key="index"> Title : {{ item.title }}, Price : {{ item.price }} </li>
v-for
部分不變,唯從 books
data 改成 books$
Observable。
v-for
directive 也能順利支援 Rx Observable。
Conclusion
- 使用 Promise,必須搭配
data
才能顯示在 HTML template,因此勢必使用this
- 使用 Rx Observable,HTML template 直接支援 Observable,不須搭配
data
,也因此不用使用this
,可以讓 Vue 更接近 FP 風格 -
v-for
directive 完美支援 Observable,因此 HTML template 部分寫法不變
Sample Code
完整範例可以在我的 GitHub 上找到
Reference
Egghead.io , Stream an API using RxJS into a Vue.js Template