基于indexDB和Typescript的定时清除取代cookie存储

2,003 阅读5分钟

常用浏览器存储方案

前言

在开发的过程中,经常遇到需要把信息存储在本地的情况,比如权限验证的token、用户信息、埋点计数、自定义皮肤信息或语言种类等。这个时候就会用到浏览器本地存储,主要有cookie、localStorage、sessionStorage、indexDB等几种常用的方案。

cookie

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。

Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie 的使用:

// 前端
document.cookie = "name=trick";

// node
response.setHeader('Set-Cookie', ['name=trick']);

localStorage

一种持久化的存储方式,也就是说如果不手动清除,数据就永远不会过期。它是采用键值对的方式存储数据,按域名将数据分别保存到对应数据库文件里。相比 Cookie 来说,它能保存更大的数据。

localStorage 的使用:

localStorage.setItem('name', 'trick');

localStorage.getItem('name'); // trick

// 移除名称为name的存储数据
localStorage.removeItem('name');

// 移除所有存储数据
localStorage.clear();

sessionStorage

sessionStorage 与 localStorage 相似,不同之处在于 localStorage 里面存储的数据没有过期时间设置,而存储在 sessionStorage 里面的数据在页面会话结束时会被清除。

sessionStorage 的使用:

// 保存数据到 sessionStorage
sessionStorage.setItem('key', 'value');

// 从 sessionStorage 获取数据
let data = sessionStorage.getItem('key');

// 从 sessionStorage 删除保存的数据
sessionStorage.removeItem('key');

// 从 sessionStorage 删除所有保存的数据
sessionStorage.clear();

indexDB

IndexedDB 是一种底层 API,用于客户端存储大量结构化数据,包括文件、二进制大型对象。它是一个基于 JavaScript 的面向对象数据库,允许存储和检索用键索引的对象,可以存储结构化克隆算法支持的任何对象。

打开创建数据库

获取数据库的访问权限,需要在 window 对象的 indexedDB 属性上调用 open( ) 方法,该方法返回一个 IDBRequest 对象。异步操作通过在 IDBRequest 对象上触发事件来和调用程序进行通信。

let db;
/**
* @param dbName 数据库名称
* @param dbVersion  数据库版本
*/
const DBOpenRequest = window.indexedDB.open("toDoList", 4);

DBOpenRequest.onerror = function(event) {
  console.log('启动indexDB失败异常')
};

DBOpenRequest.onsuccess = function(event) {
  db = DBOpenRequest.result;
  console.log('启动indexDB成功')
};

DBOpenRequest.onupgradeneeded = function(event) {
    /**
    * 用于创建对象表库
    * @param tableName 数据表名称
    * @param {keyPath}  用于搜索的主键
    */
   const objectStore = db.createObjectStore("toDoList", { keyPath: "id" });

  /**
  * @param indexName 表列名称
  * @param keyPath  键名称
  * @param {unique} 是否允许该键存在重复的值
  */
  objectStore.createIndex("hours", "hours", { unique: false });
  objectStore.createIndex("minutes", "minutes", { unique: false });
  objectStore.createIndex("day", "day", { unique: false });
};

数据库事务

indexedDB 的事务用于处理数据属性在数据库上提供静态、异步事务,所有数据的读取和写入都是在事务中完成的,并可以手动启动事务,设置事务的模式。

/**
* @param dbName 表名称
* @param 事务模式  readonly/readwrite/versionchange
*/
const transaction = db.transaction(["toDoList"], "readwrite");

/**
* @param dbName 表名称
*/
const objectStore = transaction.objectStore("toDoList");

/**
* 对数据做CRUD操作
*/
const data = [{hours: 19, minutes: 30, day: 24}];

objectStore.add(data);

objectStore.get(id);

objectStore.put(newData);

objectStore.delete(id);

浏览器存储方案区别

特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务端生成,可以设置过期时间除非手动清除,否则一直存在当前页或者浏览器被关闭会被清除除非手动清除,否则一直存在
数据存储大小4k5M5M无限
与服务端通信每次都会携带在 header 头部不参与不参与不参与

基于 indexDB 的定时清除

来源于曾经看到的面试题和实际项目需求,浏览器四种存储方案里除了 cookie 能设置过期时间以外,其他三种存储方式则没有对应的设置过期时间的 api 提供使用。但是由于 cookie 的容量存储比较小、设置数据格式单一、取出数据麻烦等缺点无法满足实际使用,故这里给出基于 indexDB 的封装,同理也可适用于封装 localStorage 和 sessionStorage。

