页面缓存导致数据错误
服务端渲染(ssr)会将 API 数据做页面元素返回给前端, 而浏览器会认为是静态页面并将页面缓存, 导致每次访问时都是缓存用户数据. 页面又包含对数据的处理并提交.
当多端更新时由于可能不是以最新数据修改提交就会出现数据相互覆盖而出错.
问题背景
作为一名互联网的爱好者, 曾经收藏很多网址, 用过浏览器链接收藏功能, Google 账号登录不上(由于被墙), 导致链接无法同步. 多浏览器之间无法共享收藏, 也曾为转门
写一篇博客去分类记录, 但太不方便使用, 接着也放弃了. 上网多年的收藏渐渐的散了. 于是决心自己实现一个链接收藏管理功能.
需求设计
- 链接页管理页做为首页, 访问简单的域名便可 https://wxaxiaoyao.cn
- 提供系统默认链接和用户自定义链接, 用户自定义支持未登录使用, 链接支持分类
- 多端编辑自动同步合并(需登录)
- 页面服务端渲染, 提升用户体验
需求分析
需求1略过, 需求2未登录使用自定义功能, 可借助localStorage存储实现, 需求3多端编辑可将数据存于后端DB, 数据合并其实远程数据与本地缓存数据合并, 可借助更新时间
实现, 谁时间最新用谁的数据, 需求4前端框架使用nuxt解决
设计实现
数据结构设计
由于用户收藏的链接数量是有限的, 且链接数据大小不大, 需求目的侧重管理与检索. 故直接使用 mysql 的json字段存贮, 无需拆分多表(拆分多表反而是表记录剧增影响数据获取). 大致 json 格式如下:
{
classify: [ // 分类列表
{ // 分类项
name: "分类名",
links: [
{
text: "链接名称",
url:"链接地址",
tiemstamp: "更新时间戳"
}
]
}
]
}
合并数据
合并数据伪代码(由于本人英文水平较差, 变量命名可能比较糟糕, 努力改进中...), 直接复制的项目代码, 重在表达逻辑实现:
mergePageData() {
const indexPageData = this.indexPageData; // 缓存数据
const selfPageData = this.selfPageData; // 远程数据
if (selfPageData.userId == undefined) { // 未认证 远程数据为空
this.selfPageData = indexPageData; // 用缓存数据替换远程数据
this.setIndexPageData(this.selfPageData); // 缓存最新远程数据
return ;
};
// 缓存数据属于用户且与当前认证用户不同, 用最新的远程数据替换缓存数据并缓存
if (indexPageData.userId && indexPageData.userId != selfPageData.userId) return this.setIndexPageData(selfPageData);
// 缓存数据不属于用户或属于当前用户则合并数据 原则将本地最新数据更新至远程数据中
_.each(indexPageData.classify || [], o => {
const classify = _.find(selfPageData.classify || [], x => x.name == o.name); // 查找分类是否存在
if (!classify) return selfPageData.push(o); // 不存在直接添加
_.each(o.links || [], l => { // 存在合并链接
const link = _.find(classify.links || [], x => x.text == l.text); // 查找链接项
if (!link) return classify.links.push(l); // 不存在添加
if ((link.timestamp || 0) <= (l.timestamp || 0)) { 远程更新时间小于本地更新时间, 则更新链接项
link.timestamp = l.timestamp;
link.url = l.url;
}
});
});
this.setIndexPageData(selfPageData); // 刷新缓存
}
问题(bug)
回到本文开头, 由于后端渲染导致浏览器缓存页面数据, 从而缓存了用户数据, 导致页面增改删链接项都是基于旧的远程数据, 多端编辑提交时相互覆盖出错. 如何解决和规避此问题?
应该可以从如下两种的方法解决:
- 禁止浏览器缓存页面
设置禁止缓存页面的响应头, 此方法在页面经过多层代理时可能会很麻烦
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
- 前端渲染时动态拉取用户数据
若对于用户自定义链接数据的SSR需求不是很强烈, 可以牺牲下做前端动态拉取渲染. 很可以保留 SSR 但还是得加客户端动态拉取逻辑.
TODO
针对缓存问题是否存在其它解决方案? 数据结构设计是否合理?