当用户刷新页面,不管是有意还是无意时,页面数据会重新从后端获取,导致用户填写的表单数据丢失。另外有些多 tab 设计的页面,当用户打开多个 tab,刷新页面后 tab 会消失。这些都会导致用户体验变差,本方案使用 sessionStorage+indexDb 可以很方便安全的缓存当前页面的数据。
痛点
你在web开发的过程中,是否碰到过这样的设计:
1、页面上是tab设计,用户可以打开多个tab,每个tab数据隔离。
2、重表单设计,页面上有大量表单需要用户输入。
如果有这样的设计,那么肯定会有很多用户反馈一个问题,那就是页面刷新时tab没有了,表单输入的内容没有了。
有时候,我们碰到这样的用户反馈,肯定也很烦躁,恨不得甩用户一脸子,“没事,你刷新干啥!”
其实,有时候确实不怪用户。用户刷新页面可能有以下几种可能:
1、不小心按到F5了。
2、不小心回退路由了。
3、不小心在当前页面点了书签。
4、不小心点击了在当前页面更新的跳转。
5、浏览器卡了。
6、页面崩溃了。
7、单纯就是手贱。
.......
不管是什么原因吧,当前页面的数据的丢失还是非常影响用户体验的。
那怎么解决这个问题呢?对于有经验的前端er,首先想到的肯定是sessionStorage和localStorage。但是这两个货,外强中干,如果页面数据量太大就扛不住了(大概5~10M)。
有人说,5~10M哪个页面都够用了啊,表单数据没有那么大吧!
可能对于大部分页面5~10M措措有余,但是对于我们大数据平台,web手撸代码的输入的页面,一个tab保存2M的数据非常普遍。而且也一个页面可以最多有10个tab,很多用户都喜欢一个浏览器开N个任务开发的页面。所以,sessionStorage和localStorage分分钟被虐哭。
因此,最安全的方案只能是indexDb。
不过indexDb也有缺陷。毕竟我们只希望缓存当前页面的数据,并不希望这个数据被长期保存。另外,indexDb是跟随在同一个域下。也就是同一个浏览器中,打开多个同一个网站的页签,所有页签共用一个indexDb的store。那么怎么实现页签级别的隔离呢?
下面介绍下我的解决方案 @rasir/page-storage
介绍
page-storage 使用 sessionStorage 和 indexDb 对当前页签的数据进行缓存,当页面关闭后缓存的数据失效。
工作原理
1、page-storage使用sessionStorage缓存当前窗口的uuid,当页面刷新时会读取sessionStorage中的uuid,如果存在则读取缓存数据,不存在则重新生成新的uuid。
2、page-storage 基于 indexDb 封装,使用简单,支持数据缓存,数据删除,数据更新,数据查询等操作,存储在indexDb中的key为每一个窗口的uuid,并且会将uuid的数据打上过期时间(expiredTime),默认 10s(可以自定义)。
3、page-storage使用web worker对indexDb中数据进行轮询续期,续期周期为过期时间(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.onStatusChange和chainPromiseCall.offStatusChange对实例状态进行监听 | 详见@rasir/chain-promise-call |
问题和解决方案
-
问题:当使用
window.open从一个页面打开一个新的页面时,sessionStorage中的数据会被复制到新的页面。这样会导致我们使用sessionWindowKey来作为每个页面独立存储的设计失效。 -
解决方案:
-
使用
window.name来给当前页面打标签,因为刷新页面时window.name不会重置。因此在构造函数中,我添加了windowName属性,用来判断当前页面是否是新开的。如果页面新开,并且不带windowName的特征码,就清除sessionStorage中的sessionWindowKey,并给window.name设置带特征码的name。当然,如果你不想我的window.name扰乱了你的设计,你可以设置noWindowName属性,这样你需要自己来保证sessionStorage的唯一性。 -
抛弃使用原生的
window.open函数。可以用<a>标签来进行跳转,或者用模拟<a>标签点击跳转,如下面这样:
let link = document.createElement("a");
link.href = "http://example.com";
link.target = "_blank";
link.click();
-
问题:如果是在 iframe 或者微前端这类沙箱隔离的场景下,
window.name可能无法被修改成功。 -
解决方案:这种情况下就需要你自己来保证
sessionStorage的唯一性。或者参看上文中的<a>标签方案。