// indexDB操作的api来源于 Ts-IndexDb 的npm工具包,欢迎点下star
type CacheDoc<T> = {
    _id: string;
    data: T;
    expiredAt?: number;
};

const isVoid = (data: any) => {
    if (data === undefined || data === null || Object.is(NaN, data)) {
        return true;
    }
    return false;
}

class FrontendCache {

    private dbName = "system";
    private tableName = "Cache";
    private time = Math.floor(new Date().valueOf() / 1000);

    constructor() {
        initDB({
            dbName: this.dbName,
            version: 3,
            tables: [
                {
                    tableName: this.tableName,
                    option: {
                        keyPath: "_id"
                    },
                    indexes: [
                        {
                            key: "expiredAt",
                            option: { unique: false }
                        }
                    ]
                }
            ]
        });

        setInterval(async () => {
            try {
                await getInstance().delete({
                    tableName: this.tableName,
                    condition: (data: any) => {
                        return data["expiredAt"] <= this.time;
                    }
                });
            } catch (error) {
                console.log(error);
            }
        }, 60_000);
    }

    async get<T>(id: string) {
        const doc = await getInstance().queryByPrimaryKey<CacheDoc<T>>({
            tableName: this.tableName,
            value: id
        });

        if (!isVoid(doc?.data) && (!doc.expiredAt || doc.expiredAt > this.time)) {
            return doc.data;
        }

        await getInstance().delete<CacheDoc<T>>({
            tableName: this.tableName,
            condition: (data) => {
                return data._id === id;
            }
        });
        return null;
    }

    /**
   * @param id
   * @param data
   * @param timeout 过期时间
   */
    async set<T>(id: string, data: T, timeout = 0): Promise<void> {
        const $set: Omit<CacheDoc<T>, "_id"> = { data };
        timeout && ($set.expiredAt = this.time + timeout);
        const result = await getInstance().updateByPrimaryKey<CacheDoc<T>>({
            tableName: this.tableName,
            value: id,
            handle: (value) => {
                const _id = value._id;
                value = { _id, ...$set };
                return value;
            }
        });
        if (!result?._id) {
            await getInstance().insert({
                tableName: this.tableName,
                data: {
                    _id: id,
                    ...$set
                }
            });
        }
    }

    async increase(id: string, amount: number): Promise<number> {
        const result = await getInstance().updateByPrimaryKey<CacheDoc<number>>({
            tableName: this.tableName,
            value: id,
            handle: (value) => {
                const preData = value.data;
                value = { ...value, data: preData + amount };
                return value;
            }
        });
        if (result?._id) {
            return this.get<number>(id) as any;
        } else {
            await getInstance().insert({
                tableName: this.tableName,
                data: {
                    _id: id,
                    data: amount
                }
            });
            return amount;
        }
    }

    async delete(id: string): Promise<boolean> {
        const result: any = await getInstance().delete<CacheDoc<any>>({
            tableName: this.tableName,
            condition: (data) => {
                return data._id === id;
            }
        });

        return result?.length !== 0;
    }

    async pop<T>(id: string): Promise<T> {
        const result: any = await getInstance().delete<CacheDoc<T>>({
            tableName: this.tableName,
            condition: (data) => {
                return data._id === id;
            }
        });
        return (result?.length !== 0 ? (result[0]?.data as T ?? null) : null) as any;
    }
}

定时用法

// 可以挂载在全局方法里
window.$cache = new FrontendCache();
Vue.prototype.$cache = new FrontendCache();

// 添加数据
await $cache.set(1, {a:1});
// {_id:1, data:{a:1}}
await $cache.set(1,{a:1},5000);
//定时5秒清除 {_id:1, data:{a:1}, expiredAt: 1625320214}

// 获取数据
await $cache.get(1);
// {a:1}

// 删除数据
await $cache.delete(1)
// 返回true或者false

// 删除数据并显示删除的数据
await $cache.pop(1)
// {a:1} 并且数据被删除

// 为数字增加值或者增加字符串
await $cache.set(1,200);
await $cache.increase(1,300);
// 打印出500, 数据改为{_id:1, data:500}
await $cache.set(1,'A');
await $cache.increase(1,'B');
// 打印出'AB', 数据改为{_id:1, data:'AB'}

需要源码感兴趣的同学可以去github查看, 有错误缺漏的地方欢迎指正。