點燈坊

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

如何從 Observable 中的 Array 取出部分 Property ?

Sam Xiao's Avatar 2020-06-03

雖然 API 會回傳 Observable,但其實骨子裡都還是 ECMAScript 的 Array 與 Object,實務上我們常只需要 Object 中部分 Property 即可,這在 RxJS 該如何做呢 ?

Version

macOS Catalina 10.15.4
WebStorm 2020.1.1
Vue 2.6.11
Vue-rx 6.2
RxJS 6.5.5
Ramda 0.27.0

RxJS

<template>
  <div>
  </div>
</template>

<script>
import { ajax } from 'rxjs/ajax'
import { pluck } from 'rxjs/operators'

let subscriptions = _ => {
  ajax('http://localhost:3000/books').pipe(
    pluck('response')
  ).subscribe(console.log)
}

export default {
  name:'App',
  subscriptions
}
</script>

11 行

ajax('http://localhost:3000/books').pipe(
  pluck('response')
).subscribe(console.log)

讀取 'http://localhost:3000/books' API,發現其有 idtitlepricecategoryIdimage 眾多 property。

fmap000

但我們只希望保留 titleprice 即可。

Array.prototype.map()

<template>
  <div>
    <ul v-for="(x, i) in books$" :key="i">
      <li>{{ x.title }} / {{ x.price }}</li>
    </ul>
  </div>
</template>

<script>
import { ajax } from 'rxjs/ajax'
import { map, pluck } from 'rxjs/operators'

let fetchBooks$ = ajax('http://localhost:3000/books').pipe(
  pluck('response'),
)

let subscriptions = _ => {
  let books$ = fetchBooks$.pipe(
    map(x => x.map(y => ({ title: y.title, price: y.price })))
  )

  return { books$ }
}

export default {
  name:'App',
  subscriptions
}
</script>

13 行

let fetchBooks$ = ajax('http://localhost:3000/books').pipe(
  pluck('response'),
)

抽出 fetchBooks$ 專門從 http://localhost:3000/books 取出資料,回傳為 Observable。

18 行

let books$ = fetchBooks$.pipe(
  map(x => x.map(y => ({ title: y.title, price: y.price })))
)

Observable 有 map(),但這種 map() 與我們習慣的 Array map() 又不太一樣,比較類似 Maybe 的 map(),專門用來改變 Observable 內部值。

Observable map() 內的 x 事實上為 Array 而非 object,若我們只想取得 object 的部分 property,這無法從 RxJS 中的 operator 得到答案。

但我們可用 Array 自帶的 map() 達成需求。

fmap001

Ramda

<template>
  <div>
    <ul v-for="(x, i) in books$" :key="i">
      <li>{{ x.title }} / {{ x.price }}</li>
    </ul>
  </div>
</template>

<script>
import { ajax } from 'rxjs/ajax'
import { map, pluck } from 'rxjs/operators'
import { map as fmap, pick } from 'ramda'

let fetchBooks$ = ajax('http://localhost:3000/books').pipe(
  pluck('response'),
)

let subscriptions = _ => {
  let books$ = fetchBooks$.pipe(
    map(fmap(pick(['title', 'price'])))
  )

  return { books$ }
}

export default {
  name:'App',
  subscriptions
}
</script>

map()x 既然是 array,且是 synchronous,這就是 Ramda 的強項了。

12 行

import { map as fmap, pick } from 'ramda'

因為在 Ramda 也稱為 map(),與 RxJS 同名,只好 alias 成為 fmap()

fmap() 命名是取自於 Functor 的 fmap()

19 行

let books$ = fetchBooks$.pipe(
  map(fmap(pick(['title', 'price'])))
)

透過 fmap()pick() 組合就能使 map() 的 callback 也 point-free 了。

Function Composition

<template>
  <div>
    <ul v-for="(x, i) in books$" :key="i">
      <li>{{ x.title }} / {{ x.price }}</li>
    </ul>
  </div>
</template>

<script>
import { ajax } from 'rxjs/ajax'
import { map, pluck } from 'rxjs/operators'
import { map as fmap, pick, compose } from 'ramda'

let onlyProps = compose(fmap, pick)

let fetchBooks$ = ajax('http://localhost:3000/books').pipe(
  pluck('response'),
)

let subscriptions = _ => {
  let books$ = fetchBooks$.pipe(
    map(onlyProps(['title', 'price']))
  )

  return { books$ }
}

export default {
  name:'App',
  subscriptions
}
</script>

14 行

let onlyProps = compose(fmap, pick)

可將 fmap()pick() 先組合起來。

21 行

let books$ = fetchBooks$.pipe(
  map(onlyProps(['title', 'price']))
)

map() 內可直接使用 onlyProps(),可讀性超高。

也可將 onlyProps() 放在自己的 helper 以供未來使用

Conclusion

  • RxJS 與 Ramda 其實各擅勝場,RxJS 強在 asynchronous,而 Ramda 強在 synchronous,尤其 Ramda 提供非常多 Array 與 Object 的 function,很適合在 RxJS 的 callback 中使用

Reference

Ramda, map()
Ramda, pick()