前端页面数据缓存方案

750 阅读7分钟

当用户刷新页面,不管是有意还是无意时,页面数据会重新从后端获取,导致用户填写的表单数据丢失。另外有些多 tab 设计的页面,当用户打开多个 tab,刷新页面后 tab 会消失。这些都会导致用户体验变差,本方案使用 sessionStorage+indexDb 可以很方便安全的缓存当前页面的数据。

痛点

你在web开发的过程中,是否碰到过这样的设计:

1、页面上是tab设计,用户可以打开多个tab,每个tab数据隔离。

2、重表单设计,页面上有大量表单需要用户输入。

如果有这样的设计,那么肯定会有很多用户反馈一个问题,那就是页面刷新时tab没有了,表单输入的内容没有了。

有时候,我们碰到这样的用户反馈,肯定也很烦躁,恨不得甩用户一脸子,“没事,你刷新干啥!”

其实,有时候确实不怪用户。用户刷新页面可能有以下几种可能:

1、不小心按到F5了。

2、不小心回退路由了。

3、不小心在当前页面点了书签。

4、不小心点击了在当前页面更新的跳转。

5、浏览器卡了。

6、页面崩溃了。

7、单纯就是手贱。

.......

不管是什么原因吧,当前页面的数据的丢失还是非常影响用户体验的。

那怎么解决这个问题呢?对于有经验的前端er,首先想到的肯定是sessionStoragelocalStorage。但是这两个货,外强中干,如果页面数据量太大就扛不住了(大概5~10M)。

有人说,5~10M哪个页面都够用了啊,表单数据没有那么大吧!

可能对于大部分页面5~10M措措有余,但是对于我们大数据平台,web手撸代码的输入的页面,一个tab保存2M的数据非常普遍。而且也一个页面可以最多有10tab,很多用户都喜欢一个浏览器开N个任务开发的页面。所以,sessionStoragelocalStorage分分钟被虐哭。

因此,最安全的方案只能是indexDb

不过indexDb也有缺陷。毕竟我们只希望缓存当前页面的数据,并不希望这个数据被长期保存。另外,indexDb是跟随在同一个域下。也就是同一个浏览器中,打开多个同一个网站的页签,所有页签共用一个indexDbstore。那么怎么实现页签级别的隔离呢?

下面介绍下我的解决方案 @rasir/page-storage

介绍

page-storage 使用 sessionStorageindexDb 对当前页签的数据进行缓存,当页面关闭后缓存的数据失效。

工作原理

1、page-storage使用sessionStorage缓存当前窗口的uuid,当页面刷新时会读取sessionStorage中的uuid,如果存在则读取缓存数据,不存在则重新生成新的uuid

2、page-storage 基于 indexDb 封装,使用简单,支持数据缓存,数据删除,数据更新,数据查询等操作,存储在indexDb中的key为每一个窗口的uuid,并且会将uuid的数据打上过期时间(expiredTime),默认 10s(可以自定义)。

3、page-storage使用web workerindexDb中数据进行轮询续期,续期周期为过期时间(expiredTime)的一半,默认 5s(可以自定义)。

4、page-storage还是用了@rasir/chain-promise-call方案,让所有操作都工作在链式调用中,保证indexDb操作的时续。

5、page-storage 所有api都支持promise操作。

形象地说

这就像给每个窗口写上一串代号(UUID),并藏在冰箱(sessionStorage)里。当我们重新进入厨房刷新页面时,就会打开冰箱找代号。如果找到了就取出我们做好放在冰箱里的食物(读取缓存数据),如果没找到就只好重新做饭(生成新的UUID)。

page-storage 宛如一个敏捷灵活、技高一筹的厨师,在 indexDb 的厨房里尽情挥舞长柄勺和菜刀。他能把食物藏好(支持数据缓存),清除垃圾(数据删除),改变菜肴口味(数据更新)以及寻找指定食材(数据查询)。每道菜都配有编号(窗口uuid),并标上保质期(expiredTime),默认10秒钟。

那保质期结束后怎么办呢?别着急!我们雇佣了 web worker 这位全职保姆,在食物快要腐坏的时候就给它续一下保质期,周期为过期时间(expiredTime)的一半。默认周期是5秒。

