4.10.1 留言模型设计

我们只需要留言的作者 id、留言内容和关联的文章 id 这几个字段,修改 lib/mongo.js,添加如下代码:

lib/mongo.js

  1. exports.Comment = mongolass.model('Comment', {
  2. author: { type: Mongolass.Types.ObjectId, required: true },
  3. content: { type: 'string', required: true },
  4. postId: { type: Mongolass.Types.ObjectId, required: true }
  5. })
  6. exports.Comment.index({ postId: 1, _id: 1 }).exec()// 通过文章 id 获取该文章下所有留言,按留言创建时间升序

4.10.2 显示留言

在实现留言功能之前,我们先让文章页可以显示留言列表。首先创建留言的模板,新建 views/components/comments.ejs,添加如下代码:

views/components/comments.ejs

  1. <div class="ui grid">
  2. <div class="four wide column"></div>
  3. <div class="eight wide column">
  4. <div class="ui segment">
  5. <div class="ui minimal comments">
  6. <h3 class="ui dividing header">留言</h3>
  7. <% comments.forEach(function (comment) { %>
  8. <div class="comment">
  9. <span class="avatar">
  10. <img src="/img/<%= comment.author.avatar %>">
  11. </span>
  12. <div class="content">
  13. <a class="author" href="/posts?author=<%= comment.author._id %>"><%= comment.author.name %></a>
  14. <div class="metadata">
  15. <span class="date"><%= comment.created_at %></span>
  16. </div>
  17. <div class="text"><%- comment.content %></div>
  18. <% if (user && comment.author._id && user._id.toString() === comment.author._id.toString()) { %>
  19. <div class="actions">
  20. <a class="reply" href="/comments/<%= comment._id %>/remove">删除</a>
  21. </div>
  22. <% } %>
  23. </div>
  24. </div>
  25. <% }) %>
  26. <% if (user) { %>
  27. <form class="ui reply form" method="post" action="/comments">
  28. <input name="postId" value="<%= post._id %>" hidden>
  29. <div class="field">
  30. <textarea name="content"></textarea>
  31. </div>
  32. <input type="submit" class="ui icon button" value="留言" />
  33. </form>
  34. <% } %>
  35. </div>
  36. </div>
  37. </div>
  38. </div>

注意:我们在提交留言表单时带上了文章 id(postId),通过 hidden 隐藏。

在文章页引入留言的模板片段,修改 views/post.ejs 为:

views/post.ejs

  1. <%- include('header') %>
  2. <%- include('components/post-content') %>
  3. <%- include('components/comments') %>
  4. <%- include('footer') %>

新建 models/comments.js,存放留言相关的数据库操作,添加如下代码:

models/comments.js

  1. const marked = require('marked')
  2. const Comment = require('../lib/mongo').Comment
  3. // 将 comment 的 content 从 markdown 转换成 html
  4. Comment.plugin('contentToHtml', {
  5. afterFind: function (comments) {
  6. return comments.map(function (comment) {
  7. comment.content = marked(comment.content)
  8. return comment
  9. })
  10. }
  11. })
  12. module.exports = {
  13. // 创建一个留言
  14. create: function create (comment) {
  15. return Comment.create(comment).exec()
  16. },
  17. // 通过留言 id 获取一个留言
  18. getCommentById: function getCommentById (commentId) {
  19. return Comment.findOne({ _id: commentId }).exec()
  20. },
  21. // 通过留言 id 删除一个留言
  22. delCommentById: function delCommentById (commentId) {
  23. return Comment.deleteOne({ _id: commentId }).exec()
  24. },
  25. // 通过文章 id 删除该文章下所有留言
  26. delCommentsByPostId: function delCommentsByPostId (postId) {
  27. return Comment.deleteMany({ postId: postId }).exec()
  28. },
  29. // 通过文章 id 获取该文章下所有留言,按留言创建时间升序
  30. getComments: function getComments (postId) {
  31. return Comment
  32. .find({ postId: postId })
  33. .populate({ path: 'author', model: 'User' })
  34. .sort({ _id: 1 })
  35. .addCreatedAt()
  36. .contentToHtml()
  37. .exec()
  38. },
  39. // 通过文章 id 获取该文章下留言数
  40. getCommentsCount: function getCommentsCount (postId) {
  41. return Comment.count({ postId: postId }).exec()
  42. }
  43. }

小提示:我们让留言也支持了 markdown。
注意:删除一篇文章成功后也要删除该文章下所有的评论,上面 delCommentsByPostId 就是用来做这件事的。

修改 models/posts.js,在:

models/posts.js

  1. const Post = require('../lib/mongo').Post

