從 API 回傳雖然是單一 Observable,若顯示時是在不同處時,我們可將單一 Observable 產生多個 Observable 分開顯示。
Version
macOS Catalina 10.15.4
WebStorm 2020.1
Vue 2.6.11
RxJS 6.5.5
Browser
+
按下後除了顯示 FP in JavaScript
外,還同時顯示了其封面圖片。
Data
{
"title": "FP in JavaScript",
"price": 100,
"categoryId": 1,
"image": "fpjs.jpg"
}
http://localhost:3000/books/1
回傳為以上 object。
Split Observable
<template>
<div>
<button v-stream:click="{ subject: click$, data: 1 }">+</button>
<h1>{{ title$ }}</h1>
<img :src="image$">
</div>
</template>
<script>
import { ajax } from 'rxjs/ajax'
import { pluck, exhaustMap, map, share } from 'rxjs/operators'
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response')
)
let subscriptions = function() {
let book$ = this.click$.pipe(
pluck('data'),
exhaustMap(fetchBook$),
share()
)
let title$ = book$.pipe(pluck('title'))
let image$ = book$.pipe(
pluck('image'),
map(x => `/images/${x}`)
)
return { title$, image$ }
}
export default {
name: 'app',
domStreams: ['click$'],
subscriptions,
}
</script>
第 3 行
<button v-stream:click="{ subject: click$, data: 1 }">+</button>
<h1>{{ title$ }}</h1>
<img :src="image$">
除了 title
外,還多顯示了其封面圖片。
13 行
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response')
)
fetchBook$()
回傳為 Observable。
18 行
let book$ = this.click$.pipe(
pluck('data'),
exhaustMap(fetchBook$),
share()
)
由於 title$
與 image$
都來自於 book$
,無法由 click$
直接 exhaustMap()
產生兩個 Observable,只能先 exhaustMap()
成 book$
,再分成 title$
與 image$
,為避免重複 API request,特別加上 share()
共用。
24 行
let title$ = book$.pipe(pluck('title'))
由 book$
產生 title$
。
26 行
let image$ = book$.pipe(
pluck('image'),
map(x => `/images/${x}`)
)
除了 pluck()
出 image
property 外,因為圖片是放在 public/images
目錄下,因此我們還須透過 map()
加上完整路徑。
Point-free
<template>
<div>
<button v-stream:click="{ subject: click$, data: 1 }">+</button>
<h1>{{ title$ }}</h1>
<img :src="image$">
</div>
</template>
<script>
import { ajax } from 'rxjs/ajax'
import { pluck, exhaustMap, map, share } from 'rxjs/operators'
import { concat } from 'ramda'
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response')
)
let subscriptions = function() {
let book$ = this.click$.pipe(
pluck('data'),
exhaustMap(fetchBook$),
share()
)
let title$ = book$.pipe(pluck('title'))
let image$ = book$.pipe(
pluck('image'),
map(concat('/images/'))
)
return { title$, image$ }
}
export default {
name: 'app',
domStreams: ['click$'],
subscriptions,
}
</script>
27 行
let image$ = book$.pipe(
pluck('image'),
map(concat('/images/'))
)
map()
的 callback 可使用 Ramda 的 concat()
使其 point-free。
Function Composition
<template>
<div>
<button v-stream:click="{ subject: click$, data: 1 }">+</button>
<h1>{{ title$ }}</h1>
<img :src="image$">
</div>
</template>
<script>
import { pipe } from 'rxjs'
import { ajax } from 'rxjs/ajax'
import { pluck, exhaustMap, map, share } from 'rxjs/operators'
import { concat } from 'ramda'
let fetchBook$ = x => ajax(`http://localhost:3000/books/${x}`).pipe(
pluck('response')
)
let makeImage = pipe(
pluck('image'),
map(concat('/images/'))
)
let subscriptions = function() {
let book$ = this.click$.pipe(
pluck('data'),
exhaustMap(fetchBook$),
share()
)
let title$ = book$.pipe(pluck('title'))
let image$ = book$.pipe(makeImage)
return { title$, image$ }
}
export default {
name: 'app',
domStreams: ['click$'],
subscriptions,
}
</script>
10 行
import { pipe } from 'rxjs'
從 rxjs
import 進 pipe()
。
亦可使用 Ramda 的
pipe()
17 行
let makeImage = pipe(
pluck('image'),
map(concat('/images/'))
)
book$.pipe()
傳進的是一堆 operator 的組合,且都是 pure function,可將這些 function 以 pipe()
組合成新 funtion。
何時會想另外抽成獨立 funtion 呢 ?
- 若
pipe()
接的 function 太多,抽成獨立 function 較易閱讀與維護時 - 若有不同 Observable 也使用相同 operator 操作,可抽成獨立 function 共用
32 行
let image$ = book$.pipe(makeImage)
如此 book$.pipe()
就可直接傳入剛組合的 makgeImage()
。
Conclusion
- 實務上顯示可能為多個 Observable,但都來自於相同 API,此時可將單一 Observable 分別
pluck()
出新 Observable,然後各自map()
處理 - RxJS 早期版本是以 Method Chaining 呈現,之後改成 Function Pipeline 形式,優點是可將這些 operator 單獨組成新 function,可使用 RxJS 的
pipe()
,亦可使用 Ramda 的pipe()
Reference
John Lindquist, Split a Request into Data Stream and Image Stream with RxJS and Vue.js
RxJS, pipe()
RxJS, exhaustMap()
RxJS, share()
RxJS, map()
Ramda, concat()