整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Django学习与实战(六):文章评论

程派微信号:codingpy

本文系作者 Django学习小组 授权编程派原创发表,并经编程派编辑,转载请注明出处及微信ID(codingpy)。

通过前五周的时间我们开发了一个简单的个人 Blog,本周我们将实现 blog 文章的评论功能。(快速查看前五期内容,请点击文末链接)

实现思路

首先需要为评论(Comment)设计一个数据库表,并编写相应的 Model,将评论与文章关联,再编写发表评论的视图,设置相应的 url 即可。

评论的 Model 设计

请点击阅读原文,查看详细代码

参照大部分博客评论的样式,我们的 BlogComment Model 包含这些字段:

  • user_name:用户在评论前先要填写他们想使用的昵称

  • user_email:用户在评论前先要填写他们想使用的邮箱

  • body:用户提交的评论内容

  • created_time:评论提交时间

  • article:评论关联的文章,因为一个评论只能关联某一篇文章,而一篇文章下可能有多个评论,因此是一对多的关系,使用 ForeignKey

评论的表单

表单用来给服务器后台提交用户填写的数据,例如平时我们看到的填写登录、注册信息的页面就是一个登录、注册表单,用户填写表单信息后,点击提交按钮,表单中填写的内容就会打包发送给服务器后台。

我们需要为用户填写评论设置一个表单,django 的 form 模块为我们提供了自动生成表单的功能,如果对表单不熟悉请参阅:官方文档:表单概述 ,以了解基本的表单使用方法(如果你对表单感觉很陌生的话)。

下面我们使用 Django 的 ModelForm ( django ModelForm 介绍 )类为我们自动生成表单。首先在 blog 目录下新建一个 forms.py (和 models.py 同一目录)文件用来存放 form 的代码:

请点击阅读原文,查看详细代码

视图函数

这里我们一如既往坚持使用基于类的通用视图,由于涉及到评论表单的提交处理,因此我们使用 FormView。这里对 FormView 的使用稍作讲解。

在 Django 的基于函数的视图中,涉及表单的处理的视图其逻辑一般是这样的:

请点击阅读原文,查看详细代码

即,首先判断用户是否通过表单 POST 了数据过来,如果是,则根据 POST 过来的数据构建一个表单,如果数据验证合法(form.is_valid),则创建评论,否则返回表单提交页。如果没有 POST 数据,则做其他相应的事情。FormView 把这些逻辑做了整合,无需写那么多 if else 语句:

请点击阅读原文,查看详细代码

为了方便地重定向回原来提交评论的文章详情页面,我们为文章(Article)的模型新增一个方法:get_absolute_url,调用该方法将得到该 Article 对应的 url,例如这是文章 1 的 url:http://localhost:8000/article/1,则调用后返回 /article/1,这样调用 HttpResponseRedirect 后将返回该 url 下的文章详情页。

请点击阅读原文,查看详细代码

同时为了在详情页渲染一个评论表单,稍微修改一下 ArticleDetailView 的视图函数,把评论表单 form 插入模板上下文中:

请点击阅读原文,查看详细代码

URL 设置

设置模板文件

新增了一个 comment.html 文件以渲染评论表单和评论列表,并且修改了 detail.html 文件以在文章详情页显示评论表单和评论列表,修改了blog/tatic 下的 style.css 为评论添加样式,由于代码比较多,就不贴出来了,主要是 html 和 css 的前端相关代码,请到 GitHub 仓库 更新相关的模板和静态资源文件。

至此,整个评论功能的框架做好了,显示效果如下:

当然这只是一个评论的框架,很多细节有待处理和完善,但无论如何,用户已经可以为我们的文章发表评论意见了。

前情回顾

第一周:Django学习与实战(一):编写博客的 Model 和首页面

第二周:Django学习与实战(二):博客详情页面和分类页面

第三周:Django学习与实战(三):文章列表分页和代码语法高亮

