Vue + Spring Boot 项目实战(十八):博客功能开发 - Go语言中文社区

Vue + Spring Boot 项目实战(十八):博客功能开发


logo


重要链接:
「系列文章目录」

「项目源码(GitHub)」

前言

各位朋友们你们好哇,隐约感觉我已经鸽了好几周了,所以虽然快过年了,我还是决定肝出一篇文章出来。前几篇文章发布后好多同学表示看不懂了,但我觉得都到第十八篇了,有些问题确实应该让你们自己尝试解决一下。不过放心,这篇文章是特地写给你们找自信的。

博客可以说是技术人的标配了,有个自己运营的博客网站更是可以在小白面前装一波逼。网上有很多开源的博客系统,如 WordPress、Hexo 等,功能强大页面美观运行稳定,但毕竟不是自己做的。一开始我也没想在这个项目里加这个模块,后来做着做着越来越像 CMS,这就好比你和朋友出去玩他想上厕所你陪他到了门口虽然你并没有什么库存但是来都来了放空一下也不是不可以,所以我就花了几个晚上把这个功能弄了一下。如果你们有兴趣真的可以搞个服务器尝试当博客运营一下,能学到很多东西。

这篇文章除了讲解如何搭出一个博客系统,还设计到如下知识点:

  • 如何使用开源编辑器?
  • Vue 如何在不同页面传递参数?
  • Spring Data JPA 分页功能如何使用?

一、mavon-editor 编辑器

其实项目一开始的时候我就暗搓搓地安装了这个编辑器,所以如果你们复制过我的 package.json 文件或者直接从 github 上下的源码,就不用再安装了。

目前常见的文本编辑器有两种,即富文本编辑器和 markdown 编辑器,我一直写作用的都是 markdown,基本不用动鼠标,而且在各个平台样式比较统一,方便迁移。一开始可能觉得语法麻烦,其实用的多的就那么几个,写几篇就熟悉了。

简单介绍一下,这个 mavon-editor 编辑器应该是最火的国产开源 markdown 编辑器里最火的一个,github 3.4k star。功能比较齐全,界面比较舒服,作者也很热情地解决使用者的问题,我用了一下,暂时没发现什么 BUG。仓库地址:

https://github.com/hinesboy/mavonEditor

readme 提供了 API 文档。

markdown 编辑器的本质是把你的输入源(按一定语法规则组织的文本)转换为 html 代码,以在浏览器上生动形象地展示,这个过程其实类似于「翻译」或者说「编译」。同时作为一个成熟的应用,又需要一些附加的按钮、快捷键等功能,但其实 markdown 本身就是为了简化功能的使用,类似加粗、斜体等操作都有相对应的语法,完全可以直接键入,不必要过分使用快捷键或按钮。

二、功能设计

博客功能大概可以分为三个部分,分别是文章展示文章管理编辑器,文章展示又可以划分为文章列表与文章详情两个部分。
在这里插入图片描述
虽然编辑器提供预览功能,但一般我们在前台只不需要向用户展示 markdown 原文,所以最好还是单独编写一个文章详情页面渲染出 html。有两种思路:

  • 第一种,在数据库中仅保存 markdown 语法的文本,在需要使用时解析为 html,并在前台渲染
  • 第二种,markdown、html 均保存在数据库中,需要使用时取出 html 并在前台渲染

第一种的好处就是节省传输的数据量与数据库空间,坏处就是需要自己编写解析方法,相当于又重写了一遍编辑器,而且难以保证解析出来的样式跟原编辑器一致(用一些公开的解析函数也存在这个问题)。

当然,如果编辑器提供了解析的 API 那就比较舒服了,但我暂时没找到相关的内容。作者可能并不想这么做,而是提供了一个可以传递 markdown 与 html 值的 save 方法,因此就这个编辑器而言,我觉得选择第二种方法方便一些。

下面是各个页面的初步设计与功能介绍:

文章列表:

展示文章的题目、摘要、封面等信息,提供文章详情页面入口。主要是前端设计与分页功能实现,后期可以扩展分类标签、检索、归档等功能,还可以在侧边栏加入作者简介等信息。
笔记本
文章详情:

这个页面用于展示文章的具体内容,也就是渲染从数据库中取出的 html。打码的部分说明了我是一个遵守平台规则的老实人。
文章详情
文章管理:

后台的管理页面,提供查看、发布、修改文章的入口以及删除功能,需要内容管理权限。
文章管理
编辑器:

核心页面,在开源编辑器的基础上,添加了标题、摘要及封面设置功能。
编辑器

三、功能实现

1.数据库设计

为了保存文章相关的信息,设计 jotter_article 表如下:
文章表
目前包含的字段是 id、标题、文章 html、md 原文、文章摘要、文章标题和发文日期。

