前端数据缓存 & 版本管理方案总结

前端数据缓存 & 版本管理方案总结

Write By CS逍遥剑仙

我的主页: csxiaoyao.com

GitHub: github.com/csxiaoyaojianxian

Email: sunjianfeng@csxiaoyao.com

QQ: 1724338257

1. 背景总览

越来越多的大型项目趋于 web 化,在浏览器中运行交互复杂的大型项目时,若每步交互都向后端提交 ajax 请求,除了增加服务器的负担外,等待相应的延迟也会降低用户体验。在前端项目中,使用 localstorage 进行数据缓存已是司空见惯的做法,但由于数据分布式地存储在多个前端浏览器中,因此数据的版本管理终究是绕不开的话题。

本文将从一个实际的 UI 编辑器项目出发,分析页面 json 数据的缓存及版本管理方案,大致思路如下:

2. 本地缓存存储选型

2.1 前端存储选型

目前,前端存储有以下几类:

  • cookie

在 H5 之前最主要的前端存储方式,大小限制 4K,且每次请求都会在请求头带上

  • localStorage

以键值对 (Key-Value) 形式永久存储,直至手动删除,一般限制 5M 大小

  • sessionStorage

与 localStorage 用法一致,区别在于 sessionStorage 在关闭页面后即被清空

  • application cache

通过配置 manifest 文件实现整个应用的离线缓存,即使没有网络也能打开

  • Web SQL

引入了一组使用 SQL 操作客户端数据库的 APIs,标准已废弃

  • IndexedDB

索引数据库是在浏览器中保存结构化数据的一种数据库,用于替换 WebSQL,使用 NoSQL 的形式来操作数据库,但并不常用

对于大型应用的待缓存数据而言,cookie 容量过小,sessionStorage 不支持持久保存,离线缓存针对的是文件而非数据,Web SQL 和 IndexedDB 针对的是结构化数据且不常用。因此,对于项目缓存的应用场景,localStorage 是不二之选。但 localStorage 本身并不支持设置有效期,直接使用可能无法满足业务场景需要,因此需要进行封装,以支持设置有效期。

2.2 localStorage 封装支持设置有效期

window.localStorage 封装到 Storage 类中,该类包含三个静态方法:setgetdel,而过期时间的实现是通过每次 set 时额外设置 expireKey 实现的。具体实现如下:

class Storage {
  /*
  * set 存储方法
  * @ param {String}     key 键
  * @ param {String}     value 值
  * @ param {String}     expired (可选)过期时间(min)
  */
  static set (key, value, expired = 0) {
    if (!window.localStorage) {
      return false
    }
    const data = JSON.stringify(value)
    const expireKey = `${key}__expires__`
    const expireTime = Date.now() + 1000 * 60 * expired
    try {
      window.localStorage.setItem(key, data)
      if (expired) {
        window.localStorage.setItem(expireKey, expireTime)
      }
      return true
    } catch (e) {
      if (e.name === 'QuotaExceededError') {
        window.localStorage.clear()
        window.localStorage.setItem(key, data)
        return true
      }
      return false
    }
  }
  /*
  * get 获取方法
  * @ param {String}     key 键
  */
  static get (key) {
    if (!window.localStorage) {
      return false
    }
    const now = Date.now()
    const expired = window.localStorage.getItem(`${key}__expires__`) || Date.now + 1
    if (now >= expired) {
      this.del(key)
      return false
    }
    const value = window.localStorage[key] ? JSON.parse(window.localStorage.getItem(key)) : window.localStorage.getItem(key)
    return value
  }
  /*
  * del 删除方法
  * @ param {String}     key 键
  */
  static del (key) {
    if (!window.localStorage) {
      return false
    }
    window.localStorage.removeItem(key)
    window.localStorage.removeItem(`${key}__expires__`)
    return true
  }
}

import Storage 后,就可以用过 Storage 的三个方法实现缓存的 CRUD 了。

3. 版本存取管理方案

3.1 定义缓存操作类

首先需要定义一个缓存操作类 UndoRedoHistory 用于对缓存数据进行存取操作,包含 3 个必备属性:_store_history_currentIndex

  • _history: 缓存队列,存储了 addState(state) 方法中传入的 state 数据实例
  • _currentIndex: 当前的缓存队列索引,通过修改索引,可以实现页面数据版本的 前进后退清空
  • _store: 由 UndoRedoHistory 传入的当前页面的数据操作实例,可将缓存队列中的 state 设置渲染到页面中

UndoRedoHistory 还包含 4 个基本操作的方法:addStateundoredoclear

  • addState: 将传入的 state 状态数据添加到缓存队列并操作索引,以实现数据的缓存添加操作
  • undo: 撤销操作,操作索引即可
  • redo: 重做操作,操作索引即可
  • clear: 清空缓存数据操作,清空队列并操作索引即可
class UndoRedoHistory {
  private _store: store // 当前页面数据操作实例,用于修改并渲染页面
  private _history: Array<object> // 缓存队列存储历史state
  private _currentIndex: number // 缓存队列索引
  constructor (store: store) {
    this._store = store
    this._history = []
    this._currentIndex = -1
  }
  /**
   * @name: addState
   * @desc: 记录操作
   */
  public addState (state: object): boolean {
    ...
  }
  // 撤销操作
  public undo (): boolean {
    ...
  }
  // 前进操作
  public redo (): boolean {
    ...
  }
  // 清空历史记录
  public clear (): boolean {
    ...
  }
}

3.2 数据监听

前面定义了缓存队列的操作类,但 addState 的执行还需要主动触发调用,以 UI 编辑器项目使用的 vue 框架来说,可以通过基于 vuex 的插件对页面数据的监听来实现 addState 的自动触发调用,其他支持监听的框架也是类似,即便是 jQuery 或是原生 JavaScript 也可以通过 发布订阅模式 实现自动调用。