第四周:Django学习与实战(四):基于类的通用视图详解

第五周:Django学习与实战(五):标签云与文章归档

于怎样实现楼中楼的评论系统具体操作

1. 实现前的思考

在经历过多说和网易云跟帖后,总算是下定决心自己要写一个评论系统了。

我们在使用的很多评论系统中,目前比较流行的就是楼中楼的方式了,比如百度贴吧,wordpress等等。在这以前,一般都是按照时间顺序进行1楼、2楼、3楼的展示,如果要回复某个人,使用@符号标识出这个用户的名字,然后回复内容。可是这样存在一个很大的问题,讨论问题没有集中在一起,其他用户根本不知道你们在讨论什么,原作者在1楼发表评论,你进来回复这个用户的评论时,已经到10楼了,原作者再回复你又到20楼了。其他用户看到10楼时,早已经忘记原作者说了什么了。

百度贴吧在改版之前就是这种方式,后来在新版中启用了楼中楼的方式,这种方式,关于某个话题的讨论就能集中在一块了。

同时,知乎也对他的评论系统进行了一次改版,不过不是改版成楼中楼,而是在每个有对话评论的后面加上一个弹窗链接查看对话,点击链接后弹窗能看到这两个人之间互动的所有评论。

采用时间顺序倒序或者正序平铺的方式展示评论,这种方式实现起来简单,但是阅读困难;采用楼中楼的方式展示评论,对用户的阅读习惯比较友好,但是实现起来可能比较困难。不过,最后仍然决定采用楼中楼的方式来,虽然本人博客的评论量也少的可怜,不过还是决定要实现一下。


2. 数据表的设计

先说下前后端使用的语言和框架,前端考虑到页面渲染和比较多的事件调用,使用了vue框架,vue应该说不是最好的选择,毕竟对一个评论的前端部门来说,可能有点大材小用,不过为了快速开发,也就选择了vue。后端使用的是php语言,数据库使用的是mysql。

数据库表的设计,既要考虑到可以导入以前的数据,又能方便以后添加新的评论。这里我创建了3个表: 文章表,用户表,评论表。

在网易云跟帖关闭之前,我把自己的数据导出来了(多说的数据已经丢失,不知道导出的格式是什么了),我们来看下网易云跟帖里导出数据的格式:

从上面的数据里,可以看到,每个文章都有标题,url和评论,评论的每一项都有自己对应的id,其回复的评论pid,内容content,该评论的用户userid, nickname和avatar。我这里也就只摘取主要的信息录入到数据库中。

2.1 用户表

用户表相对来说比较简单,要考虑的就是原有的userid也要作为字段进行保存,方便在导入评论数据时能找到对应的用户,在评论数据也导入完成后即可将该字段删除,以后新添加的注册用户用不到这个字段了。用户表的设计:

字段类型说明
idint自增,主键
widint用户原有的userid
nicknamevarchar(50)昵称
avatarvarchar(100)头像
statusint状态

设计好用户表后,将原数据中所有的用户都单独拿出来,然后使用userid作为key存储到一个数组中,这样也能起到一个去重的效果。把拿到的所有的用户数据存储到用户表

2.2 评论表

在设计评论表,主要考虑如下的因素:

  1. 评论必须依托于文章和用户才能存在,因此评论的外键是文章标识和userid,留言板是一个文章内容为空的评论形式;

  2. 我想以后新的评论能使用自增id,而不是跟随原有评论的cid来产生新的评论id,因此这次评论表的主键是id,原有的评论id只作为其中的一个字段wid来构造楼中楼的关系,这些旧评论插入到数据表时都会有新的评论id;

  3. 楼中楼的评论是处在某个评论下的,同时,楼中楼里还有相互之前的互动回复。因此这个评论的pid(parentid)表示当前评论处于哪个评论之下,同时replyid表示是回复的哪个评论;若直接回复的父级评论,则pid与replyid相同,都是父级评论的id,若回复的不是父级评论,则pid为父级评论的id,replyid为回复评论的id;pid或replyid为0时,则表示直接对文章发表评论。

