likes
comments
collection
share

Node.js<二十一>——项目实战-评论模块

作者站长头像
站长
· 阅读数 39

该模块是基于动态管理模块之后做的,主要实现的功能有:评论动态、回复评论、删除评论、获取评论、获取评论/回复的回复,该文章主要是帮助自己复盘这个模块所写的代码,大家单看的话可能收益不是很大

  1. router/comment.router.js

既然是一个新的模块,那么肯定是要新建一个路由的,里面注册了很多api所对应的中间件,比如添加评论、删除评论等等。我们开发api时尽量要符合restful风格:宾语 URL 应该全部使用名词复数,比如下面的commentsreplies,如果是针对于评论进行操作的,则我们要让用户以params的形式拼接到url上传递过来

const Router = require('koa-router')

const {
  verifyAuth,
  verifyPermission
} = require('../middleware/auth.middleware')
const {
  create,
  reply,
  remove,
  update,
  list,
  replyList
} = require('../controller/comment.controller')

const commentRouter = new Router({ prefix: '/comments' })

// 添加评论
commentRouter.post('/', verifyAuth, create)
// 回复评论
commentRouter.post('/:commentId/reply', verifyAuth, reply)
// 删除评论
commentRouter.delete('/:commentId/delete', verifyAuth, verifyPermission, remove)
// 获取评论
commentRouter.get('/', list)
// 获取评论/回复的回复
commentRouter.get('/:commentId/replies', replyList)

module.exports = commentRouter
  1. controller/comment.controller.js

这个文件通过类的方式,在其原型上面集成了对应api的操作函数,各个函数的大体思路很相似,如果是登录之后才能做的操作,就需要先从ctx.body中获取到从token中解析出来的userId,其次需要知道有关操作的信息,比如说添加评论需要知道对应的内容content和动态id等等;获取到这些之后,再将参数传递到有关数据库操作的函数中去,完成之后响应结果即可

const commentService = require("../service/comment.service")

class CommentController {
  // 添加评论
  async create(ctx, next) {
    const { id: userId } = ctx.user
    const { dynamicId, content } = ctx.request.body
    const results = await commentService.create(userId, dynamicId, content)
    ctx.body = results
  }
  
  // 回复评论
  async reply(ctx, next) {
    const { id: userId } = ctx.user
    const { commentId } = ctx.params
    const { dynamicId, content } = ctx.request.body
    const results = await commentService.reply(userId, dynamicId, content, commentId)
    ctx.body = results
  }

  // 删除评论
  async remove(ctx, next) {
    const { commentId } = ctx.params
    const results = await commentService.delete(commentId)
    ctx.body = results
  }

  // 获取评论
  async list(ctx, next) {
    const { dynamicId, offset, limit } = ctx.query
    const results = await commentService.getList(dynamicId, offset, limit)
    ctx.body = results
  }

  // 获取评论/回复的回复
  async replyList(ctx, next) {
    const { commentId } = ctx.params
    const { offset, limit } = ctx.query
    const results = await commentService.getReplyList(commentId, offset, limit)
    ctx.body = results
  }
}

module.exports = new CommentController()
  1. service/comment.service.js

一般直接操作数据库的代码我们会分离出来放到service文件夹中,这样子更加利于管理,这里并没有提供编辑评论/回复的操作,因为这样可能会造成误解;其次发现有一些操作的sql语句很大一部分是重复的,我们可以单独抽离成一个变量或者是一个函数,通过参数来决定最终sql语句的返回值,这样可以在一定程度上简化我们的代码

const connection = require('../app/database')

const getSqlFragment = (isReply) => {
  return (
    `INSERT INTO comment 
    (user_id, dynamic_id, content${isReply ? ', comment_id' : ''}) 
    VALUES(?, ?, ?${isReply ? ', ?' : ''});`
  )
}

const sqlFragment = `
  SELECT c.id id, c.content content, c.dynamic_id dynamicId, c.comment_id commentId, c.createAt createTime, c.updateAt updateTime,
  JSON_OBJECT('id', u.id, 'name', u.name) user
  FROM comment c
  LEFT JOIN user u 
  ON u.id = c.user_id
`

