JavaScript手寫一個(gè)前端存儲(chǔ)工具庫(kù)
在項(xiàng)目開發(fā)的過(guò)程中,為了減少提高性能,減少請(qǐng)求,開發(fā)者往往需要將一些不易改變的數(shù)據(jù)放入本地緩存中。如把用戶使用的模板數(shù)據(jù)放入 localStorage 或者 IndexedDB。代碼往往如下書寫。
// 這里將數(shù)據(jù)放入內(nèi)存中 let templatesCache = null; // 用戶id,用于多賬號(hào)系統(tǒng) const userId: string = '1'; const getTemplates = ({ refresh = false } = { refresh: false }) => { // 不需要立即刷新,走存儲(chǔ) if (!refresh) { // 內(nèi)存中有數(shù)據(jù),直接使用內(nèi)存中數(shù)據(jù) if (templatesCache) { return Promise.resolve(templatesCache) } const key = `templates.${userId}` // 從 localStorage 中獲取數(shù)據(jù) const templateJSONStr = localStroage.getItem(key) if (templateJSONStr) { try { templatesCache = JSON.parse(templateJSONStr); return Promise.resolve(templatesCache) } catch () { // 解析失敗,清除 storage 中數(shù)據(jù) localStroage.removeItem(key) } } } // 進(jìn)行服務(wù)端掉用獲取數(shù)據(jù) return api.get('xxx').then(res => { templatesCache = cloneDeep(res) // 存入 本地緩存 localStroage.setItem(key, JSON.stringify(templatesCache)) return res }) };
可以看到,代碼非常冗余,同時(shí)這里的代碼還沒有處理數(shù)據(jù)版本、過(guò)期時(shí)間以及數(shù)據(jù)寫入等功能。如果再把這些功能點(diǎn)加入,代碼將會(huì)更加復(fù)雜,不易維護(hù)。
于是個(gè)人寫了一個(gè)小工具 storage-tools 來(lái)處理這個(gè)問題。
使用 storage-tools 緩存數(shù)據(jù)
該庫(kù)默認(rèn)使用 localStorage 作為數(shù)據(jù)源,開發(fā)者從庫(kù)中獲取 StorageHelper 工具類。
import { StorageHelper } from "storage-tools"; // 當(dāng)前用戶 id const userId = "1"; // 構(gòu)建模版 store // 構(gòu)建時(shí)候就會(huì)獲取 localStorage 中的數(shù)據(jù)放入內(nèi)存 const templatesStore = new StorageHelper({ // 多賬號(hào)用戶使用 key storageKey: `templates.${userId}`, // 當(dāng)前數(shù)據(jù)版本號(hào),可以從后端獲取并傳入 version: 1, // 超時(shí)時(shí)間,單位為 秒 timeout: 60 * 60 * 24, }); // 從內(nèi)存中獲取數(shù)據(jù) const templates = templatesStore.getData(); // 沒有數(shù)據(jù),表明數(shù)據(jù)過(guò)期或者沒有存儲(chǔ)過(guò) if (templates === null) { api.get("xxx").then((val) => { // 存儲(chǔ)數(shù)據(jù)到內(nèi)存中去,之后的 getData 都可以獲取到數(shù)據(jù) store.setData(val); // 閑暇時(shí)間將當(dāng)前內(nèi)存數(shù)據(jù)存儲(chǔ)到 localStorage 中 requestIdleCallback(() => { // 期間內(nèi)可以多次掉用 setData store.commit(); }); }); }
StorageHelper 工具類支持了其他緩存源,代碼如下:
import { IndexedDBAdaptor, StorageAdaptor, StorageHelper } from "storage-tools"; // 當(dāng)前用戶 id const userId = "1"; const sessionStorageStore = new StorageHelper({ // 配置同上 storageKey: `templates.${userId}`, version: 1, timeout: 60 * 60 * 24, // 適配器,傳入 sessionStorage adapter: sessionStorage, }); const indexedDBStore = new StorageHelper({ storageKey: `templates.${userId}`, version: 1, timeout: 60 * 60 * 24, // 適配器,傳入 IndexedDBAdaptor adapter: new IndexedDBAdaptor({ dbName: "userInfo", storeName: "templates", }), }); // IndexedDB 只能異步構(gòu)建,所以現(xiàn)在只能等待獲取構(gòu)建獲取完成 indexedDBStore.whenReady().then(() => { // 準(zhǔn)備完成后,我們就可以 getData 和 setData 了 const data = indexedDBStore.getData(); // 其余代碼 }); // 只需要有 setItem 和 getItem 就可以構(gòu)建 adaptor class MemoryAdaptor implements StorageAdaptor { readonly cache = new Map(); // 獲取 map 中數(shù)據(jù) getItem(key: string) { return this.cache.get(key); } setItem(key: string, value: string) { this.cache.set(key, value); } } const memoryStore = new StorageHelper({ // 配置同上 storageKey: `templates.${userId}`, version: 1, timeout: 60 * 60 * 24, // 適配器,傳入攜帶 getItem 和 setItem 對(duì)象 adapter: new MemoryAdaptor(), });
當(dāng)然了,我們還可以繼承 StorageHelper 構(gòu)建業(yè)務(wù)類。
// 也可以基于 StorageHelper 構(gòu)建業(yè)務(wù)類 class TemplatesStorage extends StorageHelper { // 傳入 userId 以及 版本 constructor(userId: number, version: number) { super({ storageKey: `templates.${userId}`, // 如果需要運(yùn)行時(shí)候更新,則可以動(dòng)態(tài)傳遞 version, timeout: 60 * 60 * 24, }); } // TemplatesStorage 實(shí)例 static instance: TemplatesStorage; // 如果需要版本信息的話, static version: number = 0; static getStoreInstance() { // 獲取版本信息 return getTemplatesVersion().then((newVersion) => { // 沒有構(gòu)建實(shí)例或者版本信息不相等,直接重新構(gòu)建 if ( newVersion !== TemplatesStorage.version || !TemplatesStorage.instance ) { TemplatesStorage.instance = new TemplatesStorage("1", newVersion); TemplatesStorage.version = newVersion; } return TemplatesStorage.instance; }); } /** * 獲取模板緩存和 api 請(qǐng)求結(jié)合 */ getTemplates() { const data = super.getData(); if (data) { return Promise.resolve(data); } return api.get("xxx").then((val) => { this.setTemplates(val); return super.getData(); }); } /** * 保存數(shù)據(jù)到內(nèi)存后提交到數(shù)據(jù)源 */ setTemplats(templates: any[]) { super.setData(templates); super.commit(); } } /** * 獲取模版信息函數(shù) */ const getTemplates = () => { return TemplatesStorage.getStoreInstance().then((instance) => { return instance.getTemplates(); }); };
針對(duì)于某些特定列表順序需求,我們還可以構(gòu)建 ListStorageHelper。
import { ListStorageHelper, MemoryAdaptor } from "../src"; // 當(dāng)前用戶 id const userId = "1"; const store = new ListStorageHelper({ storageKey: `templates.${userId}`, version: 1, // 設(shè)置唯一鍵 key,默認(rèn)為 'id' key: "searchVal", // 列表存儲(chǔ)最大數(shù)據(jù)量,默認(rèn)為 10 maxCount: 100, // 修改數(shù)據(jù)后是否移動(dòng)到最前面,默認(rèn)為 true isMoveTopWhenModified: true, // 添加數(shù)據(jù)后是否是最前面, 默認(rèn)為 true isUnshiftWhenAdded: true, }); store.setItem({ searchVal: "new game" }); store.getData(); // [{ // searchVal: 'new game' // }] store.setItem({ searchVal: "new game2" }); store.getData(); // 會(huì)插入最前面 // [{ // searchVal: 'new game2' // }, { // searchVal: 'new game' // }] store.setItem({ searchVal: "new game" }); store.getData(); // 會(huì)更新到最前面 // [{ // searchVal: 'new game' // }, { // searchVal: 'new game2' // }] // 提交到 localStorage store.commit();
storage-tools 項(xiàng)目演進(jìn)
任何項(xiàng)目都不是一觸而就的,下面是關(guān)于 storage-tools 庫(kù)的編寫思路。希望能對(duì)大家有一些幫助。
StorageHelper 支持 localStorage 存儲(chǔ)
項(xiàng)目的第一步就是支持本地儲(chǔ)存 localStorage 的存取。
// 獲取從 1970 年 1 月 1 日 00:00:00 UTC 到用戶機(jī)器時(shí)間的秒數(shù) // 后續(xù)有需求也會(huì)向外提供時(shí)間函數(shù)配置,可以結(jié)合 sync-time 庫(kù)一起使用 const getCurrentSecond = () => parseInt(`${new Date().getTime() / 1000}`); // 獲取當(dāng)前空數(shù)據(jù) const getEmptyDataStore = (version: number): DataStore<any> => { const currentSecond = getCurrentSecond(); return { // 當(dāng)前數(shù)據(jù)的創(chuàng)建時(shí)間 createdOn: currentSecond, // 當(dāng)前數(shù)據(jù)的修改時(shí)間 modifiedOn: currentSecond, // 當(dāng)前數(shù)據(jù)的版本 version, // 數(shù)據(jù),空數(shù)據(jù)為 null data: null, }; }; class StorageHelper<T> { // 存儲(chǔ)的 key private readonly storageKey: string; // 存儲(chǔ)的版本信息 private readonly version: number; // 內(nèi)存中數(shù)據(jù),方便隨時(shí)讀寫 store: DataStore<T> | null = null; constructor({ storageKey, version }) { this.storageKey = storageKey; this.version = version || 1; this.load(); } load() { const result: string | null = localStorage.getItem(this.storageKey); // 初始化內(nèi)存信息數(shù)據(jù) this.initStore(result); } private initStore(storeStr: string | null) { // localStorage 沒有數(shù)據(jù),直接構(gòu)建 空數(shù)據(jù)放入 store if (!storeStr) { this.store = getEmptyDataStore(this.version); return; } let store: DataStore<T> | null = null; try { // 開始解析 json 字符串 store = JSON.parse(storeStr); // 沒有數(shù)據(jù)或者 store 沒有 data 屬性直接構(gòu)建空數(shù)據(jù) if (!store || !("data" in store)) { store = getEmptyDataStore(this.version); } else if (store.version !== this.version) { // 版本不一致直接升級(jí) store = this.upgrade(store); } } catch (_e) { // 解析失敗了,構(gòu)建空的數(shù)據(jù) store = getEmptyDataStore(this.version); } this.store = store || getEmptyDataStore(this.version); } setData(data: T) { if (!this.store) { return; } this.store.data = data; } getData(): T | null { if (!this.store) { return null; } return this.store?.data; } commit() { // 獲取內(nèi)存中的 store const store = this.store || getEmptyDataStore(this.version); store.version = this.version; const now = getCurrentSecond(); if (!store.createdOn) { store.createdOn = now; } store.modifiedOn = now; // 存儲(chǔ)數(shù)據(jù)到 localStorage localStorage.setItem(this.storageKey, JSON.stringify(store)); } /** * 獲取內(nèi)存中 store 的信息 * 如 modifiedOn createdOn version 等信息 */ get(key: DataStoreInfo) { return this.store?.[key]; } upgrade(store: DataStore<T>): DataStore<T> { // 獲取當(dāng)前的秒數(shù) const now = getCurrentSecond(); // 看起來(lái)很像 getEmptyDataStore 代碼,但實(shí)際上是不同的業(yè)務(wù) // 不應(yīng)該因?yàn)榇a相似而合并,不利于后期擴(kuò)展 return { // 只獲取之前的創(chuàng)建時(shí)間,如果沒有使用當(dāng)前的時(shí)間 createdOn: store?.createdOn || now, modifiedOn: now, version: this.version, data: null, }; } }
StorageHelper 添加超時(shí)機(jī)制
添加超時(shí)機(jī)制很簡(jiǎn)單,只需要在 getData 的時(shí)候檢查一下數(shù)據(jù)即可。
class StorageHelper<T> { // 其他代碼 ... // 超時(shí)時(shí)間,默認(rèn)為 -1,即不超時(shí) private readonly timeout: number = -1; constructor({ storageKey, version, timeout }: StorageHelperParams) { // 傳入的數(shù)據(jù)是數(shù)字類型,且大于 0,就設(shè)定超時(shí)時(shí)間 if (typeof timeout === "number" && timeout > 0) { this.timeout = timeout; } } getData(): T | null { if (!this.store) { return null; } // 如果小于 0 就沒有超時(shí)時(shí)間,直接返回?cái)?shù)據(jù),事實(shí)上不可能小于0 if (this.timeout < 0) { return this.store?.data; } // 修改時(shí)間加超時(shí)時(shí)間大于當(dāng)前時(shí)間,則表示沒有超時(shí) // 注意,每次 commit 都會(huì)更新 modifiedOn if (getCurrentSecond() < (this.store?.modifiedOn || 0) + this.timeout) { return this.store?.data; } // 版本信息在最開始時(shí)候處理過(guò)了,此處直接返回 null return null; } }
StorageHelper 添加其他存儲(chǔ)適配
此時(shí)我們可以添加其他數(shù)據(jù)源適配,方便開發(fā)者自定義 storage。
/** * 適配器接口,存在 getItem 以及 setItem */ interface StorageAdaptor { getItem: (key: string) => string | Promise<string> | null; setItem: (key: string, value: string) => void; } class StorageHelper<T> { // 其他代碼 ... // 非瀏覽器環(huán)境不具備 localStorage,所以不在此處直接構(gòu)造 readonly adapter: StorageAdaptor; constructor({ storageKey, version, adapter, timeout }: StorageHelperParams) { // 此處沒有傳遞 adapter 就會(huì)使用 localStorage // adapter 對(duì)象必須有 getItem 和 setItem // 此處沒有進(jìn)一步判斷 getItem 是否為函數(shù)以及 localStorage 是否存在 // 沒有辦法限制住所有的異常 this.adapter = adapter && "getItem" in adapter && "setItem" in adapter ? adapter : localStorage; this.load(); } load() { // 此處改為 this.adapter const result: Promise<string> | string | null = this.adapter.getItem( this.storageKey, ); } commit() { // 此處改為 this.adapter this.adapter.setItem(this.storageKey, JSON.stringify(store)); } }
StorageHelper 添加異步獲取
如有些數(shù)據(jù)源需要異步構(gòu)建并獲取數(shù)據(jù),例如 IndexedDB 。這里我們先建立一個(gè) IndexedDBAdaptor 類。
import { StorageAdaptor } from "../utils"; // 把 indexedDB 的回調(diào)改為 Promise function promisifyRequest<T = undefined>( request: IDBRequest<T> | IDBTransaction, ): Promise<T> { return new Promise<T>((resolve, reject) => { // @ts-ignore request.oncomplete = request.onsuccess = () => resolve(request.result); // @ts-ignore request.onabort = request.onerror = () => reject(request.error); }); } /** * 創(chuàng)建并返回 indexedDB 的句柄 */ const createStore = ( dbName: string, storeName: string, upgradeInfo: IndexedDBUpgradeInfo = {}, ): UseStore => { const request = indexedDB.open(dbName); /** * 創(chuàng)建或者升級(jí)時(shí)候會(huì)調(diào)用 onupgradeneeded */ request.onupgradeneeded = () => { const { result: store } = request; if (!store.objectStoreNames.contains(storeName)) { const { options = {}, indexList = [] } = upgradeInfo; // 基于 配置項(xiàng)生成 store const store = request.result.createObjectStore(storeName, { ...options }); // 建立索引 indexList.forEach((index) => { store.createIndex(index.name, index.keyPath, index.options); }); } }; const dbp = promisifyRequest(request); return (txMode, callback) => dbp.then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)) ); }; export class IndexedDBAdaptor implements StorageAdaptor { private readonly store: UseStore; constructor({ dbName, storeName, upgradeInfo }: IndexedDBAdaptorParams) { this.store = createStore(dbName, storeName, upgradeInfo); } /** * 獲取數(shù)據(jù) */ getItem(key: string): Promise<string> { return this.store("readonly", (store) => promisifyRequest(store.get(key))); } /** * 設(shè)置數(shù)據(jù) */ setItem(key: string, value: string) { return this.store("readwrite", (store) => { store.put(value, key); return promisifyRequest(store.transaction); }); } }
對(duì) StorageHelper 類做如下改造
type CreateDeferredPromise = <TValue>() => CreateDeferredPromiseResult<TValue>; // 劫持一個(gè) Promise 方便使用 export const createDeferredPromise: CreateDeferredPromise = <T>() => { let resolve!: (value: T | PromiseLike<T>) => void; let reject!: (reason?: any) => void; const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; }); return { currentPromise: promise, resolve, reject, }; }; export class StorageHelper<T> { // 是否準(zhǔn)備好了 ready: CreateDeferredPromiseResult<boolean> = createDeferredPromise< boolean >(); constructor({ storageKey, version, adapter, timeout }: StorageHelperParams) { this.load(); } load() { const result: Promise<string> | string | null = this.adapter.getItem( this.storageKey, ); // 檢查一下當(dāng)前的結(jié)果是否是 Promise 對(duì)象 if (isPromise(result)) { result .then((res) => { this.initStore(res); // 準(zhǔn)備好了 this.ready.resolve(true); }) .catch(() => { this.initStore(null); // 準(zhǔn)備好了 this.ready.resolve(true); }); } else { // 不是 Promise 直接構(gòu)建 store this.initStore(result); // 準(zhǔn)備好了 this.ready.resolve(true); } } // 詢問是否做好準(zhǔn)備 whenReady() { return this.ready.currentPromise; } }
如此,我們就完成了 StorageHelper 全部代碼。
列表輔助類 ListStorageHelper
ListStorageHelper 基于 StorageHelper 構(gòu)建,方便特定業(yè)務(wù)使用。
// 數(shù)組最大數(shù)量 const STORE_MAX_COUNT: number = 10; export class ListStorageHelper<T> extends StorageHelper<T[]> { // 主鍵,默認(rèn)為 id readonly key: string = "id"; // 存儲(chǔ)最大數(shù)量,默認(rèn)為 10 readonly maxCount: number = STORE_MAX_COUNT; // 是否添加在最前面 readonly isUnshiftWhenAdded: boolean = true; // 修改后是否放入最前面 readonly isMoveTopWhenModified: boolean = true; constructor({ maxCount, key, isMoveTopWhenModified = true, isUnshiftWhenAdded = true, storageKey, version, adapter, timeout, }: ListStorageHelperParams) { super({ storageKey, version, adapter, timeout }); this.key = key || "id"; // 設(shè)置配置項(xiàng) if (typeof maxCount === "number" && maxCount > 0) { this.maxCount = maxCount; } if (typeof isMoveTopWhenModified === "boolean") { this.isMoveTopWhenModified = isMoveTopWhenModified; } if (typeof this.isUnshiftWhenAdded === "boolean") { this.isUnshiftWhenAdded = isUnshiftWhenAdded; } } load() { super.load(); // 沒有數(shù)據(jù),設(shè)定為空數(shù)組方便統(tǒng)一 if (!this.store!.data) { this.store!.data = []; } } getData = (): T[] => { const items = super.getData() || []; // 檢查數(shù)據(jù)長(zhǎng)度并移除超過(guò)的數(shù)據(jù) this.checkThenRemoveItem(items); return items; }; setItem(item: T) { if (!this.store) { throw new Error("Please complete the loading load first"); } const items = this.getData(); // 利用 key 去查找存在數(shù)據(jù)索引 const index = items.findIndex( (x: any) => x[this.key] === (item as any)[this.key], ); // 當(dāng)前有數(shù)據(jù),是更新 if (index > -1) { const current = { ...items[index], ...item }; // 更新移動(dòng)數(shù)組數(shù)據(jù) if (this.isMoveTopWhenModified) { items.splice(index, 1); items.unshift(current); } else { items[index] = current; } } else { // 添加 this.isUnshiftWhenAdded ? items.unshift(item) : items.push(item); } // 檢查并移除數(shù)據(jù) this.checkThenRemoveItem(items); } removeItem(key: string | number) { if (!this.store) { throw new Error("Please complete the loading load first"); } const items = this.getData(); const index = items.findIndex((x: any) => x[this.key] === key); // 移除數(shù)據(jù) if (index > -1) { items.splice(index, 1); } } setItems(items: T[]) { if (!this.store) { return; } this.checkThenRemoveItem(items); // 批量設(shè)置數(shù)據(jù) this.store.data = items || []; } /** * 多添加一個(gè)方法 getItems,等同于 getData 方法 */ getItems() { if (!this.store) { return null; } return this.getData(); } checkThenRemoveItem = (items: T[]) => { if (items.length <= this.maxCount) { return; } items.splice(this.maxCount, items.length - this.maxCount); }; }
該類繼承了 StorageHelper,我們依舊可以直接調(diào)用 commit 提交數(shù)據(jù)。如此我們就不需要維護(hù)復(fù)雜的 storage 存取邏輯了。
代碼都在 storage-tools 中,歡迎各位提交 issue 以及 pr。
到此這篇關(guān)于JavaScript手寫一個(gè)前端存儲(chǔ)工具庫(kù)的文章就介紹到這了,更多相關(guān)JavaScript前端存儲(chǔ)工具庫(kù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS匿名函數(shù)和匿名自執(zhí)行函數(shù)概念與用法分析
這篇文章主要介紹了JS匿名函數(shù)和匿名自執(zhí)行函數(shù)概念與用法,結(jié)合實(shí)例形式分析了匿名函數(shù)和匿名自執(zhí)行函數(shù)的概念、功能、應(yīng)用場(chǎng)景及相關(guān)使用技巧,需要的朋友可以參考下2018-03-03使用JavaScript實(shí)現(xiàn)文本收起展開(省略)功能
省略號(hào),作為一種常見的文本處理方式,在很多情況下都十分常見,特別是當(dāng)我們需要在省略號(hào)后面添加額外文字時(shí),本文為大家介紹了使用JavaScript實(shí)現(xiàn)文本收起展開功能的相關(guān)方法,希望對(duì)大家有所幫助2024-04-04JS+JQuery實(shí)現(xiàn)無(wú)縫連接輪播圖
這篇文章主要介紹了JS+JQuery實(shí)現(xiàn)無(wú)縫連接輪播圖,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12JS清空上傳控件input(type="file")的值的代碼
最近做的一個(gè)小功能,需要清空<input type="file">的值,但上傳控件<input type="file">的值不能通過(guò)JavaScript來(lái)修改。2008-11-11webpack打包時(shí)如何修改文件名的實(shí)現(xiàn)示例
本文主要介紹了webpack打包時(shí)如何修改文件名的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06如何用threejs實(shí)現(xiàn)實(shí)時(shí)多邊形折射
這篇文章主要介紹了如何用threejs實(shí)現(xiàn)實(shí)時(shí)多邊形折射,對(duì)three.js庫(kù)感興趣的同學(xué),可以參考下2021-05-05