因此我们的评论表是这样设计的:

字段类型说明
idint自增,主键
widint评论原有的主键cid
uidint用户id
replyidint该评论回复的评论id,没有则为0
pidint该评论所在的父级id,没有则为0
aidvarchar(100)文章的标识
contentvarchar(300)评论内容
createtimeint评论时间的时间戳

表中的aid(文章的标识)可以是文章的url,文章的id或者其他任何能够唯一识别该文章的东东都可以。这里我们使用的是文章的uri来作为唯一标识,比如上面数据中的文章,我们使用/node/2017/02/20/node-express-forum.html来标识文章。其他文章同理。

将这些评论写入表时,我们还要注意的是,原数据中,每个评论都对应着一个用户,在我设计的系统里,用户与评论分来了,只使用uid来进行关联。新的用户与新的评论都是使用自有的自增主键,因此在原有评论进行入库时,需要将原来的userid转换为新用户表中的主键id,新旧数据进行统一。

文章表不做解释。


3. 具体实现

前端部分主要是负责展示每个文章的评论,同时让登录用户可以添加评论。

3.1 展示评论

我们已经对每个评论都添加了文章标识,前端只要根据aid就能拿到当前文章所有的评论。不过我们的评论是要楼中楼的方式展示的,不能一股脑的把数据平铺到页面中。我们在2.2中也说了,pid为0的评论都是直接对文章进行评论的,这些评论应该是作为一级评论展示的;pid为其他数据的,必然是属于某个评论之下,应当作为楼中楼展示。

同时,无论一级评论,还是楼中楼的评论,都有可能产生分页的情况,因此这里也要做好分页处理。

那么最终,我们前端拿到的结构应该大致是这样的:

前端拿到接口返回的数据后,就可以渲染页面了。在头像的处理上,也考虑到了https的环境,因此返回的头像链接都是//开头的形式。

3.2 参与评论

用户对文章或者某个评论产生了共鸣,需要留言讨论一番,我们就需要用户能够把自己的评论也添加进去。

评论的类型,细分的话,可以分为3类:

  • 直接对文章发表评论,pid与replyid为空;

  • 对一级评论进行回复,pid与replyid均为一级评论的id;

  • 对楼中楼进行回复,pid为一级评论的id,replyid为你回复的评论的id

我这里前端的实现参考了oschina(开源中国)的评论方式。直接对文章评论,是直接在顶部的评论窗口进行输入;对其他评论进行回复时,采用弹窗的方式来进行回复。弹窗回复的好处就是,页面不用滚动,用户对某个评论的感知也能停留在这个位置;同时也不用增加各种不必要的小输入框来让用户输入评论。

3.3 登录

在登录问题上,我也是纠结了不少的时间,究竟是使用自己的登录系统呢,还是使用第三方登录呢,或者是用户不用注册登录,只要输入邮箱和昵称就能进行评论呢?

使用自己的评论系统,那么就需要开发一套注册和登录流程,开发麻烦,而且对于想要回复一句话的用户来说,可能就直接放弃注册了;若只要输入邮箱和昵称就能评论,我考虑到可能会引起用户的无限评论,无法控制。因此,最后还是考虑接入第三方的登录,这里选择了使用微博作为第三方登录的入口,后续会考虑加入github的帐号登录。

关于如何接入微博的第三方登录,我们下篇文章再讲,文档齐全,对不熟悉的开发者来说,刚开始可能有点懵逼,不过应该问题不大。

3.4 添加邮箱功能

用户在第三方登录成功后,在名字旁边有个小的input输入框,可以让用户输入邮箱来接收回复提醒,这个输入完全是自愿的,不输入邮箱也依然可以评论。也是考虑到本站是个小站,访问量极低,用户可能一时兴起评论了两句,事后又想起这个网站来,又不知道怎么找了。因此就想着添加一个邮件提醒功能,不让大神的评论石沉大海。

