NodeJS Express MongoDB 实现评论功能

创建
阅读 692

没有评论功能,就不能互动;虽然没多的人互动,但总得备着,万一有呢 😄

记录一下实现思路。实现了登录后发表评论,匿名发表评论,以及回复评论。

与相关模块关联

在 MongoDB 中比较提倡将相关的内容内嵌保存,例如可以将一片文章所有评论内容,作为一个数组与各个文章中保存在一起。这样做是可以做,但是考虑到我的评论是一个相对独立的功能,而且后面还有其他不同的模块可能都会有评论、留言功能,拎出来作为单独的模块来实现,结构清晰,便于管理,更加合适。

通过 kind 字段指定评论所属模块,kindId 关联该模块相关条目的 id,比如 { kind : "post" , kindId: ObjectId("5ffd90d683333600122e7f70")} 关联的是 post 中 id 为 5ffd90d683333600122e7f70 的内容

这里 kind 还单独列出一类为 general,是为了用于一些没有对应模块的特殊页面,kindgeneral 的评论没有相应模块,也没有相应的条目 id,所以再新增加一个字段 identifier 来区别各个页面,目前这个 identifier 就交给前端来控制了。

记录评论的用户信息

小站为了降低评论门槛,允许匿名评论,只要填写评论内容就可以发表了。

所以这里再区分一下,如果用户登录了,将评论者的用户信息通过 createUser 关联上,如果未登录,则通过 anonymousInfo 保存评论者的用户信息

新增评论

如果新增评论,就不用传 parent,如果是回复评论,通过 parent 指定要回复的评论 id

查的时候不存在 parent 都是一级评论,存在 parent 都是二级评论。

ps: 如果 parent 也是不是一级评论也是可以回复成功的,但是当前的查询语句不能查出来,没法展示。就只能通过前端控制,不提供回复二级评论的功能,哈哈 😂

评论查询

评论查询是当前功能的难点。

涉及到一些之前未接触过的查询语句,通过在聚合查询(aggregate)中的 $lookup 使用管道 pipeline 并指定 $project 显性输出关联文档中需要的字段,而不要输出关联文档中所有字段。因为在 users 文档中,保存了用户的密码、用户邮箱等敏感信息,而这些信息在查看评论时候,并不需要。

实现源码

评论的 schema

const mongoose = require('mongoose');
const xss = require('xss');

