没有评论功能,则不成其为社交平台。我们应用的定位是社交性的博客平台,所以我们必须为用户提供在博文底下留言评论的功能。由此,在模型结构上,评论应该与博文相联系;后端逻辑上,只有登录过的用户才能够发表评论。
新建评论模型
上一节中的点赞功能由于没有额外的信息,所以只需要修改已有的模型。而为了存储评论的文本,我们需要为其创建专有的模型。
新建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')
。
网友评论