點燈坊

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

如何對 API 回傳資料加上 Cache ?

Sam Xiao's Avatar 2020-07-11

若 API 每次傳入的 Query String 相同,所回傳的結果也相同,對於這類 API 可以加上 Cache,當參數相同時,就不再發起 API Request,如此不但節省後端資源,使用者體驗更佳。事實上 ECMAScript 的 Closure + IIFE 就能實作出 Cache,並不需要依賴其他 Library。

Version

macOS Catalina 10.15.4
WebStorm 2020.1.3
Vue 2.6.11
Axios 0.19.2

Browser

cache000

只有第一次顯示 From API,表示直接發 API request 取得資料,之後都是顯示 From cache 從 cache 取得資料。

Data

{
  "id": 1,
  "title": "FP in JavaScript",
  "price": 100,
  "categoryId": 1,
  "image": "fpjs.jpg"
}

http://localhost:3000/books/:id 會回傳指定 id 的書籍。

Axios

<template>
  <div>
    <button @click="onSubmit">Get Book</button>
    <p></p>
    {{ title }} / {{ price }}
  </div>
</template>

<script>
import axios from 'axios'

let fetchBook = x => axios.get(`http://localhost:3000/books/${x}`)
  .then(x => x.data)

let onSubmit = function() {
  fetchBook(1)
    .then(x => {
      this.title = x.title
      this.price = x.price
    })
}

export default {
  name: 'app',
  data: () => ({
    title: '',
    price: ''
  }),
  methods: {
    onSubmit
  }
}
</script>

12 行

let fetchBook = x => axios.get(`http://localhost:3000/books/${x}`)
  .then(x => x.data)

fetchBook() 為 API function,負責將 URL 傳入 Axios.get() 並取得 Promise ,因為資料藏在 data property 內,先從 x.data 取出再包成 Promise 回傳。

14 行

let onSubmit = function() {
  fetchBook(1)
    .then(x => {
      this.title = x.title
      this.price = x.price
    })
}

fetchBook() 要求傳入 id,因為 Axios 回傳 Promise,因此在 .then() 內以 side effect 寫入 titleprice

http://localhost:3000/books/1 永遠回傳相同資料,下一次按下 Get Book 時,又回重新發起 API request 取得相同資料,關於這類資料不易變動的 API,若能適度的 cache,將大幅提升效率。

API Cache

<template>
  <div>
    <button @click="onSubmit">Get Book</button>
    <p></p>
    {{ title }} / {{ price }}
  </div>
</template>

<script>
import axios from 'axios'

let _fetchBook = x => axios.get(`http://localhost:3000/books/${x}`)
  .then(x => x.data)

let fetchBook = (_ => {
  let storage = {}

  return x => {
    if (storage[x]) {
      console.log('From storage')
      return storage[x]
    } else {
      console.log('From API')
      return (storage[x] = _fetchBook(x), storage[x])
    }
  }
})()

let onSubmit = function() {
  fetchBook(1)
    .then(x => {
      this.title = x.title
      this.price = x.price
    })
}

export default {
  name: 'app',
  data: () => ({
    title: '',
    price: ''
  }),
  methods: {
    onSubmit
  }
}
</script>

要實作 cache 其實並不需要額外 library,只要使用 ECMAScript 語言特性:Closure + IIFE 即可達成。

13 行

let _fetchBook = x => axios.get(`http://localhost:3000/books/${x}`)
  .then(x => x.data)

將原本 fetchBook() 改成 _fetchBook(),內容完全不變。

16 行

let fetchBook = (_ => {
  ...

  return x => {}
})()

fetchBook() 改成 IIFE,由於是 IIFE,所以 argument 完全不重要,以 _ 表示即可,並回傳 x => {} function,其 signature 與原 fetchBook() 完全相同。

17 行

let storage = {}

Cache 的關鍵就在此:在 IIFE 內宣告 storage Object。

  • 因為 Object 具有 key / value 特性,因此可當作 cache 使用
  • 由於 Object 是宣告在 return function 外,也就是 Closure,所以無論 function 被呼叫幾次,storage Object 依然存在,因此可當作 cache 使用

19 行

return x => {
  if (storage[x]) {
    console.log('From storage')
    return storage[x]
  } else {
    console.log('From API')
    return (storage[x] = _fetchBook(x), storage[x])
  }
}

若該 key 存在於 storage object,則直接回傳 storage[x],內部存的是 Promise

若不存在,則呼叫 _fetchBook() 打 API,先存到 storage object 後再回傳。

Nullish Coalescing Operator

<template>
  <div>
    <button @click="onSubmit">Get Book</button>
    <p></p>
    {{ title }} / {{ price }}
  </div>
</template>

<script>
import axios from 'axios'

let _fetchBook = x => axios.get(`http://localhost:3000/books/${x}`)
  .then(x => x.data)

let fetchBook = (_ => {
  let storage = {}
  return x => storage[x] ?? (storage[x] = _fetchBook(x), storage[x])
})()

let onSubmit = function() {
  fetchBook(1)
    .then(x => {
      this.title = x.title
      this.price = x.price
    })
}

export default {
  name: 'app',
  data: () => ({
    title: '',
    price: ''
  }),
  methods: {
    onSubmit
  }
}
</script>

15 行

let fetchBook = (_ => {
  let storage = {}
  return x => storage[x] ?? (storage[x] = _fetchBook(x), storage[x])
})()

之前的 console.log() 只是為了釐清真的從 storage Object 而來,實務上可以拿掉。

搭配 ES2020 的 ?? 可作進一步化簡。

cache001

也可從 DevTools 證明無論按幾次都只有一次 API request。

Higher Order Function

<template>
  <div>
    <button @click="onSubmit">Get Book</button>
    <p></p>
    {{ title }} / {{ price }}
  </div>
</template>

<script>
import axios from 'axios'

let _fetchBook = x => axios.get(`http://localhost:3000/books/${x}`)
  .then(x => x.data)

let cache = f => (() => {
  let storage = {}
  return x => storage[x] ?? (storage[x] = f(x), storage[x])
})()

let fetchBook = cache(_fetchBook)

let onSubmit = function() {
  fetchBook(1)
    .then(x => {
      this.title = x.title
      this.price = x.price
    })
}

export default {
  name: 'app',
  data: () => ({
    title: '',
    price: ''
  }),
  methods: {
    onSubmit
  }
}
</script>

20 行

let fetchBook = cache(_fetchBook)

整理出 cache() higher order function,只要將原本 API function 傳入後,就自動產生具有 cache 功能的 API function。

15 行

let cache = f => (() => {
  let storage = {}
  return x => storage[x] ?? (storage[x] = f(x), storage[x])
})()

將 IIFE 再度升級,讓 cache() 能串入普通 API function,然後回傳具有 cache 功能的 API function。

Conclusion

  • Closure 天生就能夠 cache,只要善用 ECMAScript 語言特性就能實作 cache
  • 可將 Promise 直接存入 storage object,可避開自行建立 Promise
  • 對於 Code Reuse 部分,OOP 會將相同部分使用繼承,或 Extract Class 之後再 DI 注入;FP 則會將相同部分抽成 Higher Order Function,相異部分傳入 callback