使用 Observable 可大幅降低對 this 的依賴

前端的難點之一是讀取 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

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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 行

1
2
3
4
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 行

1
2
3
<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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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 行

1
2
3
subscriptions: () => ({
books$
}),

subscriptions 內宣告 books$ Observable。

15 行

1
2
3
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

api001

由於沒有使用到 this,因此 Observable 可大膽使用 Arrow Function,不必如 Promise 為了遷就 this 而使用 Function Expression。

第 4 行

1
2
3
<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。

api000

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
RxJS, pipe()
RxJS, pluck()
RxJS Marbles, pluck()