4.10.1 留言模型设计
我们只需要留言的作者 id、留言内容和关联的文章 id 这几个字段,修改 lib/mongo.js,添加如下代码:
lib/mongo.js
exports.Comment = mongolass.model('Comment', {author: { type: Mongolass.Types.ObjectId, required: true },content: { type: 'string', required: true },postId: { type: Mongolass.Types.ObjectId, required: true }})exports.Comment.index({ postId: 1, _id: 1 }).exec()// 通过文章 id 获取该文章下所有留言,按留言创建时间升序
4.10.2 显示留言
在实现留言功能之前,我们先让文章页可以显示留言列表。首先创建留言的模板,新建 views/components/comments.ejs,添加如下代码:
views/components/comments.ejs
<div class="ui grid"><div class="four wide column"></div><div class="eight wide column"><div class="ui segment"><div class="ui minimal comments"><h3 class="ui dividing header">留言</h3><% comments.forEach(function (comment) { %><div class="comment"><span class="avatar"><img src="/img/<%= comment.author.avatar %>"></span><div class="content"><a class="author" href="/posts?author=<%= comment.author._id %>"><%= comment.author.name %></a><div class="metadata"><span class="date"><%= comment.created_at %></span></div><div class="text"><%- comment.content %></div><% if (user && comment.author._id && user._id.toString() === comment.author._id.toString()) { %><div class="actions"><a class="reply" href="/comments/<%= comment._id %>/remove">删除</a></div><% } %></div></div><% }) %><% if (user) { %><form class="ui reply form" method="post" action="/comments"><input name="postId" value="<%= post._id %>" hidden><div class="field"><textarea name="content"></textarea></div><input type="submit" class="ui icon button" value="留言" /></form><% } %></div></div></div></div>
注意:我们在提交留言表单时带上了文章 id(postId),通过 hidden 隐藏。
在文章页引入留言的模板片段,修改 views/post.ejs 为:
views/post.ejs
<%- include('header') %><%- include('components/post-content') %><%- include('components/comments') %><%- include('footer') %>
新建 models/comments.js,存放留言相关的数据库操作,添加如下代码:
models/comments.js
const marked = require('marked')const Comment = require('../lib/mongo').Comment// 将 comment 的 content 从 markdown 转换成 htmlComment.plugin('contentToHtml', {afterFind: function (comments) {return comments.map(function (comment) {comment.content = marked(comment.content)return comment})}})module.exports = {// 创建一个留言create: function create (comment) {return Comment.create(comment).exec()},// 通过留言 id 获取一个留言getCommentById: function getCommentById (commentId) {return Comment.findOne({ _id: commentId }).exec()},// 通过留言 id 删除一个留言delCommentById: function delCommentById (commentId) {return Comment.deleteOne({ _id: commentId }).exec()},// 通过文章 id 删除该文章下所有留言delCommentsByPostId: function delCommentsByPostId (postId) {return Comment.deleteMany({ postId: postId }).exec()},// 通过文章 id 获取该文章下所有留言,按留言创建时间升序getComments: function getComments (postId) {return Comment.find({ postId: postId }).populate({ path: 'author', model: 'User' }).sort({ _id: 1 }).addCreatedAt().contentToHtml().exec()},// 通过文章 id 获取该文章下留言数getCommentsCount: function getCommentsCount (postId) {return Comment.count({ postId: postId }).exec()}}
小提示:我们让留言也支持了 markdown。
注意:删除一篇文章成功后也要删除该文章下所有的评论,上面 delCommentsByPostId 就是用来做这件事的。
修改 models/posts.js,在:
models/posts.js
const Post = require('../lib/mongo').Post
下添加如下代码:
const CommentModel = require('./comments')// 给 post 添加留言数 commentsCountPost.plugin('addCommentsCount', {afterFind: function (posts) {return Promise.all(posts.map(function (post) {return CommentModel.getCommentsCount(post._id).then(function (commentsCount) {post.commentsCount = commentsCountreturn post})}))},afterFindOne: function (post) {if (post) {return CommentModel.getCommentsCount(post._id).then(function (count) {post.commentsCount = countreturn post})}return post}})
在 PostModel 上注册了 addCommentsCount 用来给每篇文章添加留言数 commentsCount,在 getPostById 和 getPosts 方法里的:
.addCreatedAt()
下添加:
.addCommentsCount()
这样主页和文章页的文章就可以正常显示留言数了。
然后将 delPostById 修改为:
// 通过用户 id 和文章 id 删除一篇文章delPostById: function delPostById (postId, author) {return Post.deleteOne({ author: author, _id: postId }).exec().then(function (res) {// 文章删除后,再删除该文章下的所有留言if (res.result.ok && res.result.n > 0) {return CommentModel.delCommentsByPostId(postId)}})}
小提示:虽然目前看起来使用 Mongolass 自定义插件并不能节省代码,反而使代码变多了。Mongolass 插件真正的优势在于:在项目非常庞大时,可通过自定义的插件随意组合(及顺序)实现不同的输出,如上面的
getPostById需要将取出 markdown 转换成 html,则使用.contentToHtml(),否则像getRawPostById则不必使用。
修改 routes/posts.js,在:
routes/posts.js
const PostModel = require('../models/posts')
下引入 CommentModel:
const CommentModel = require('../models/comments')
在文章页传入留言列表,将:
// GET /posts/:postId 单独一篇的文章页router.get('/:postId', function (req, res, next) {...})
修改为:
// GET /posts/:postId 单独一篇的文章页router.get('/:postId', function (req, res, next) {const postId = req.params.postIdPromise.all([PostModel.getPostById(postId), // 获取文章信息CommentModel.getComments(postId), // 获取该文章所有留言PostModel.incPv(postId)// pv 加 1]).then(function (result) {const post = result[0]const comments = result[1]if (!post) {throw new Error('该文章不存在')}res.render('post', {post: post,comments: comments})}).catch(next)})
现在刷新文章页试试吧,此时已经显示了留言的输入框。
4.10.3 发表与删除留言
现在我们来实现发表与删除留言的功能。将 routes/comments.js 修改如下:
const express = require('express')const router = express.Router()const checkLogin = require('../middlewares/check').checkLoginconst CommentModel = require('../models/comments')// POST /comments 创建一条留言router.post('/', checkLogin, function (req, res, next) {const author = req.session.user._idconst postId = req.fields.postIdconst content = req.fields.content// 校验参数try {if (!content.length) {throw new Error('请填写留言内容')}} catch (e) {req.flash('error', e.message)return res.redirect('back')}const comment = {author: author,postId: postId,content: content}CommentModel.create(comment).then(function () {req.flash('success', '留言成功')// 留言成功后跳转到上一页res.redirect('back')}).catch(next)})// GET /comments/:commentId/remove 删除一条留言router.get('/:commentId/remove', checkLogin, function (req, res, next) {const commentId = req.params.commentIdconst author = req.session.user._idCommentModel.getCommentById(commentId).then(function (comment) {if (!comment) {throw new Error('留言不存在')}if (comment.author.toString() !== author.toString()) {throw new Error('没有权限删除留言')}CommentModel.delCommentById(commentId).then(function () {req.flash('success', '删除留言成功')// 删除成功后跳转到上一页res.redirect('back')}).catch(next)})})module.exports = router
至此,我们完成了创建留言和删除留言的逻辑。刷新页面,尝试留言试试吧。留言成功后,将鼠标悬浮在留言上可以显示出 删除 的按钮,点击可以删除留言。