class CommentService {
  // 添加评论
  async create(userId, dynamicId, content) {
    const statement = getSqlFragment()
    const [results] = await connection.execute(statement, [userId, dynamicId, content])
    return results
  }
  
  // 回复评论/回复
  async reply(userId, dynamicId, content, commentId) {
    const statement = getSqlFragment(true)
    const [results] = await connection.execute(statement, [userId, dynamicId, content, commentId])
    return results
  }

  // 删除评论
  async delete(commentId) {
    const statement = `DELETE FROM comment WHERE id = ?;`
    const [results] = await connection.execute(statement, [commentId])
    return results
  }

  // 获取评论
  async getList(dynamicId, offset = 0, limit = 5) {
    const statement = `
      ${sqlFragment}
      WHERE dynamic_id = ?
      LIMIT ?, ?;
    `
    const [results] = await connection.execute(statement, [dynamicId, offset, limit])
    return results
  }

  // 获取回复
  async getReplyList(commentId, offset = 0, limit = 5) {
    const statement = `
      ${sqlFragment}
      WHERE comment_id = ?
      LIMIT ?, ?;
    `
    const [results] = await connection.execute(statement, [commentId, offset, limit])
    return results
  }
}

module.exports = new CommentService()
  1. middleware/duth.middleware.js

但是评论的删除涉及到权限的认证,比如用户1不能删除用户2的评论。我们之前是写过一个针对于动态的权限验证中间件verifyPermission的,现在我们要对其做一个改动,让其可以适用于所有模块的权限验证

既然要做到适用于每一个权限的操作,我们就要考虑到的它的通用性。仔细想想我们是怎么检验权限的?不就是先看看用户操作的目标存不存在,如果不存在就返回报错信息,如果目标存在,再根据用户id比较一下目标所对应的用户是不是当前的操作者,如果不是则说明当前操作者没有权限,返回错误信息,如果有权限就进入到下一个中间件中

但是又有一个问题,每一个模块操作的表都不一样,我们有什么办法可以获取到表名称呢?一般需要权限操作的都需要传对应的id过来,而对应的名称前面一部分就是我们的表名称,比如说commentId对应的表就是comment,我们只需要截取一下即可

const errTypes = require('../constants/err-types')
const { checkResource } = require('../service/auth.service')

// middleware/auth.middleware.js
const verifyPermission = async (ctx, next) => {
  // 从解析出来的token中获取用户id,用于下面权限的校验
  const { id: userId } = ctx.user
  // 由于一般需要此权限操作的都会通过params传一个目标id过来,其key的前一部分刚好对应我们的表名
  const key = Object.keys(ctx.params)[0]
  const id = ctx.params[key]
  const tableName = key?.replace('Id', '')
  // 我们需要利用checkResource函数从数据库中查询对应数据
  const result = await checkResource(id, tableName)
  // 通过查询到的数据是否为空来判断用户操作的目标是否存在,不存在则返回错误信息
  if (!result) {
    const error = new Error(errTypes.TARGET_IS_NOT_EXISTS)
    return ctx.app.emit('error', error, ctx)
    // 判断当前用户和操作目标对应的用户是不是同一个,如果不是则说明其没有权限,返回错误信息
  } else if (result.user_id !== userId) {
    const error = new Error(errTypes.UN_PERMISSION)
    return ctx.app.emit('error', error, ctx)
  }
  await next()
}

module.exports = {
  verifyPermission
}
  1. service/auth.service.js

由于以前的checkResource函数是针对于动态的,但现在需要改造成通用的,所以其内部的查询sql语句也要根据传入的参数而动态改变

const connection = require('../app/database')

class AuthService {
  async checkResource(id, tableName) {
    const statement = `SELECT * FROM ${tableName} WHERE id = ?;`
    const [results] = await connection.execute(statement, [id])
    return results[0]
  }
}

module.exports = new AuthService()

至此,我们的评论模块就基本完成了