“真实世界”全栈开发-3.10-评论功能

2018-02-11  本文已影响47人  桥头堡2015

没有评论功能,则不成其为社交平台。我们应用的定位是社交性的博客平台,所以我们必须为用户提供在博文底下留言评论的功能。由此,在模型结构上,评论应该与博文相联系;后端逻辑上,只有登录过的用户才能够发表评论。

新建评论模型

上一节中的点赞功能由于没有额外的信息,所以只需要修改已有的模型。而为了存储评论的文本,我们需要为其创建专有的模型。

新建models/Comment.js文件,写入:

const mongoose = require('mongoose');

const CommentSchema = new mongoose.Schema({
  body: String,
  author: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
}, {timestamps: true});

CommentSchema.methods.toJSONFor = function (user) {
  return {
    id: this._id,
    body: this.body,
    createdAt: this.createdAt,
    author: this.author.toProfileJSONFor(user)
  };
};

mongoose.model('Comment', CommentSchema);
module.exports = CommentSchema;

注意我们为评论模型也定义了toJSONFor方法,它返回的JSON对象的格式符合第二部分中的设计。

老套路,我们需要在app.js里登记这个新模型。

require('./models/User');
require('./models/Article');
// +++
require('./models/Comment');
// +++
require('./config/passport');

修改博文模型

接下来我们要修改博文模型。用户与博文的关系、博文与评论的关系,两者都是隶属。但有一点不同,那就是我们没有提供删除用户的功能,所以不会出现“孤儿”博文的情况。而删除一篇博文时,其下所有的评论也要自动删除,这个时候最佳的策略是把评论存为博文的子文档。在使用上,子文档和一般的文档(也就是一个模型对象)最大的差别在于子文档不能单独存储,而是依赖于父文档的save()。更多内容请移步Mongoose的相关文档

打开models/Article.js,加入:

// +++
const CommentSchema = require('./Comment');
// +++

const ArticleSchema = new mongoose.Schema({
  // ...
  // +++
  comments: [CommentSchema],
  // +++
}, {timestamps: true});

以上就是评论功能所需的所有模型层面的改动。请注意,我们没有在用户模型里存储所发表评论的列表,因为我们不需要读取某个用户的所有评论。评论的读取只和博文有关。

实现API端点

与评论相关的操作,应用中有三个:发表新评论,删除已有评论,以及读取某博文的所有评论。

发表新评论

这个操作也属于博文路由的一部分,其端点为POST /api/articles/:slug/comments,需要身份验证,(如果成功)返回新添评论的JSON对象,请求体的格式为:

{
  "comment": {
    "body": "His name was my name too."
  }
}

打开routes/api/articles.js,首先导入评论的模型:

const User = mongoose.model('User');
// +++
const Comment = mongoose.model('Comment');
// +++
const auth = require('../auth');

加入一条新路由:

// 响应评论
const respondComment = (req, res, next) => {
  res.json(res.locals.comment.toJSONFor(res.locals.user));
};

// 新建评论
router.post('/:slug/comments', auth.required, loadCurrentUser, (req, res, next) => {
  const article = res.locals.article;
  const comment = new Comment(req.body.comment);
  comment.article = article;
  comment.author = res.locals.user;
  article.comments.push(comment);
  article.save()
    .then(() => {
      res.locals.comment = comment;
      return next()
    })
    .catch(next);
}, respondComment);

删除已有评论

要精准地删除某条评论,我们须在URL中指定其ID,并且在后端抽取出来。这里我们不用router.param来做URL中的参数抽取,因为不需要利用Comment模型来从数据库读取评论对象(评论子文档已经随博文主文档一同读取了)。

// 删除评论
router.delete('/:slug/comments/:comment_id', auth.required, loadCurrentUser,
  (req, res, next) => {
    const article = res.locals.article;
    const comment = article.comments.id(req.params.comment_id);
    if (!res.locals.user.equals(comment.author))
      res.status(403).json({errors: {user: 'not the comment author'}});
    comment.remove();
    article.save()
      .then(() => res.sendStatus(204))
      .catch(next);
  });

值得指出的是,这里我们用到了Mongoose里的Document.prototype.equals方法来比较当前用户和待删除的评论的作者。代码里,res.locals.user是一个User对象,而comment.author,没有经过populate,实际上是一个ObjectId。虽然如此,经过测验,两者好像也可以比较,如果后者确实同前者的_id相等,返回的值为true。这是Mongoose的相关文档里没有提及的。

读取某博文的所有评论

端点为GET /api/articles/:slug/comments,身份验证可有可无,返回评论列表。

// 读取博文的所有评论
router.get('/:slug/comments', auth.optional, loadCurrentUser,
  (req, res, next) => {
    res.locals.article.comments.sort((a, b) => a.createdAt - b.createdAt);
    Promise.all(res.locals.article.comments.map((comment, idx) =>
      res.locals.article.populate(`comments.${idx}.author`).execPopulate()
    ))
      .then(articles =>
        res.json(articles[0].comments.map(comment => comment.toJSONFor(res.locals.user)))
      )
      .catch(next);
  });

上面的代码里值得指出的是populate不能用于子文档,所以我们得用article.populate('comments[0].author'),而不是comment.populate('author')

上一篇 下一篇

猜你喜欢

热点阅读