下添加如下代码:

  1. const CommentModel = require('./comments')
  2. // 给 post 添加留言数 commentsCount
  3. Post.plugin('addCommentsCount', {
  4. afterFind: function (posts) {
  5. return Promise.all(posts.map(function (post) {
  6. return CommentModel.getCommentsCount(post._id).then(function (commentsCount) {
  7. post.commentsCount = commentsCount
  8. return post
  9. })
  10. }))
  11. },
  12. afterFindOne: function (post) {
  13. if (post) {
  14. return CommentModel.getCommentsCount(post._id).then(function (count) {
  15. post.commentsCount = count
  16. return post
  17. })
  18. }
  19. return post
  20. }
  21. })

在 PostModel 上注册了 addCommentsCount 用来给每篇文章添加留言数 commentsCount,在 getPostByIdgetPosts 方法里的:

  1. .addCreatedAt()

下添加:

  1. .addCommentsCount()

这样主页和文章页的文章就可以正常显示留言数了。

然后将 delPostById 修改为:

  1. // 通过用户 id 和文章 id 删除一篇文章
  2. delPostById: function delPostById (postId, author) {
  3. return Post.deleteOne({ author: author, _id: postId })
  4. .exec()
  5. .then(function (res) {
  6. // 文章删除后,再删除该文章下的所有留言
  7. if (res.result.ok && res.result.n > 0) {
  8. return CommentModel.delCommentsByPostId(postId)
  9. }
  10. })
  11. }

小提示:虽然目前看起来使用 Mongolass 自定义插件并不能节省代码,反而使代码变多了。Mongolass 插件真正的优势在于:在项目非常庞大时,可通过自定义的插件随意组合(及顺序)实现不同的输出,如上面的 getPostById 需要将取出 markdown 转换成 html,则使用 .contentToHtml(),否则像 getRawPostById 则不必使用。

修改 routes/posts.js,在:

routes/posts.js

  1. const PostModel = require('../models/posts')

下引入 CommentModel:

  1. const CommentModel = require('../models/comments')

在文章页传入留言列表,将:

  1. // GET /posts/:postId 单独一篇的文章页
  2. router.get('/:postId', function (req, res, next) {
  3. ...
  4. })

修改为:

  1. // GET /posts/:postId 单独一篇的文章页
  2. router.get('/:postId', function (req, res, next) {
  3. const postId = req.params.postId
  4. Promise.all([
  5. PostModel.getPostById(postId), // 获取文章信息
  6. CommentModel.getComments(postId), // 获取该文章所有留言
  7. PostModel.incPv(postId)// pv 加 1
  8. ])
  9. .then(function (result) {
  10. const post = result[0]
  11. const comments = result[1]
  12. if (!post) {
  13. throw new Error('该文章不存在')
  14. }
  15. res.render('post', {
  16. post: post,
  17. comments: comments
  18. })
  19. })
  20. .catch(next)
  21. })

现在刷新文章页试试吧,此时已经显示了留言的输入框。

4.10.3 发表与删除留言

现在我们来实现发表与删除留言的功能。将 routes/comments.js 修改如下:

  1. const express = require('express')
  2. const router = express.Router()
  3. const checkLogin = require('../middlewares/check').checkLogin
  4. const CommentModel = require('../models/comments')
  5. // POST /comments 创建一条留言
  6. router.post('/', checkLogin, function (req, res, next) {
  7. const author = req.session.user._id
  8. const postId = req.fields.postId
  9. const content = req.fields.content
  10. // 校验参数
  11. try {
  12. if (!content.length) {
  13. throw new Error('请填写留言内容')
  14. }
  15. } catch (e) {
  16. req.flash('error', e.message)
  17. return res.redirect('back')
  18. }
  19. const comment = {
  20. author: author,
  21. postId: postId,
  22. content: content
  23. }
  24. CommentModel.create(comment)
  25. .then(function () {
  26. req.flash('success', '留言成功')
  27. // 留言成功后跳转到上一页
  28. res.redirect('back')
  29. })
  30. .catch(next)
  31. })
  32. // GET /comments/:commentId/remove 删除一条留言
  33. router.get('/:commentId/remove', checkLogin, function (req, res, next) {
  34. const commentId = req.params.commentId
  35. const author = req.session.user._id
  36. CommentModel.getCommentById(commentId)
  37. .then(function (comment) {
  38. if (!comment) {
  39. throw new Error('留言不存在')
  40. }
  41. if (comment.author.toString() !== author.toString()) {
  42. throw new Error('没有权限删除留言')
  43. }
  44. CommentModel.delCommentById(commentId)
  45. .then(function () {
  46. req.flash('success', '删除留言成功')
  47. // 删除成功后跳转到上一页
  48. res.redirect('back')
  49. })
  50. .catch(next)
  51. })
  52. })
  53. module.exports = router

至此,我们完成了创建留言和删除留言的逻辑。刷新页面,尝试留言试试吧。留言成功后,将鼠标悬浮在留言上可以显示出 删除 的按钮,点击可以删除留言。