2023-08-21 00:40:39
尽管先前已经给博客编写功能添加了导入导出功能,以防备断网时候博客编排内容无法提交的情况产生,还有一个问题需要解决——如何避免编写过程中因手滑页面退出导致的编写内容丢失。
这一块其实很容易就能想到采用云同步来实现。基本原理就是用户在前端富文本编辑器编辑,触发编辑相关事件后,就向服务器发送最新的编辑内容以更新。服务器端则可以将数据暂存在redis服务器中。用户需要同步时,再读取数据即可。
首先是控制器,一个保存接口一个读取接口。在保存前堆数据内容可以进行一个简单的校验。
@PostMapping("/cloud/save")
public RetResult<String> saveToCloud(String raw){
if (raw == null || "".equals(raw.trim())){
return RetResult.fail("数据为空,未同步");
}
return blogService.saveToCloud(raw);
}
@GetMapping("/cloud/load")
public RetResult<String> loadFromCloud(){
return blogService.loadFromCloud();
}
接着是service的实现。其实就是非常简单的redis存储与读取。不过需要注意的是要提前预测 RuntimeException 的产生,以及时的反馈给前端当前同步的状态。
@Override
public RetResult<String> saveToCloud(String raw) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
try{
ops.set(CLOUD_BLOG_SAVE, raw);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String time = sdf.format(Calendar.getInstance().getTime());
return RetResult.success("同步成功", time);
} catch (Exception e) {
return RetResult.fail("同步失败!" + e.getMessage());
}
}
@Override
public RetResult<String> loadFromCloud() {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
try{
String res = ops.get(CLOUD_BLOG_SAVE);
if (res == null || "".equals(res.trim())) return RetResult.fail("无数据内容!");
return RetResult.success("拉取数据成功", res);
} catch (Exception e) {
return RetResult.fail("拉取最新数据失败!" + e.getMessage());
}
}
相对来说,前端逻辑实现需要注意的点要多一些。
首先需要考虑的就是同步的频率选择,起初设想的是用定时器来定时发送请求。这种情况存在两种问题,其一,如果编写博客过程中离开,定时器的执行则是无意义的,因为内容一直没有变。其二,如果定时器的时间设置的太短,请求发送太频繁,占用网络资源,设置的太长的话,有时候才思文涌,一分钟几百字出来,在定时器执行前——吧嗒,隔壁小孩调皮的按了你电脑的Ctrl+F4,这时候正当你愤怒的想要呵斥时,看到了她水灵灵的眼镜瞅着你,嘟着小嘴,你是忍气吞声呢还是忍气吞声呢?
既然定时器不行,那么还有一种策略就是事件触发。可以参考腾讯文档,它的保存时机是每次有内容变更时就及时保存下来,当没有变动的时候也不会更新同步。这种触发式的同步策略就很有弹性,因此我采用的就是事件触发的方式存储的。
一般来说,一款经典的富文本编辑器都有编辑事件监听器,我使用的编辑器是wangeditor,在官网中有介绍可以采用 editor.config.onchange 设置编辑监听事件。
export default {
data () {
return {
saveCloud: {
status: 0,
describe: '未同步',
time: '',
step: 0
}
}
},
mounted () {
// cloud save
this.editor.config.onchange = this.saveToCloud
this.editor.config.onchangeTimeout = 500
},
methods: {
saveToCloud () {
if (this.saveCloud.step < 10) {
this.saveCloud.step++
return
}
axios.post('/api/blog/cloud/save', 'raw=' + this.editor.txt.html()).then(res => {
this.saveCloud.status = res.data.code
this.saveCloud.describe = res.data.msg
this.saveCloud.time = res.data.data
this.saveCloud.step = 0
}).catch(err => {
console.log(err)
this.saveCloud.status = -500
this.saveCloud.describe = '访问服务器异常'
this.saveCloud.step = 0
})
},
loadFromCloud () {
axios.get('/api/blog/cloud/load').then(res => {
if (res.data.code === 200) {
this.editor.txt.html(res.data.data)
} else {
alert(res.data.msg)
}
}).catch(err => {
console.log(err)
alert('连接服务器异常,请F12查看console')
})
}
},
beforeRouteLeave (to, from, next) {
this.saveCloud.step = 100
this.saveToCloud()
next()
}
}
比较关键的两个设计是step和beforeRouteLeave。
在数据上云时,固然可以使用和腾讯文档一样只要变更就上传,但是这需要增量上传才不会浪费网络资源,不过如何增量是个非常复杂的问题,需要后续深入研究各种情况(目前的想到的方式有两种,一种是类似git存储原理,一种是按照编辑事件增量,类似于redis的aof)。我这边则是折中了一下,就是当编辑事件触发了十次才推送一次更新,即step累计到10才执行,请求相应结束后值0。
至于beforeRouteLeave的设计是在有step的基础上才有必要存在的,因为有了10次step,有时候页面切换前,并没有达到step,这时候就需要在路由跳转前强行推送一次了。目前10次step存在一个明显的问题,就是浏览器异常退出且有更新未保存的时候,新增的几步编辑会丢失,不过影响应该不是很大吧()
以下就是结果图啦👻