2.编辑器的引入与改造

如果之前没有安装过编辑器,可以先在项目 wj-vue 根目录下执行如下命令:

$ npm install mavon-editor --save

再在 main.js 里全局注册一下:

import mavonEditor from 'mavon-editor'
...
...
Vue.use(mavonEditor)

即可在组件中使用。考虑到编辑功能应该向具有内容管理权限的用户使用,我们在 components/admin/content 文件夹下新建一个组件,命名为 ArticleEditor.vue。该组件的主体就是 mavon-editor 编辑器,最初的状态如下:

<template>
  <mavon-editor
    v-model="article.articleContentMd"
    style="height: 100%;"
    ref=md
    @save="saveArticles"
    fontSize="16px">
  </mavon-editor>
</template>

<script>
  export default {
    name: 'Editor',
    data () {
      return {
        article: {}
      }
    }
</script>

接下来我们需要对它做一些邪恶的事情,把它改造成我们想要的样子。改造工序是:

  • 第一步,添加标题输入栏
  • 第二步,插入自定义工具,提供摘要与封面录入功能
  • 第三步,编写 save 方法,与后端交互

实现标题的输入只需要添加一个 <el-input>

      <el-input
        v-model="article.articleTitle"
        style="margin: 10px 0px;font-size: 18px;"
        placeholder="请输入标题"></el-input>

插入自定义工具,文档中并没有相关内容,但是我寻思肯定有人会问,就搜了一下 issues,果然
插槽
作者在 2018 年 8 月份就这个问题提交了一次更新。当然其实也可以直接看源码,里面设置了 4 个插槽,对应不同的插入位置。
插槽
为了保证我们插入的图标跟原来的图标样式一致,需要再瞅一眼 tool-bar 的源码。里面的按钮大概是这样写的

<button type="button"
		v-if="toolbars.save"
		@click="$clicks('save')"
		class="op-icon fa fa-mavon-floppy-o"
        aria-hidden="true"
        :title="`${d_words.tl_save} (ctrl+s)`"></button>

我们照葫芦画瓢设置一下 type、class 和 title 属性,弄一个添加摘要和封面的按钮:

<button type="button"
		class="op-icon el-icon-document"
		:title="'摘要/封面'"
		slot="left-toolbar-after"
		@click="dialogVisible = true"></button>

其实样式主要是 class 里的 op-icon 控制的。

添加摘要和封面的表单被我做在弹出框里了。上传文章封面复用之前上传图书封面的组件。

<el-dialog
  :visible.sync="dialogVisible"
  width="30%">
  <el-divider content-position="left">摘要</el-divider>
  <el-input
    type="textarea"
    v-model="article.articleAbstract"
    rows="6"
    maxlength="255"
    show-word-limit></el-input>
  <el-divider content-position="left">封面</el-divider>
  <div style="margin-top: 20px">
    <el-input v-model="article.articleCover" autocomplete="off" placeholder="图片 URL"></el-input>
    <img-upload @onUpload="uploadImg" ref="imgUpload" style="margin-top: 5px"></img-upload>
  </div>
  <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
  </span>
</el-dialog>

界面是这个样子:
封面、摘要
然后我惊奇地发现图片上传不了,好像之前也有同学反映过这个问题,我没当回事。后来折腾半天,发现是图片上传这个组件比较狗,需要单独设置属性才能带上 cookie,不带 cookie 后端就拿不到 sessionId,就会被 shiro 拦截。修改后的组件模板部分如下:

<template>
  <el-upload
    class="img-upload"
    ref="upload"
    action="http://localhost:8443/api/admin/content/books/covers"
    with-credentials
    :on-preview="handlePreview"
    :on-remove="handleRemove"
    :before-remove="beforeRemove"
    :on-success="handleSuccess"
    multiple
    :limit="1"
    :on-exceed="handleExceed"
    :file-list="fileList">
    <el-button size="small" type="primary">点击上传</el-button>
    <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
  </el-upload>
</template>

相比之前添加了 with-credentials

最后一步编写保存方法,常规操作,向后端发送数据即可。

组件完整的代码如下:

<template>
  <div>
    <el-row style="margin: 18px 0px 0px 18px ">
      <el-breadcrumb separator-class="el-icon-arrow-right">
        <el-breadcrumb-item :to="{ path: '/admin/dashboard'}">管理中心</el-breadcrumb-item>
        <el-breadcrumb-item :to="{ path: '/admin/content/book'}">内容管理</el-breadcrumb-item>
        <el-breadcrumb-item :to="{ path: '/admin/content/article'}">文章管理</el-breadcrumb-item>
        <el-breadcrumb-item>编辑器</el-breadcrumb-item>
      </el-breadcrumb>
    </el-row>
    <el-row>
      <el-input
        v-model="article.articleTitle"
        style="margin: 10px 0px;font-size: 18px;"
        placeholder="请输入标题"></el-input>
    </el-row>
    <el-row style="height: calc(100vh - 140px);">
      <mavon-editor
        v-model="article.articleContentMd"
        style="height: 100%;"
        ref=md
        @save="saveArticles"
        fontSize="16px">
        <button type="button" class="op-icon el-icon-document" :title="'摘要/封面'" slot="left-toolbar-after"
                @click="dialogVisible = true"></button>
      </mavon-editor>
      <el-dialog
        :visible.sync="dialogVisible"
        width="30%">
        <el-divider content-position="left">摘要</el-divider>
        <el-input
          type="textarea"
          v-model="article.articleAbstract"
          rows="6"
          maxlength="255"
          show-word-limit></el-input>
        <el-divider content-position="left">封面</el-divider>
        <div style="margin-top: 20px">
          <el-input v-model="article.articleCover" autocomplete="off" placeholder="图片 URL"></el-input>
          <img-upload @onUpload="uploadImg" ref="imgUpload" style="margin-top: 5px"></img-upload>
        </div>
        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
        </span>
      </el-dialog>
    </el-row>
  </div>
</template>

<script>
  import ImgUpload from './ImgUpload'

  export default {
    name: 'Editor',
    components: {ImgUpload},
    data () {
      return {
        article: {},
        dialogVisible: false
      }
    },
    mounted () {
      if (this.$route.params.article) {
        this.article = this.$route.params.article
      }
    },
    methods: {
      saveArticles (value, render) {
        // value 是 md,render 是 html
        this.$confirm('是否保存并发布文章?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
            this.$axios
              .post('/admin/content/article', {
                id: this.article.id,
                articleTitle: this.article.articleTitle,
                articleContentMd: value,
                articleContentHtml: render,
                articleAbstract: this.article.articleAbstract,
                articleCover: this.article.articleCover,
                articleDate: this.article.articleDate
              }).then(resp => {
              if (resp && resp.status === 200) {
                this.$message({
                  type: 'info',
                  message: '已保存成功'
                })
              }
            })
          }
        ).catch(() => {
          this.$message({
            type: 'info',
            message: '已取消发布'
          })
        })
      },
      uploadImg () {
        this.article.articleCover = this.$refs.imgUpload.url
      }
    }
  }
