Write By CS逍遥剑仙
我的主页: csxiaoyao.com
GitHub: github.com/csxiaoyaojianxian
Email: sunjianfeng@csxiaoyao.com
QQ: 1724338257
越来越多的大型项目趋于 web 化,在浏览器中运行交互复杂的大型项目时,若每步交互都向后端提交 ajax 请求,除了增加服务器的负担外,等待相应的延迟也会降低用户体验。在前端项目中,使用 localstorage 进行数据缓存已是司空见惯的做法,但由于数据分布式地存储在多个前端浏览器中,因此数据的版本管理终究是绕不开的话题。
本文将从一个实际的 UI 编辑器项目出发,分析页面 json 数据的缓存及版本管理方案,大致思路如下:
目前,前端存储有以下几类:
在 H5 之前最主要的前端存储方式,大小限制 4K,且每次请求都会在请求头带上
以键值对 (Key-Value) 形式永久存储,直至手动删除,一般限制 5M 大小
与 localStorage 用法一致,区别在于 sessionStorage 在关闭页面后即被清空
通过配置 manifest 文件实现整个应用的离线缓存,即使没有网络也能打开
引入了一组使用 SQL 操作客户端数据库的 APIs,标准已废弃
索引数据库是在浏览器中保存结构化数据的一种数据库,用于替换 WebSQL,使用 NoSQL 的形式来操作数据库,但并不常用
对于大型应用的待缓存数据而言,cookie 容量过小,sessionStorage 不支持持久保存,离线缓存针对的是文件而非数据,Web SQL 和 IndexedDB 针对的是结构化数据且不常用。因此,对于项目缓存的应用场景,localStorage 是不二之选。但 localStorage 本身并不支持设置有效期,直接使用可能无法满足业务场景需要,因此需要进行封装,以支持设置有效期。
将 window.localStorage
封装到 Storage 类中,该类包含三个静态方法:set
、get
、del
,而过期时间的实现是通过每次 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 了。
首先需要定义一个缓存操作类 UndoRedoHistory
用于对缓存数据进行存取操作,包含 3 个必备属性:_store
、_history
、_currentIndex
。
addState(state)
方法中传入的 state 数据实例UndoRedoHistory
传入的当前页面的数据操作实例,可将缓存队列中的 state 设置渲染到页面中UndoRedoHistory
还包含 4 个基本操作的方法:addState
、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 { ... } }
前面定义了缓存队列的操作类,但 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 } }
前面叙述了缓存数据的本地存储和存取方式,同一个页面的数据会存储为两份:
那么这两份数据应该如何取舍?在 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 }
若后端 db 存储数据时不进行版本校验,当页面 1 和页面 2 都加载了版本1数据,若页面 1 执行保存更新后端数据为版本 2 后,页面 2 再执行保存时,由于版本 3 是基于版本 1 的修改,后端数据会丢失页面 1 中的修改。尤其是对于一些会涉及到后端操作的关联数据,会直接导致数据异常,这显然是不符合预期的。
在 UI 编辑器项目中,采取了一种简单高效的处理方式,通过给每个数据版本设置版本号,在后端 db 存储时进行判断,若 db 中已有的数据版本号与传入的数据版本号不一致,则拒绝更新,前端弹窗提醒,保障了版本的一致性。
用户若选择强制覆盖,则后端会跳过版本校验,强制更新数据,若选择刷新页面,则页面重载,当前页面更新为最新的远程数据。
虽然后端通过版本号校验拦截了冲突版本的保存,但体验并不好,因为版本冲突只有在提交保存后才会反馈给用户,若此时用户已在本地进行了大量修改,也只能被迫放弃。究其根源,是因为目前主流的 HTTP 协议是单工通信,不能在页面间建立联系。在 UI 编辑器项目中,通过建立 websocket 长连接,实现了页面与服务器的双工通信,关联了不同页面间的状态。
用户 1 打开了页面后用户 2 也打开此页面,此时,websocket 服务会向用户 1 的页面 推送锁定指令,锁定页面 1 为 只读状态,当用户 1 操作页面 1 时会消息提醒。
整体执行流程如下:
上述多用户同时操作的场景,页面仅仅是浮窗消息提醒,但在远程版本更新的场景下,用户必须对本地数据版本进行处理,可以选择继续编辑,最后强制覆盖,也可以立即更新页面数据为远程最新版本。
整体执行流程如下:
本文总结了在 UI 编辑器项目的前端数据缓存和版本管理方案,能够实际地解决大型前端项目中的数据管理问题,若有更好的方案,欢迎留言交流。
对当下企业而言,所有行业的边界在消失,颠覆性的创新不断涌现在动荡、不确定、复...
怎样在 云服务器 安装ftp?在 云服务器 中也可以利用FTP进行文件传输,首先需要...
近年来,我国数据要素市场加速扩容,数据要素应用场景衍生拓展,数据要素在增加...
一、前言 在嵌入式项目开发中,字符串格式化是很常见的操作,我们一般都会使用 C...
本文转载自微信公众号「小姐姐味道」,作者小姐姐养的狗02号。转载本文请联系小...
《 终于有人把数据中台讲明白了 》一文讲到数据中台的定义和价值,本文将介绍数...
这是一篇基础文章,主要帮新手解决 GOPATH 和 Go Module 的问题。希望这篇文章能...
公司介绍 我们公司是福建优享生活信息科技有限公司,从事软件和信息技术服务业、...
一、设备与资料 HT-M00L单通道网关,在ESP32 + SX1278 LoRa节点芯片的硬件基础上...
云数据库怎么买便宜?一是货比三家,对比了价格买会更便宜;二是直接等活动,如...