封装indexDB成为通用的存储方案

779 阅读4分钟

介绍

浏览器本地存储API有很多,例如:CookieIndexDBWebStorage 他们都有各自的缺点,我们希望设计一套通用的本地存储API去解决这个问题。

首先我们要知道每个本地存储API的缺点:

  • Cookie:容量小(大约4KB),只能存储字符串类型的数据,发送请求时浏览器会自动携带对应的 Cookie 无法干预此操作。
  • IndexDB:容量大(当前可用磁盘空间的50%),可以存储任意类型的数据,存储又可以分为临时存储(默认)和持久存储,但是老版本浏览器不兼容。
    • 临时存储:默认行为,超过存储容量会采用 LRU 算法自动清空最近最少使用的源
    • 持久存储:尚在实验阶段,通过 navigator.storage.persist() 请求本地数据存储的权限返回 Promise 对象状态值为布尔值表示开启成功或失败。
    • 全局存储限制:当前可用磁盘空间的 50%。
    • 组存储限制:为全局限制的 20%,但它至少有 10 MB,最大为 2GB。相同 eTLD+1被视为一个,每个具体的子域或域被视为一个
  • WebStorage:包含两种类型:localStoragesessionStorage,存储容量(大约5MB)。
    • localStorage:只能存储字符串类型的数据,浏览器兼容性好。
    • sessionStorage:只能存储字符串类型的数据,只在当前会话中有效,页面关闭数据清除。

eTLD+1域e表示可用的,TLD 表示顶级域名,TLD+1 表示顶级域名和直接上级的域名。

:所有具有相同 eTLD+1 的域被视为一个组。在这个组中,所有源共享一个存储配额,这个配额是全局存储限制的20%。

:每个具体的子域或域被视为一个源。例如:mozilla.orgwww.mozilla.orgjoe.blogs.mozilla.org 都是不同的源但是他们具有相同的 eTLD+1域 所以共享一个存储配额。

对于 indexDB 中 eTLD+1 概念不清楚可查阅官方文档:indexDB浏览器存储限制和清理标准

indexDB基本使用

const request = window.indexedDB.open("MyTestDatabase", 3); // 开启数据库。

request.onupgradeneeded = (event) => {
  const db = event.target.result;

  // 为数据库创建对象存储(objectStore)
  const objectStore = db.createObjectStore("form", { keyPath: "myKey" });
  
  // 创建索引,方便后续查找。
  objectStore.createIndex("name", "name", { unique: false });
};

// 后续封装 inexdDB 时也需要在 onsucess 回调中拿到 event.target.result 在执行其他操作。
request.onsucess = (event) => {
    const db = event.target.result;
    
    // 想要操作数据必须开启事务。
    const transaction = db.transaction(["customers"], "readwrite");
    const objectStore = transaction.objectStore('form'); // 拿到对应的对象存储。
    
    // 在 onupgradeneeded 设置了索引之后才可以直接删除,否则需要使用游标查找对应的项之后在执行删除操作。
    const request = objectStore.delete(key); // 执行删除操作。
    request.onsuccess = (event) => {
        // event.target.result 是删除的结果。
    });
    
    // 使用游标查找。
    const request$1 = objectStore.openCursor();
    request$1.onsuccess = function(event) {
        const cursor = event.target.result;
        if(cursor){
            // 这里进行筛查要对哪个数据进行增删改操作。
            if(shouldStopProcessing(cursor)){ // 找到对应的数据。
                cursor.delete(); // 执行删除操作。
            } else {
                cursor.continue(); // 不断取出下一项。
            }
        } else {
            console.wran('已经没有更多数据了');
        }
    }
    
}

// 保存 IDBDatabase 接口。
request.onerror = (event) => {
  console.error(`数据库错误:${event.target.errorCode}`);
};

预期的结果:

基于以上的问题能够使用的API只有 indexDBlocalStorage,我们希望可以在现代浏览器中使用 indexDB 在老版本浏览器中使用 localStorage 进行存储数据,

遇到的问题:

它们的存储方式是不同的,例如:indexDB 需要调用 open 方法开启数据库后才开始存储操作并且所有的操作都是基于异步的,而 localStorage 只需要使用 getItemsetItem等方法直接同步存储。

如何解决:

我们可以通过模板模式设置一个基类内部包含 getItemsetItemremoveItemcleargetAll等方法并且要求所有操作都是异步的,让 indexDBlocalStorage 都继承该基类去解决问题。

代码实现

设置模板:

class Template {
    getItem(key){
        throw new Error('必须实现 getItem 方法');
    },
    setItem(key,value){
        throw new Error('必须实现 setItem 方法');
    },
    removeItem(key){
        throw new Error('必须实现 removeItem 方法');
    },
    clear(){
        throw new Error('必须实现 clear 方法');
    },
    getAll(){
        throw new Error('必须实现 getAll 方法');
    }
}

实现LocalStorage类:

class LocalStorage {

    getItem(key) {
        return new Promise((resolve, reject) => {
            const result = localStorage.getItem(key);
            try {
                resolve({
                    status: true,
                    message: '获取数据成功',
                    data: JSON.parse(result)
                });
            } catch (e) {
                resolve({
                    status: true,
                    message: '获取数据成功',
                    data: result
                });
            }
        });
    }

    setItem(key, value) {
        return new Promise((resolve, reject) => {
            try {
                localStorage.setItem(key, value);
                resolve({
                    status: true,
                    message: '新增数据成功',
                    data: null
                });
            } catch (e) {
                reject(e);
            }

        });
    }

