一、功能需求
介绍:
- 在做个人博客网站时。在我们编辑博客时,有可能会突然关闭浏览器或浏览器崩溃的情况,而此时我们的文章才写一半,还没进行保存。如果没有自动保存功能,则此时只能惟有泪千行了。因此需要一个自动保存文章为草稿的功能。
- 我在此处实现该功能的思路:在前端每隔 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 接口:
RedisServiceImpl 实现类(因为文章参数类继承了文章类,因此反射获取属性的时候需要获取父类属性):/** * 保存文章 * * @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);
@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 和文章实体就可以进行草稿保存了。
其中 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("自动保存文章失败"); } }
/** * 文章自动保存时存储在 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 等方法也可以实现。关键点就是在一个地方暂存文章。
转载:https://blog.csdn.net/fanrendale/article/details/105376719
查看评论