另外,我们还使用了@rasir/chain-promise-call,这就像一个魔法阵,让所有操作都在链式调用中高效运转,保证了食物烹饪过程中步骤的次序和连接。

注意事项

1、浏览器兼容性:只要是支持Promise的浏览器版本都能兼容。

2、依赖@rasir/chain-promise-call

3、本项目使用的 "core-js": "^3.32.1",如果在使用过程中出现兼容问题,请检查core-js版本。

4、本文中说是的链式调用并不是jQuery式的链式调用,而是将所有链式api放入一个栈中,当前一个异步函数执行完毕,才会执行下一个函数。保证indexDb操作数据存储的时序。

快速使用

npm i @rasir/page-storage -S

使用方法

1、html中直接引用min.js


<script src="xxx/xxx/page-storage-0.0.1.min.js"></script>

<script>

const storage = new PageStorage({

dbName: "demo_db",

sessionWindowKey: "DEMO_KEY",

});

storage.chainSavePageStorage({ a: 1, b: 2 });

storage.chainGetPageStorage().then((data) => {

console.log(data);

});

</script>

2、npm使用。


import PageStorage from "@rasir/page-storage";

const storage = new PageStorage({

dbName: "demo_db",

sessionWindowKey: "DEMO_KEY",

});

storage.chainSavePageStorage({ a: 1, b: 2 });

storage.chainGetPageStorage().then((data) => {

console.log(data);

});

参数说明


interface IPageStorage {

noWindowName?: boolean; // 是否不使用窗口名称作为窗口特征码,默认 false

windowName?: string; // 当前窗口的特征码,默认 PAGE_STORAGE

windowId?: string; // 当前窗口的uuid

expiredTime?: number; // 过期时间,单位秒,默认 10s

dbName: string; // indexdb中的storeName 必传

sessionWindowKey: string; // 在 sessionStorage 中保存 windowId 的 key 必传

}

APIs

API说明参数类型
getPageStorage获取当前窗口数据(): Promise<T | undefined>
savePageStorage保存当前窗口数据(data: T): Promise<boolean>
updatePageStorage直接修改当前窗口的数据(callback: (data?: T) => T | Promise<T>): Promise<boolean>;
removePageStorage删除当前窗口的数据(): Promise<boolean>;
dispose停止给数据续期(): Promise<boolean>;
chainGetPageStorage加入链式调用的获取当前窗口数据(): Promise<T | undefined>
chainSavePageStorage加入链式调用的保存当前窗口数据(data: T): Promise<boolean>;
chainUpdatePageStorage加入链式调用的直接修改当前窗口的数据(callback: (data?: T) => T | Promise<T>): Promise<boolean>;
chainRemovePageStorage加入链式调用的删除当前窗口的数据(): Promise<boolean>;
chainDispose加入链式调用的停止给数据续期(): Promise<boolean>;
chainPromiseCall链式调用函数,可以使用chainPromiseCall.onStatusChangechainPromiseCall.offStatusChange对实例状态进行监听详见@rasir/chain-promise-call

问题和解决方案

  • 问题:当使用 window.open 从一个页面打开一个新的页面时,sessionStorage 中的数据会被复制到新的页面。这样会导致我们使用 sessionWindowKey 来作为每个页面独立存储的设计失效。

  • 解决方案:

  1. 使用window.name 来给当前页面打标签,因为刷新页面时 window.name 不会重置。因此在构造函数中,我添加了 windowName 属性,用来判断当前页面是否是新开的。如果页面新开,并且不带 windowName 的特征码,就清除 sessionStorage 中的 sessionWindowKey ,并给 window.name设置带特征码的 name。当然,如果你不想我的 window.name 扰乱了你的设计,你可以设置 noWindowName 属性,这样你需要自己来保证 sessionStorage 的唯一性。

  2. 抛弃使用原生的 window.open 函数。可以用 <a> 标签来进行跳转,或者用模拟 <a> 标签点击跳转,如下面这样:


let link = document.createElement("a");

link.href = "http://example.com";

link.target = "_blank";

link.click();

  • 问题:如果是在 iframe 或者微前端这类沙箱隔离的场景下,window.name 可能无法被修改成功。

  • 解决方案:这种情况下就需要你自己来保证 sessionStorage 的唯一性。或者参看上文中的<a>标签方案。