const commentSchema = new mongoose.Schema({
  // 评论内容
  body: {
    type: String,
    required: true,
  },
  // 父评论id,没有父评论,就是一级评论
  parent: {
    type: mongoose.Types.ObjectId,
    ref: 'Comment',
    default: null,
  },
  // general 通用类型,不关联 kindId
  kind: {
    type: String,
    required: true,
    enum: ['general', 'post'],
  },
  kindId: {
    type: mongoose.Types.ObjectId,
    refPath: 'kind',
  },
  // 尽可能使用 kindId,当不存在 kindId 时,通过 identifier 作为某个页面、类别的标示
  identifier: {
    type: String,
  },
  // 是否为未登录用户
  anonymous: {
    type: Boolean,
    default: false,
  },
  // 未登录用户的用户信息
  anonymousInfo: {
    username: {
      type: String,
    },
    email: {
      type: String,
    },
    url: {
      type: String,
    },
  },
  // 登录用户的用户信息,未登录用户没有创建者
  createUser: {
    ref: 'users',
    type: mongoose.Types.ObjectId,
    trim: true,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

commentSchema.pre('save', function xssBody() {
  // 使用 xss 库处理输入的内容,防止 xss 攻击
  this.body = xss(this.body);
  if (this.anonymousInfo) {
    if (this.anonymousInfo.username) {
      this.anonymousInfo.username = xss(this.anonymousInfo.username);
    }
    if (this.anonymousInfo.email) {
      this.anonymousInfo.email = xss(this.anonymousInfo.email);
    }
    if (this.anonymousInfo.url) {
      this.anonymousInfo.url = xss(this.anonymousInfo.url);
    }
  }
});

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

新建评论

const Comment = require('../models/comment');
const router = express.Router();

router.post(
  '/result',
  async (req, res) => {
    const comment = new Comment();
    comment.body = req.body.body.trim();
    comment.kind = req.body.kind;
    comment.kindId = req.body.kindId;
    if (req.body.parent) {
      comment.parent = req.body.parent;
    }
    if (req.user) {
      comment.createUser = req.user.id;
      comment.anonymous = false;
    } else {
      comment.anonymousInfo.username = req.body.username.trim() ? req.body.username.trim() : '匿名';
      comment.anonymousInfo.email = req.body.email.trim();
      comment.anonymousInfo.url = req.body.url.trim();
      comment.anonymous = true;
    }
    try {
      await comment.save();
      res.render('comment/result', { result_msgs: ['留言成功'] });
    } catch (e) {
      res.render('comment/result', { result_msgs: ['留言失败'] });
    }
  },
);

查询评论

const Comment = require('../models/comment');

const comments = await Comment.aggregate([
  {
    $match: {
      kind: 'post',
      kindId: post._id,
      parent: null,
    },
  },
  {
    $lookup: {
      from: 'users', // 从哪个Schema中查询(一般需要复数,除非声明Schema的时候专门有处理)
      as: 'createUser',
      let: { createUser: '$createUser' },
      pipeline: [
        {
          $match: {
            $expr: {
              $eq: ['$$createUser', '$_id'],
            },
          },
        },
        { $project: { name: 1 } },
      ],
    },
  },
  {
    $project: {
      parent: 1,
      createdAt: 1,
      anonymous: 1,
      anonymousInfo: 1,
      body: 1,
      kind: 1,
      kindId: 1,
      createUser: { $arrayElemAt: ['$createUser', 0] },
    },
  },
  {
    $lookup: {
      from: 'comments',
      let: { id: '$_id' },
      as: 'children',
      pipeline: [
        { $match: { $expr: { $eq: ['$$id', '$parent'] } } },
        {
          $lookup: {
            from: 'users', 
            as: 'createUser',
            let: { createUser: '$createUser' },
            pipeline: [
              {
                $match: {
                  $expr: {
                    $eq: ['$$createUser', '$_id'],
                  },
                },
              },

              { $project: { name: 1 } },
            ],
          },
        },
        {
          $project: {
            parent: 1,
            createdAt: 1,
            anonymous: 1,
            anonymousInfo: 1,
            body: 1,
            kind: 1,
            kindId: 1,
            createUser: { $arrayElemAt: ['$createUser', 0] },
          },
        },
      ],
    },
  },
]);
查询语句返回的数据结构
[
  {
    "_id": "600d91ded96de9001253ab43",
    "parent": null,
    "anonymous": false,
    "createdAt": "2021-01-24T15:27:26.313Z",
    "body": "评论1",
    "kind": "post",
    "kindId": "600d91cdd96de9001253ab42",
    "createUser": {
      "_id": "5e8f1a60eadeec001299ea75",
      "name": "ryanlid"
    },
    "children": [
      {
        "_id": "600d923ed96de9001253ab46",
        "parent": "600d91ded96de9001253ab43",
        "anonymous": false,
        "createdAt": "2021-01-24T15:29:02.370Z",
        "body": "回复评论1",
        "kind": "post",
        "kindId": "600d91cdd96de9001253ab42",
        "createUser": {
          "_id": "5e8f1a60eadeec001299ea75",
          "name": "ryanlid"
        }
      }
    ]
  },
  {
    "_id": "600d91ebd96de9001253ab44",
    "parent": null,
    "anonymous": false,
    "createdAt": "2021-01-24T15:27:39.390Z",
    "body": "评论2",
    "kind": "post",
    "kindId": "600d91cdd96de9001253ab42",
    "createUser": {
      "_id": "5e8f1a60eadeec001299ea75",
      "name": "ryanlid"
    },
    "children": []
  },
  {
    "_id": "600d9211d96de9001253ab45",
    "anonymousInfo": {
      "username": "匿名评论",
      "email": "test@example.com",
      "url": "http://www.example.com"
    },
    "parent": null,
    "anonymous": true,
    "createdAt": "2021-01-24T15:28:17.651Z",
    "body": "匿名评论内容1",
    "kind": "post",
    "kindId": "600d91cdd96de9001253ab42",
    "children": [
      {
        "_id": "600d9275d96de9001253ab47",
        "anonymousInfo": {
          "username": "匿名回复用户",
          "email": "test2@example.com",
          "url": "http://www.example.com"
        },
        "parent": "600d9211d96de9001253ab45",
        "anonymous": true,
        "createdAt": "2021-01-24T15:29:57.730Z",
        "body": "匿名回复内容1",
        "kind": "post",
        "kindId": "600d91cdd96de9001253ab42"
      }
    ]
  }
]

本文链接 https://www.yidiankuaile.com/post/mongodb-implement-comments

最后更新