</script>

后端部分首先是 pojo、DAO、service 一套,目前没什么可说的,不想自己写可以参考源码:

https://github.com/Antabot/White-Jotter/tree/master/wj/src/main/java/com/gm/wj

controller 中保存对应的方法如下:

 @PostMapping("api/admin/content/article")
 public Result saveArticle(@RequestBody JotterArticle article) {
     jotterArticleService.addOrUpdate(article);
     return ResultFactory.buildSuccessResult("保存成功");
 }

前端路由写法参考:

 {
   path: '/admin/content/editor',
   name: 'Editor',
   component: Editor,
   meta: {
     requireAuth: true
   }
 }

由于编辑器不在后台管理目录中,所以不用设置动态加载,虽然前端非要访问也能访问,但是反正没有写的权限,所以无所谓了。

3.文章列表页面

这个页面主要涉及到分页的问题。之前我们图书馆页面的分页是纯靠前端进行的,这里我们用后端来实现一下。

Spring Data 提供了 org.springframework.data.domain.Page 类,该类包含了页码、页面尺寸等信息,可以很方便地实现分页。我们要做的,就是编写一个传入页码与页面尺寸参数的方法,这个方法可以写在 service 层。

public Page list(int page, int size) {
    Sort sort = new Sort(Sort.Direction.DESC, "id");
    return  jotterArticleDAO.findAll(PageRequest.of(page, size, sort));
}

这里我们构造了一个 PageRequest 类来配合查询,sort 参数是可选的,如果报错了可能是版本问题,较新的版本里取消了公共构造方法,而是用静态工厂方法代替。将语句替换为

Sort sort = Sort.by(Sort.Direction.DESC, "id")

即可。接下来编写 controller 对应方法:

@GetMapping("/api/article/{size}/{page}")
public Page listArticles(@PathVariable("size") int size, @PathVariable("page") int page) {
    return jotterArticleService.list(page - 1, size);
}

这里 page 是 1 的话其实会查询到第二页的内容,而前端组件传入的参数就是当页页码,所以需要 - 1。随便输个参数测试一下,可以看到后端查询出来的数据结构如下:

{"content":[{"id":1,"articleTitle":"凉风有兴","articleContentHtml":
                        
                        
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/neuf_soleil/article/details/104033436
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