3.5 特别注意

前端部门引入了vue框架,评论模块在每个文章页都会加载。为了防止评论模块中的vue库对外部的资源造成影响(比如版本冲突等),我先把全局变量给了wzVue,然后在把Vue注销掉:

同时,在刚开始实现完成评论功能的时候,用户只要进到这个页面,评论就会加载。但是有个问题就是,用户不一定会把你的文章看到底部,不一定就看你的评论。因此后来文章就改成了按需加载,只有用户滚动到底部,有想要看评论的意向时采取加载评论。

最终展示的效果就是这样:


4. 总结

作为一名前端开发,用仅有的后端知识开发一套博客的评论系统,显得是非常的简陋,整个框架的设计感觉也是很糙。同时缓存系统用的不熟练,不能做到评论信息的立即更新。这个系统依然还有很多改进的地方。欢迎大家对蚊子(师少兵)多多提意见和建议。

在写这篇文章的时候,想着是以后要改版的时候,可以做成评论同步加载的方式进行。生成后的文章,更新频率极低,甚至不太变动,那么缓存的就是评论的内容,每当有新的评论时,就删除当前文章的缓存,重新加载新的数据,然后再缓存上新的数据,这样在评论数据更新比较低的时候,可以缓存的时间更长,同时也有利于搜索引起对评论内容的抓取。

 前面给大家介绍了Vue的组件功能,本文我们通过一个评论列表的案例来巩固下组件的内容,具体效果如下:

在这里插入图片描述

Vue组件案例

1.基本页面

  我们先来整理下基础的页面如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="./lib/vue-2.4.0.js"></script>
</head>
<body>
    <div id="app">

    </div>
    <script>
        var vm = new Vue({
            el: "#app",
            data: {

            },
            methods: {

            }
        })
    </script>
</body>
</html>

2.列表效果

  在基础页面的基础上我们来添加 底部的 评论列表,用bootstrap来实现。如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="./lib/vue-2.4.0.js"></script>
    <link rel="stylesheet" href="./lib/bootstrap-3.3.7.css">
</head>
<body>
    <div id="app">
        <ul class="list-group">
            <!--循环取出列表数据-->
            <li class="list-group-item" v-for="(item ,i) in list" :key="item.id">
                <span class="badge">{{item.name}}</span>
                {{item.content}}
            </li>
        </ul>
    </div>


    <script>
        var vm = new Vue({
            el: "#app",
            data: {
                list: [
                        {id: Date.now(),name:"波波烤鸭1",content:"非常棒..."},
                        {id: Date.now(),name:"波波烤鸭2",content:"非常棒..."},
                        {id: Date.now(),name:"波波烤鸭3",content:"非常棒..."}
                    ]
            },
            methods: {}  
        })
    </script>
</body>
</html>

在这里插入图片描述

3.添加评论组件

  现在我们通过Vue的组件来添加 评论的头部,

在这里插入图片描述

在这里插入图片描述

组件使用

在这里插入图片描述


效果

在这里插入图片描述

4.实现效果

  组件添加好后,我们通过点击 发表评论 来添加内容到 评论列表中。实现的逻辑是

  1. 通过点击 发表评论 触发点击事件 调用组件中methods中定义的方法
  2. 在methods中定义的方法中 加载保存中 localStorage中的列表数据到list中
  3. 将 录入的信息 添加到list中,然后将数据保存到 localStorage中
  4. 调用父组件中的方法来刷新列表数据

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


效果

在这里插入图片描述

添加的效果是实现了,但是在 第一次刷新的时候显示的还是 固定的数据,这时我们可以 在Vue实例的生命周期的方法的 created 中再显示的调用一次 加载数据的方法

在这里插入图片描述

在这里插入图片描述

这样开始加载的就是 localStorage中的数据了。搞定~

完整代码