博客——使用 Redis 实现博客编辑的自动保存草稿功能
时间:2022-07-25
本文章向大家介绍博客——使用 Redis 实现博客编辑的自动保存草稿功能,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
一、功能需求
介绍:
- 在做个人博客网站时。在我们编辑博客时,有可能会突然关闭浏览器或浏览器崩溃的情况,而此时我们的文章才写一半,还没进行保存。如果没有自动保存功能,则此时只能惟有泪千行了。因此需要一个自动保存文章为草稿的功能。
- 我在此处实现该功能的思路:在前端每隔 3 分钟调用一次自动保存草稿的接口,数据暂存在 Redis 数据库中(有效期设置为 1 天)。这样当我们意外关闭了页面,下次该用户写博客时会加载出之前草稿。
二、Springboot 中 Redis 设置
- 首先我们 Springboot 项目需要集成 Redis,具体集成方法我就不详述了(网上搜很多)。下面贴出我的 Redis 的序列化配置:
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 配置连接工厂
redisTemplate.setConnectionFactory(factory);
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值(默认使用 JDK 的序列化方式)
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY 是都有包括 private 和 public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非 final 修饰的,final修饰的类,比如 String,Integer 等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// 解决jackson2无法反序列化LocalDateTime的问题
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
jackson2JsonRedisSerializer.setObjectMapper(om);
// 使用 StringRedisSerializer 来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 值采用 json 序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// 设置 hash 的 key 和 value 序列化模式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
- 因为我们存储的是文章信息,所以肯定是一个对象,由此使用 Redis 的 Hash 类型来存储。我们使用 RedisTemplate 来操作,以下代码为对 Hash 类型数据进行操作的工具类 RedisUtil。
/**
* Hash 存储 map 实现多个键值保存并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String,Object> map, long time){
try {
redisTemplate.opsForHash().putAll(key, map);
if(time>0){
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object,Object> hmget(String key){
return redisTemplate.opsForHash().entries(key);
}
/**
* 删除hash表中的值
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item){
redisTemplate.opsForHash().delete(key,item);
}
- 对于 Redis 的业务操作,我提取出了 RedisService 。此处的操作主要是文章类的新增、获取和删除操作。提取出的方法如下:
RedisService 接口:
/**
* 保存文章
*
* @param key
* @param article 文章
* @param expireTime 过期时间
* @return
*/
boolean saveArticle(String key, ArticlePublishParam article, long expireTime);
/**
* 获取文章
*
* @param key
* @return
*/
ArticlePublishParam getArticle(String key);
/**
* 删除文章
*
* @param key
*/
void deleteArticle(String key);
RedisServiceImpl 实现类(因为文章参数类继承了文章类,因此反射获取属性的时候需要获取父类属性):
@Override
public boolean saveArticle(String key, ArticlePublishParam articlePublishParam, long expireTime) {
// 1. 首先将文章转为 map
BeanMap beanMap = BeanMap.create(articlePublishParam);
// 2. 保存到 redis
return redisUtil.hmset(key, beanMap, expireTime);
}
@Override
public ArticlePublishParam getArticle(String key) {
Map<Object, Object> map = redisUtil.hmget(key);
if (CollectionUtils.isEmpty(map)){
return null;
}else {
return JSON.parseObject(JSON.toJSONString(map), ArticlePublishParam.class);
}
}
@Override
public void deleteArticle(String key) {
// 1. 首先获取 Article 类的所有字段名称
List<String> fieldNameList = getFieldNameList(ArticlePublishParam.class);
// 2. 删除对应的对象 hash
redisUtil.hdel(key, fieldNameList.toArray());
}
/**
* 获取一个类的所有字段名称
* @param clazz
* @return
*/
private List<String> getFieldNameList(Class clazz) {
List<String> fieldNameList = new ArrayList<>();
// 1. 获取本类字段
Field[] filed = clazz.getDeclaredFields();
for(Field fd : filed) {
String filedName = fd.getName();
// 将序列化的属性排除
if (!"serialVersionUID".equals(filedName)) {
fieldNameList.add(filedName);
}
}
// 2. 获取父类字段
Class<?> superClazz = clazz.getSuperclass();
if (superClazz != null) {
Field[] superFields = superClazz.getDeclaredFields();
for (Field superField : superFields) {
String filedName = superField.getName();
// 将序列化的属性排除
if (!"serialVersionUID".equals(filedName)) {
fieldNameList.add(filedName);
}
}
}
return fieldNameList;
}
三、使用 RedisService 实现草稿功能
- 此时我们只需要根据业务生成对应的 key 和文章实体就可以进行草稿保存了。
/**
* 自动保存,编辑文章时每隔 3 分钟自动将数据保存到 Redis 中(以防数据丢失)
*
* @param param
* @param principal
* @return
*/
@PostMapping("/autoSave")
public ReturnResult autoSave(@RequestBody ArticlePublishParam param, Principal principal) {
if (Objects.isNull(param)) {
return ReturnResult.error("参数错误");
}
if (Objects.isNull(principal)) {
return ReturnResult.error("当前用户未登录");
}
// 1. 获取当前用户 ID
User currentUser = userService.findUserByUsername(principal.getName());
// 2. 生成存储的 key
String key = MessageFormat.format(AUTO_SAVE_ARTICLE, currentUser.getId());
// 3. 保存到 Redis 中, 过期时间为 1 天。此处是文章的参数类 ArticlePublishParam
boolean flag = redisService.saveArticle(key, param, 24L * 60 * 60 * 1000);
if (flag) {
log.info("保存 key=" + key + " 的编辑内容文章到 Redis 中成功!");
return ReturnResult.success();
} else {
return ReturnResult.error("自动保存文章失败");
}
}
其中 key 的生成使用的格式如下:
/**
* 文章自动保存时存储在 Redis 中的 key ,后面 {0} 是用户 ID
*/
String AUTO_SAVE_ARTICLE = "auto_save_article::{0}";
- 获取文章的实现此时就比较简单了,如下:
/**
* 从 Redis 中获取当前登录用户的草稿文章
*
* @param principal
* @return
*/
@GetMapping("/getAutoSaveArticle")
public ReturnResult getAutoSaveArticle(Principal principal) {
if (Objects.isNull(principal)) {
return ReturnResult.error("当前用户未登录");
}
// 1. 获取当前用户 ID
User currentUser = userService.findUserByUsername(principal.getName());
// 2. 生成存储的 key
String key = MessageFormat.format(AUTO_SAVE_ARTICLE, currentUser.getId());
// 3. 获取文章信息
ArticlePublishParam article = redisService.getArticle(key);
if (article != null && StringUtils.isNotBlank(article.getTagsStr())){
String[] split = article.getTagsStr().split(",");
article.setTagStringList(Arrays.asList(split));
}
log.info("获取草稿文章 key=" + key + " 的内容为:" + article);
return ReturnResult.success(article);
}
- 最后就是删除草稿,当我们成功提交文章后,就调用删除方法,对草稿进行删除,此处只贴出了具体的删除代码。
// 文章新增或修改成功,则将当前用户在 Redis 中的草稿进行删除
// 生成存储的 key
String key = MessageFormat.format(AUTO_SAVE_ARTICLE, currentUser.getId());
redisService.deleteArticle(key);
log.info("删除草稿文章 key=" + key + " 成功!");
四、前端对自动保存接口进行调用
- 此时后台接口已经准备好,我们需要做的就是前台每隔 3 分钟调用一次保存方法。我们也可以自己加一个手动保存的按钮。
// 每隔 3 分钟自动将数据存入草稿中,没提交时以防数据丢失, saveDraft() 是一个 ajax 方法
setInterval(function () { saveDraft() }, 3 * 60 * 1000);
五、总结
归纳: 到此,自动保存草稿的核心已经介绍完了。实现还是比较简单,同时也有其他的方法,比如使用 localStorage 等方法也可以实现。关键点就是在一个地方暂存文章。
- JAVA增删改查XML文件
- javascript去掉字符串前后空格
- 构建通用的 React 和 Node 应用
- 有趣的 CSS 像素艺术
- 【插件开发】—— 11 窃听风云(Java事件监听原理-GEF实例讲解)
- 【面试虐菜】—— JAVA面试题(2)
- JavaMelody监控spring、struts
- 《JavaScript语言精粹》—— 读书总结
- 视差滚动技术的简介及运用
- 【面试虐菜】—— JAVA面试题(3)
- Carousel 旋转画廊特效的疑难杂症
- 区块链为IBM与Visa等老牌公司注入新的生机
- 【java.lang.UnsupportedClassVersionError】版本不一致出错
- Yeoman 官网教学案例:使用 Yeoman 构建 WebApp
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法