點燈坊

失くすものさえない今が強くなるチャンスよ

將單一 Observable 分成多個 Observable

Sam Xiao's Avatar 2020-06-10

從 API 回傳雖然是單一 Observable,若顯示時是在不同處時,我們可將單一 Observable 產生多個 Observable 分開顯示。

Version

macOS Catalina 10.15.4
WebStorm 2020.1
Vue 2.6.11
RxJS 6.5.5

Browser

split000

+ 按下後除了顯示 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()