    removeItem(key) {
        return new Promise((resolve, reject) => {
            try {
                localStorage.removeItem(key);
                resolve({
                    status: true,
                    message: '删除数据成功',
                    data: null
                });
            } catch (e) {
                reject(e);
            }
        });
    }

    clear() {
        return new Promise((resolve, reject) => {
            try {
                localStorage.clear();
                resolve({
                    status: true,
                    message: '清除所有数据成功',
                    data: null
                });
            } catch (e) {
                reject(e);
            }
        })
    }

    getAll() {
        return new Promise((resolve, reject) => {
            try {
                const len = localStorage.length;
                const items = [];
                for (let i = 0; i < len; i++) {
                    const key = localStorage.key(i);
                    const value = localStorage.getItem(key);
                    items.push({key: value});
                }
                resolve({
                    status: true,
                    message: '获取所有数据成功',
                    data: items
                })
            } catch (e) {
                reject(e);
            }
        })
    }
}

实现IndexDB类

// 这个方法用于打开 indexDB 数据库并返回 Promise 对象。
function openDatabase(databaseName, version, storeName = 'store_personal') {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(databaseName, version);

        request.onupgradeneeded = (event) => {
            const db = event.target.result;

            // 为数据库创建对象存储(objectStore)
            const objectStore = db.createObjectStore(storeName, {
                keyPath: "key",
                autoIncrement: true
            });

            // 创建一个索引
            objectStore.createIndex("key", "key", {unique: false});
        };

        request.onsuccess = (event) => {
            resolve(event);
        };

        request.onerror = (event) => {
            console.error(`数据库错误:${event.target.errorCode}`);
            reject(event.target.errorCode);
        };
    });
}

class IndexDB {
    constructor(databaseName,version,storeName) {
        this.IDBDatabase = openDatabase(databaseName,version,storeName);
        this.storeName = storeName;
    }

    getItem(key) {
        return new Promise(async (resolve) => {
            const IDBDatabase = await this.IDBDatabase; // 等待外界拿到indexDB数据库实例。
            const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readonly');
            const objectStore = transaction.objectStore(this.storeName);
            const request = objectStore.get(key);
            request.onsuccess = (event) => {
                resolve({
                    status: true,
                    message: '查找成功',
                    data: event.target.result || request.result
                });
            };

            request.onerror = (event) => {
                resolve({
                    status: false,
                    message: '查找失败',
                    data: event.target.error || null
                });
            }
        });
    }

    setItem(key, value) {
        return new Promise(async (resolve, reject) => {
            const IDBDatabase = await this.IDBDatabase;
            const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readwrite');
            const objectStore = transaction.objectStore(this.storeName);

            // put方法如果之前存在则修改,否则新增。如果想要put修改生效则需要设置主键让数据库知道哪些数据是相同的。
            const request = objectStore.put({key, value});
            request.onsuccess = () => {
                resolve({
                    status: true,
                    message: '新增成功',
                    data: {key, value}
                })
            }

            request.onerror = (event) => {
                resolve({
                    status: false,
                    message: '新增失败',
                    data: event.target.error || null
                });
            }
        });
    }

    deleteItem(key) {
        return new Promise(async (resolve) => {
            const IDBDatabase = await this.IDBDatabase;
            const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readwrite');
            const objectStore = transaction.objectStore(this.storeName);
            const request = objectStore.delete(key);

            request.onsuccess = (event) => {
                resolve({
                    status: true,
                    message: '删除成功',
                    data: event.target.result || request.result
                });
            }

            request.onerror = (event) => {
                resolve({
                    status: false,
                    message: '删除失败',
                    data: event.target.error || null
                });
            }
        });
    }

    clear() {
        return new Promise(async (resolve) => {
            const IDBDatabase = await this.IDBDatabase;
            const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readwrite');
            const objectStore = transaction.objectStore(this.storeName);
            const request = objectStore.clear();

            request.onsuccess = (event) => {
                resolve({
                    status: true,
                    message: '数据清除成功',
                    data: null
                });
            }

            request.onerror = (event) => {
                resolve({
                    status: false,
                    message: '数据清除失败',
                    data: event.target.error || null
                });
            }
        });
    }

    getAll() {
        return new Promise(async (resolve) => {
            const IDBDatabase = await this.IDBDatabase;
            const transaction = IDBDatabase.target.result.transaction([this.storeName], 'readwrite');
            const objectStore = transaction.objectStore(this.storeName);
            const request = objectStore.getAll();

            request.onsuccess = (event) => {
                resolve({
                    status: true,
                    message: '获取所有数据成功',
                    data: event.target.result || request.result
                });
            }

            request.onerror = (event) => {
                resolve({
                    status: false,
                    message: '获取所有数据失败',
                    data: event.target.error || null
                });
            }
        });
    }
}

根据浏览器支持情况选择对应的类:

function useStore() {
    const _indexDB = new IndexDB('store', 1, 'store_personal');
    const _localStorage = new LocalStorage();
    const strategy = window.indexedDB !== undefined ? _indexDB : _localStorage;

    return {
        getItem(key) {
            return strategy.getItem(key);
        },
        setItem(key,value) {
            return strategy.setItem(key,value);
        },
        removeItem(key) {
            return strategy.removeItem(key);
        },
        clear(){
            return strategy.clear();
        },
        getAll(){
            return strategy.getAll();
        }
    }
}