既然 API 回傳結果與 DOM event 都是 Observable,可將兩個 Observable 合併成單一 Observable 顯示。
Version
macOS Catalina 10.15.4
WebStorm 2020.1
Vue 2.6.11
RxJS 6.5.5
Browser
+
按下後除了顯示 FP in JavaScript
外,還同時顯示了其封面圖片。
若圖片載入錯誤,則顯示 Speaking 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$
,並加上 share()
避免 multiple API request。
26 行
let image$ = book$.pipe(
pluck('image'),
map(x => `/images/${x}`)
)
除了 pluck()
出 image
property 外,因為圖片是放在 public/images
目錄下,因此我們還須透過 map()
加上完整路徑。
Image Loading Error
<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 => `/image/${x}`)
)
return { title$, image$ }
}
export default {
name: 'app',
domStreams: ['click$'],
subscriptions,
}
</script>
23 行
let image$ = book$.pipe(
pluck('image'),
map(x => `/image/${x}`)
)
若 map()
的 callback 寫錯,從 images
打成 image
。
則 <img>
將載入圖片失敗。
這種載入圖片失敗,能使用 RxJS 解決嗎 ?
merge()
<template>
<div>
<button v-stream:click="{ subject: click$, data: 1 }">+</button>
<h1>{{ title$ }}</h1>
<img v-stream:error="imageError$" :src="image$">
</div>
</template>
<script>
import { ajax } from 'rxjs/ajax'
import { merge } from 'rxjs'
import { map, mapTo, pluck, exhaustMap, 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 successTitle$ = book$.pipe(pluck('title'))
let successImage$ = book$.pipe(
pluck('image'),
map(x => `/image/${x}`)
)
let failedTitle$ = this.imageError$.pipe(
mapTo('Speaking JavaScript')
)
let failedImage$ = this.imageError$.pipe(
mapTo('/images/spjs.jpg')
)
let title$ = merge(successTitle$, failedTitle$)
let image$ = merge(successImage$, failedImage$)
return { title$, image$ }
}
export default {
name: 'app',
domStreams: [
'click$',
'imageError$'
],
subscriptions,
}
</script>
第 5 行
<img v-stream:error="imageError$" :src="image$">
當圖片載入失敗時,會觸發 <img>
的 error
event,可將 error
event 轉成 imageError$
Subject。
47 行
domStreams: [
'click$',
'imageError$'
],
宣告 imageError$
於 domStreams
。
25 行
let successTitle$ = book$.pipe(pluck('title'))
let successImage$ = book$.pipe(
pluck('image'),
map(x => `/image/${x}`)
)
當圖片順利載入成功時則產生 successTitle$
與 successImage$
Observable,負責顯示 FP in JavaScript
與其封面圖片。
32 行
let failedTitle$ = this.imageError$.pipe(
mapTo('Speaking JavaScript')
)
let failedImage$ = this.imageError$.pipe(
mapTo('/images/spjs.jpg')
)
當圖片載入失敗時則使用 imageError$
產生 failedTitle$
與 failedImage$
Observable,負責顯示 Speaking JavaScript
與其封面圖片。
由於產生固定值,因此使用 mapTo()
即可。
mapTo()
功能等同於map()
,差別在map()
會隨著 Observable 內部值而變,而mapTo()
為常數
40 行
let title$ = merge(successTitle$, failedTitle$)
let image$ = merge(successImage$, failedImage$)
本文關鍵在此,將 successTitle$
與 failedTitle$
兩個 Observable 合併然後顯示即可。
merge()
原意為將兩個 Observable 加以合併。
merge()
將任意個數 Observable 合併成單一 Observable
但這裡很有趣的是 successTitle$
與 failedTitle$
兩個 Observable 其實不會同時存在,因此儘管是 merge()
,但其實最後只是 successTitle$
或 failedTitle$
兩者之一而已。
Point-free
<template>
<div>
<button v-stream:click="{ subject: click$, data: 1 }">+</button>
<h1>{{ title$ }}</h1>
<img v-stream:error="imageError$" :src="image$">
</div>
</template>
<script>
import { ajax } from 'rxjs/ajax'
import { merge } from 'rxjs'
import { map, mapTo, pluck, exhaustMap, 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 successTitle$ = book$.pipe(pluck('title'))
let successImage$ = book$.pipe(
pluck('image'),
map(concat('/image/'))
)
let failedTitle$ = this.imageError$.pipe(
mapTo('Speaking JavaScript')
)
let failedImage$ = this.imageError$.pipe(
mapTo('/images/spjs.jpg')
)
let title$ = merge(successTitle$, failedTitle$)
let image$ = merge(successImage$, failedImage$)
return { title$, image$ }
}
export default {
name: 'app',
domStreams: [
'click$',
'imageError$'
],
subscriptions,
}
</script>
25 行
let successImage$ = book$.pipe(
pluck('image'),
map(concat('/image/'))
)
map()
的 callback 可使用 Ramda 的 concat()
使其 point-free。
Conclusion
- 因為成功與失敗的 Observable 其實不會同時出現,可使用
merge()
巧妙的將兩個 Observable 合一顯示
Reference
John Lindquist, Handle Image Loading Errors in Vue.js with RxJS and domStreams
RxJS, exhaustMap()
RxJS, share()
RxJS, map()
RxJS, mapTo()
Ramda, concat()
Ramda, always()