美文网首页
“真实世界”全栈开发-3.10-评论功能

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

作者: 桥头堡2015 | 来源:发表于2018-02-11 10:29 被阅读47次

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

    新建评论模型

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

    新建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')

    相关文章

      网友评论

          本文标题:“真实世界”全栈开发-3.10-评论功能

          本文链接:https://www.haomeiwen.com/subject/bglktftx.html