若 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
只有第一次顯示 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 寫入 title
與 price
。
若 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 的 ??
可作進一步化簡。
也可從 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