// vuex 订阅操作,监听 mutation 的调用
store.subscribe((mutation, state) => {
  ...
  // 获取当前的 mutation
  const { type } = mutation
  // 筛选过滤等操作,只有支持的部分类型的 mutation 才会执行添加缓存记录操作
  ...
  // 通过前面定义的 UndoRedoHistory 类执行 addState 操作
  undoRedoInstance.addState(_.cloneDeep(state))
})

需要注意的是,vuex 支持命名空间,可以通过命名空间来区分参数是否需要监听,具体操作可以查询相关资料,下面是 vuex 命名空间的添加,如此处添加了名为 editor 的子空间。

// modules/editor.js
export default {
  namespaced: true, // 启用命名空间
  state,
  getters,
  actions,
  mutations
}

// index.js
import editor from './modules/editor'
export default new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  modules: {
    editor
  }
}

4. 前端版本选择策略

前面叙述了缓存数据的本地存储和存取方式,同一个页面的数据会存储为两份:

  • db 远程数据库
  • local 本地 localStorage 缓存

那么这两份数据应该如何取舍?在 UI 编辑器项目中,页面的 json 数据会有一个 time 字段标记数据的生成时间。页面加载时,会选择最新的数据用于加载。

关于 time 时间戳的获取:

数据的时间戳需要使用服务器时间,避免本地时间误差导致版本错乱 服务器时间戳的获取,可以使用页面初始化接口传入的时间戳与本地时间戳计算出时间差 diffTime,这样就可以每次获取服务器时间可以通过计算:服务器时间戳 = 本地时间戳 + diffTime

// 从接口获取 db 数据 jsonDataFromServer
// 从本地获取 local 数据 jsonDataFromCache
// ...
// 默认使用远程 db 中存储的数据(不存在时本地新建空数据)
let jsonData = jsonDataFromServer
// server / local 都存在时,选用最新的数据
if (jsonDataFromServer.time && jsonDataFromCache.time) {
  if (jsonDataFromServer.time > jsonDataFromCache.time) {
    jsonData = jsonDataFromServer
  } else {
    jsonData = jsonDataFromCache
  }
} else if (jsonDataFromCache.time) {
  // 若 db 为空,缓存存在,则使用 local 缓存数据
  jsonData = jsonDataFromCache
}

需要注意的是,在涉及到数据版本对比时,需要将与数据实际内容无关的字段删除,如这里的 time,UI 编辑器中的版本比对方法如下,返回 0 表示传入的两个数据相等,1 表示数据 1 更新,-1 表示数据 1 旧于数据 2。

public static compareData (data1: PageJson, data2: PageJson): number {
  const time1 = data1.time
  const time2 = data2.time
  let dataToCompare1 = cloneDeep(data1)
  let dataToCompare2 = cloneDeep(data2)
  // 去除版本数据无关属性,如数据更新的时间等
  const eleDelHandler = (pageData: PageJson) => {
    delete (pageData as any).time
    ...
  }
  eleDelHandler(dataToCompare1)
  eleDelHandler(dataToCompare2)
  if (JSON.stringify(dataToCompare1) === JSON.stringify(dataToCompare2)) {
    return 0
  } else if (time1 > 0 && time2 > 0 && time1 > time2) {
    return 1
  } else if (time1 > 0 && time2 > 0 && time1 < time2) {
    return -1
  }
  return -1000
}

5. 版本一致性校验保障

若后端 db 存储数据时不进行版本校验,当页面 1 和页面 2 都加载了版本1数据,若页面 1 执行保存更新后端数据为版本 2 后,页面 2 再执行保存时,由于版本 3 是基于版本 1 的修改,后端数据会丢失页面 1 中的修改。尤其是对于一些会涉及到后端操作的关联数据,会直接导致数据异常,这显然是不符合预期的。

01.png

在 UI 编辑器项目中,采取了一种简单高效的处理方式,通过给每个数据版本设置版本号,在后端 db 存储时进行判断,若 db 中已有的数据版本号与传入的数据版本号不一致,则拒绝更新,前端弹窗提醒,保障了版本的一致性。

02.png

用户若选择强制覆盖,则后端会跳过版本校验,强制更新数据,若选择刷新页面,则页面重载,当前页面更新为最新的远程数据。

03.png

6. 版本冲突提示优化

6.1 websocket 消息推送

虽然后端通过版本号校验拦截了冲突版本的保存,但体验并不好,因为版本冲突只有在提交保存后才会反馈给用户,若此时用户已在本地进行了大量修改,也只能被迫放弃。究其根源,是因为目前主流的 HTTP 协议是单工通信,不能在页面间建立联系。在 UI 编辑器项目中,通过建立 websocket 长连接,实现了页面与服务器的双工通信,关联了不同页面间的状态。

6.2 多用户同时操作

用户 1 打开了页面后用户 2 也打开此页面,此时,websocket 服务会向用户 1 的页面 推送锁定指令,锁定页面 1 为 只读状态,当用户 1 操作页面 1 时会消息提醒。

04.png

整体执行流程如下:

05.png

6.3 远程版本更新

上述多用户同时操作的场景,页面仅仅是浮窗消息提醒,但在远程版本更新的场景下,用户必须对本地数据版本进行处理,可以选择继续编辑,最后强制覆盖,也可以立即更新页面数据为远程最新版本。

06.png

整体执行流程如下:

07.png

7. 总结

本文总结了在 UI 编辑器项目的前端数据缓存和版本管理方案,能够实际地解决大型前端项目中的数据管理问题,若有更好的方案,欢迎留言交流。

sign

原文地址:https://cloud.tencent.com/developer/article/1824824

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340