我面試最喜歡問的開放題:如何嚴謹二次封裝 localStorage?
在很多公司中,內(nèi)部都會封裝一些適用于公司內(nèi)部業(yè)務(wù)的方法庫來提高整個團隊的開發(fā)效率,比如:
- 防抖節(jié)流
- 懶加載、虛擬滾動
- dom增刪改查、移動、拖拽
- 管理狀態(tài)
而在 Vue3 項目中,這種方法庫表現(xiàn)為:hooks庫,市面上有很多優(yōu)秀的庫,比如:vueuse。
最近我在面試中,喜歡問一道有關(guān)于 hooks 的開放問題:二次封裝一個 loaclStorage 的 hooks 時,需要考慮哪些問題呢?
其實這是一道很簡單的題,只不過想考考面試者在做業(yè)務(wù)的時候,會不會考慮更多的邊界情況~接下來說說我對這個問題的小小的理解(可能也不是很全面)。
注意命名,防止污染
比如我現(xiàn)在一個域名下有兩個子項目:
- A項目
- B項目
且這兩個項目都需要存儲 userInfo,那要怎么防止這兩組數(shù)據(jù)互相污染呢?所以需要注意命名,在存儲的時候加上對應(yīng)的項目名前綴,或者其他標識符,保證這組數(shù)據(jù)是唯一的
const PROJECT_NAME = 'test-project'
localStorage.setItem(
`${PROJECT_NAME}_userInfo`,
JSON.stringify({ name: 'lsx' })
)
注意版本,迭代防范
請看一個例子,假如我們存儲一段信息,類型是 string
// 存數(shù)據(jù)
const set = () => {
const info = get()
if (!info) {
localStorage.setItem(
`${PROJECT_NAME}_info`,
'info_string'
)
}
}
// 取數(shù)據(jù)
const get = () => {
const info = localStorage.getItem(
`${PROJECT_NAME}_info`
)
return info
}
然后項目上線了一段時間,但是這個時候,突然決定要換成 object 類型了,這時候?qū)?yīng)的存取方法也變了
// 存數(shù)據(jù)
const set = () => {
const info = get()
if (!info) {
localStorage.setItem(
`${PROJECT_NAME}_info`,
JSON.stringify({ name: 'lsx' })
)
}
}
// 取數(shù)據(jù)
const get = () => {
const info = localStorage.getItem(
`${PROJECT_NAME}_info`
)
return JSON.parse(info)
}
但是這樣其實是有隱患的,因為項目已經(jīng)上線了一段時間,有些用戶已經(jīng)存過這個數(shù)據(jù)了,且存的是 string 類型,但是新版本上線之后,取數(shù)據(jù)卻用了 object 的方式去取數(shù)據(jù),這就導(dǎo)致了JSON.parse(字符串)會報錯,影響正常的業(yè)務(wù)邏輯~
所以最好是加一個版本號,或者做一下錯誤兼容,這樣就能避免了~
const PROJECT_NAME = 'test-project'
// 每次升級時改變版本號,規(guī)則自己定
const VERSION = 1
// 存數(shù)據(jù)
localStorage.setItem(
`${PROJECT_NAME}_userInfo_${VERSION}`,
JSON.stringify({ name: 'lsx' })
)
// 取數(shù)據(jù)
localStorage.getItem(
`${PROJECT_NAME}_userInfo_${VERSION}`
)
時效性,私密性
時效性,那就是給存進去的數(shù)據(jù)加一個時效,過了某個時間,這個數(shù)據(jù)就時效了,方法就是每次存數(shù)據(jù)進去的時候,加一個時間戳:
// 原來
localStorage.setItem(
`${PROJECT_NAME}_userInfo`,
JSON.stringify({ name: 'lsx' })
)
const TIME_OUT = 3 * 60 * 60 * 1000
// 加時間戳
localStorage.setItem(
`${PROJECT_NAME}_userInfo`,
JSON.stringify({
data: { name: 'lsx' },
// 記錄當前時間
time: new Date().getTime()
})
)
// 取數(shù)據(jù)時判斷時間戳
const get = () => {
let info = localStorage.getItem(
`${PROJECT_NAME}_userInfo_${VERSION}`
)
info = JSON.parse(info)
const now = new Date().getTime()
if (now - info.time >= TIME_OUT) {
localStorage.removeItem(
`${PROJECT_NAME}_userInfo_${VERSION}`
)
return null
}
return info
}
有一些數(shù)據(jù)我們不得不存在 localStorage 中,但是又不想被用戶看到,這時候就需要進行加密了(加密規(guī)則自己定):
// 加密函數(shù)
const encrypt = (v) => {}
// 解密函數(shù)
const decrypt = (v) => {}
// 存數(shù)據(jù)
localStorage.setItem(
`${PROJECT_NAME}_userInfo_${VERSION}`,
// 加密
encrypt(JSON.stringify({ name: 'lsx' }))
)
// 取數(shù)據(jù) 解密
decrypt(localStorage.getItem(
`${PROJECT_NAME}_userInfo_${VERSION}`
))
兼容 SSR
SSR 就是服務(wù)端渲染,是在服務(wù)端運行代碼,拼接成一個頁面,發(fā)送到瀏覽器去展示出來,所以在服務(wù)端是使用不了 localStorage 的,因為不是瀏覽器環(huán)境,所以你像封裝一個比較通用的 localStorage,得兼顧 SSR 的情況:
// 在 SSR 中使用對象替代 localStorage
const SSRStorage = {
map: {},
setItem(v) {
this.map[key] = v
},
getItem(key) {
return this.map[key]
}
}
let storage = null
// 判斷環(huán)境
if (!window) {
storage = SSRStorage
} else {
storage = window.localStorage
}