专栏首页指缝阳光博客——使用 Redis 实现博客编辑的自动保存草稿功能

博客——使用 Redis 实现博客编辑的自动保存草稿功能

一、功能需求

介绍:

  1. 在做个人博客网站时。在我们编辑博客时,有可能会突然关闭浏览器或浏览器崩溃的情况,而此时我们的文章才写一半,还没进行保存。如果没有自动保存功能,则此时只能惟有泪千行了。因此需要一个自动保存文章为草稿的功能。
  2. 我在此处实现该功能的思路:在前端每隔 3 分钟调用一次自动保存草稿的接口,数据暂存在 Redis 数据库中(有效期设置为 1 天)。这样当我们意外关闭了页面,下次该用户写博客时会加载出之前草稿。

二、Springboot 中 Redis 设置

  1. 首先我们 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;
}
  1. 因为我们存储的是文章信息,所以肯定是一个对象,由此使用 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);
}
  1. 对于 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 实现草稿功能

  1. 此时我们只需要根据业务生成对应的 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}";
  1. 获取文章的实现此时就比较简单了,如下:
/**
 * 从 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);
} 
  1. 最后就是删除草稿,当我们成功提交文章后,就调用删除方法,对草稿进行删除,此处只贴出了具体的删除代码。
// 文章新增或修改成功,则将当前用户在 Redis 中的草稿进行删除
// 生成存储的 key
String key = MessageFormat.format(AUTO_SAVE_ARTICLE, currentUser.getId());
redisService.deleteArticle(key);
log.info("删除草稿文章 key=" + key + " 成功!");

四、前端对自动保存接口进行调用

  1. 此时后台接口已经准备好,我们需要做的就是前台每隔 3 分钟调用一次保存方法。我们也可以自己加一个手动保存的按钮。
// 每隔 3 分钟自动将数据存入草稿中,没提交时以防数据丢失, saveDraft() 是一个 ajax 方法
setInterval(function () { saveDraft() }, 3 * 60 * 1000);

五、总结

归纳: 到此,自动保存草稿的核心已经介绍完了。实现还是比较简单,同时也有其他的方法,比如使用 localStorage 等方法也可以实现。关键点就是在一个地方暂存文章。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一杯茶的时间,上手 Gatsby 搭建个人博客

    我的博客最初是用 Github Pages 默认的 Jekyll[2] 框架,其使用的 Liquid[3] 模板引擎在使用上有诸多不便。

    一只图雀
  • 算法排序之冒泡排序与插入排序

    这两种算法排序都稳定 时间复杂度为O(n*n) 由这两种排序引出的快速排序与希尔排序在时间复杂度上有较大改进 小编后期将会推出

    Max超
  • WordPress 4.9“Tipton”正式版已于11月14号正式发布

    说起博客开源程序,我想很多人都会想到wp,它是一种使用PHP语言开发的博客平台,用户可以在支持PHP和MySQL数据库的服务器上架设属于自己的网站,当然如果你的...

    李洋个人博客
  • Python + Selenium 自动发布文章(三):CSDN

      这是本系列的第三篇文章,主要介绍如何用Python+Selenium 自动发布CSDN博客,一些必要的条件在之前的文章里面已经提到过,这里也不再重复。

    happyJared
  • python接口自动化(三十四)-封装与调用--函数和参数化(详解)

      前面虽然实现了参数的关联,但是那种只是记流水账的完成功能,不便于维护,也没什么可读性,随着水平和技能的提升,再返回头去看前边写的代码,简直是惨不忍睹那样的代...

    北京-宏哥
  • 在 Ubuntu 14.04 服务器上部署 Hexo 博客

    本文将介绍如何在一台 Ubuntu 14.04 的 CVM 云服务器上快速部署 Hexo 博客站点,如何快速发布一篇博文并通过云服务器上的私有 Git 仓库部署...

    EarlGrey
  • markdown欢迎使用Markdown编辑器写博客

    仇诺伊
  • 如何使用-markdown编辑器

    本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦:

    菲宇
  • CSDN-markdown编辑器使用指南

    本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦:

    K同学啊
  • Markdown

    欢迎使用Markdown编辑器写博客 本Markdown编辑器使用StackEdit修改而来,用它写博客,将会带来全新的体验哦: Markdown和扩展Mark...

    用户1212940
  • 博客园文章编辑器5.0版本发布(markdown版)

    (后来我自己发现了一些问题,于是偷偷发了博客园文章编辑器的4.0.1版本,也没通知大家,不过好在有自动升级功能)

    liulun
  • Typecho安卓客户端Nabo

    无需任何插件,仅需博客开启XMLRPC。编辑器仅支持 markdown 这个android项目不开源,权那他保证绝不会私自盗取账号密码 开发者:权那他

    布衣者
  • python接口自动化(十六)--参数关联接口后传(详解)

      大家对前边的自动化新建任务之后,接着对这个新建任务操作了解之后,希望带小伙伴进一步巩固胜利的果实,夯实基础。因此再在沙场实例演练一下博客园的相关接口。我们用...

    北京-宏哥
  • Python + Selenium 自动发布文章(一):开源中国

      还是说说出这个系列的起因吧。之前写完或是修改了Markdown文章,我还分别需要在多个平台进行发布或是更新维护这些内容,这些平台目前包括我的博客、简书、开源...

    happyJared
  • WP-Sweep 插件清理 WordPress 垃圾评论和数据结构

    魏艾斯博客www.vpsss.net
  • 免费数学神器Mathpix发布移动版,一起来写更快的公式

    写博客、记笔记最麻烦的可能还不是文字,而是图表和公式,我们需要花些时间手写并嵌入数学公式。其实用 LaTeX 表达式写数学公式还是挺麻烦的,至少一般人做不到手写...

    机器之心
  • 自己动手用electron+vue开发博客园文章编辑器客户端【一】

    其实第一个版本已经很好了,不知足,后来自己又做了兼容markdown的,结果用来用去,发现不是自己想要的

    liulun
  • spring boot之从零开始开发自己的网站

    Janti
  • 做个开源博客学习Vite2 + Vue3 (三)博客设计和代码设计 功能设计代码设计model设计model代码状态设计

    项目搭建好了之后是不是可以编码了呢? 等等不要着急,我们是不是应该先设计一下?比如博客的功能等?

    用户1174620

扫码关注云+社区

领取腾讯云代金券