koa2 仿知乎笔记

Koa2 仿知乎笔记

路由

普通路由

const Router = require("koa-router")
const router = new Router()

router.get("/", (ctx) => {
     
    ctx.body = "这是主页"
})
router.get("/user", (ctx) => {
     
    ctx.body = "这是用户列表"
})

app.use(router.routes());

ctx.body 可以渲染页面, 也可以是返回的数据内容

前缀路由

const Router = require("koa-router")
const userRouter = new Router({
      prefix: "/users" })

userRouter.get("/", (ctx) => {
     
    ctx.body = "这是用户列表"
})

app.use(userRouter.routes());

使用的是 prefix 前缀,简化路由的书写

HTTP options 方法

主要作用就是检查一下某接口支持哪些 HTTP 方法

allowedMethods 的作用

  1. 响应 options 的方法,告诉它所支持的请求方法
app.use(router.allowedMethods());

加上它,使该接口支持了 options 请求

koa2 仿知乎笔记_第1张图片

  1. 相应地返回 405(不允许)和 501(没实现)

405 是告诉你还没有写该 HTTP 方法

koa2 仿知乎笔记_第2张图片

501 是告诉你它还不支持该 HTTP 方法( 比如 Link… )

koa2 仿知乎笔记_第3张图片

获取 HTTP 请求参数

获取 ? 后面的值

ctx.query

获取 路由 参数

ctx.params.id

获取 body 参数

这个需要安装第三方中间件 koa-bodyparser

npm i koa-bodyparser --save

使用 koa-bodyparser

const bodyparser = require("koa-bodyparser")

app.use(bodyparser())

然后再获取

ctx.request.body

获取 header

ctx.header 或者 ctx.headers

更合理的目录结构

koa2 仿知乎笔记_第4张图片

主页

  • app/index.js

    const Koa = require("koa");
    const bodyparser = require("koa-bodyparser");
    const app = new Koa();
    const routing = require("./routes");
    
    app.use(bodyparser());
    routing(app);
    
    app.listen(3000, () => console.log("服务启动成功 - 3000"));
    

路由

  • app/routes/home.js

    const Router = require("koa-router");
    const router = new Router();
    const {
            index } = require("../controllers/home");
    
    router.get("/", index);
    
    module.exports = router;
    

    这里传入类方法作为 router 的回调函数

  • app/routes/users.js

    const Router = require("koa-router");
    const router = new Router({
            prefix: "/users" });
    const {
           
      find,
      findById,
      create,
      update,
      delete: del,
    } = require("../controllers/users");
    
    const db = [{
            name: "李雷" }];
    
    // 获取用户列表 - get
    router.get("/", find);
    
    // 获取指定用户 - get
    router.get("/:id", findById);
    
    // 添加用户 - post
    router.post("/", create);
    
    // 修改用户 - put
    router.put("/:id", update);
    
    // 删除用户 - delete
    router.delete("/:id", del);
    
    module.exports = router;
    
  • app/routes/index.js

    const fs = require("fs");
    module.exports = (app) => {
           
      // console.log(fs.readdirSync(__dirname));
      fs.readdirSync(__dirname).forEach((file) => {
           
        if (file === "index.js") return;
        const route = require(`./${
             file}`);
        app.use(route.routes()).use(route.allowedMethods());
      });
    };
    

    这里把 app.use 的写法封装起来简化

控制器

  • controllers/home.js

    class HomeCtl {
           
      index(ctx) {
           
        ctx.body = "这是主页";
      }
    }
    
    module.exports = new HomeCtl();
    

    使用类和类方法的方法把具体逻辑封装到控制器中

  • controllers/users.js

    const db = [{
            name: "李雷" }];
    
    class UserCtl {
           
      find(ctx) {
           
        ctx.body = db;
      }
      findById(ctx) {
           
        ctx.body = db[ctx.params.id * 1];
      }
      create(ctx) {
           
        db.push(ctx.request.body);
        // 返回添加的用户
        ctx.body = ctx.request.body;
      }
      update(ctx) {
           
        db[ctx.params.id * 1] = ctx.request.body;
        ctx.body = ctx.request.body;
      }
      delete(ctx) {
           
        db.splice(ctx.params.id * 1, 1);
        ctx.status = 204;
      }
    }
    
    module.exports = new UserCtl();
    

自定义防报错中间件

  • app/index.js

    app.use(async (ctx, next) => {
           
      try {
           
        await next();
      } catch (err) {
           
        ctx.status = err.status || err.statusCode || 500;
        ctx.body = {
           
          message: err.message,
        };
      }
    });
    

    此中间件会抛出自定义的错误和运行时错误和服务器内部错误

    但是不能抛出 404 错误

  • app/controllers/users.js

    class UserCtl {
           
      // ...
      findById(ctx) {
           
        if (ctx.params.id * 1 >= db.length) {
           
          ctx.throw(412, "先决条件失败: id 大于等于数组长度了");
        }
        ctx.body = db[ctx.params.id * 1];
      }
      // ...
    }
    

    自定义错误如上,当用户输入的 id 值超出 db 的长度时,会主动抛出 412 错误

使用 koa-json-error

koa-json-error 是一个非常强大的错误处理第三方中间件,可以处理 404 错误,返回堆栈信息等等

在生产环境中不能返回堆栈信息,在开发环境中需要返回堆栈信息

安装

npm i koa-json-error --save

使用

app.use(error({
     
    postFormat: (e, {
      stack, ...rest }) => process.env.NODE_ENV === "production" ? rest : {
      stack, ...rest }
}))

以上代码不需要理解,复制即可

process.env.NODE_ENV - 获取环境变量

production - 代表生产环境

因为需要判断是否是生产环境,所以还需要更改 package.json 文件

  • windows

    需要安装 cross-env

    npm i cross-env --save-dev
    
    "scripts": {
           
        "start": "cross-env NODE_ENV=production node app",
        "dev": "nodemon app"
    },
    
  • mac

    "scripts": {
           
        "start": "NODE_ENV=production node app",
        "dev": "nodemon app"
    },
    

koa-parameter 校验请求参数

安装 koa-parameter

npm i koa-parameter --save

使用 koa-parameter

const parameter = require("koa-parameter");

app.use(parameter(app));

在更新和删除时需要验证

  • app/controllers.users.js

    create(ctx) {
           
        // 请求参数验证
        ctx.verifyParams({
           
            name: {
            type: "string", required: true },
            age: {
            type: "number", required: false },
        });
        // ...
    }
    
    update(ctx) {
           
        // ...
        ctx.verifyParams({
           
            name: {
            type: "string", required: true },
            age: {
            type: "number", required: false },
        });
        // ...
    }
    

为什么要用 NoSQL ?

  • 简单(没有原子性、一致性、隔离性等复杂规范)
  • 便于横向拓展
  • 适合超大规模数据的存储
  • 很灵活地存储复杂结构的数据(Schema Free)

云 MongoDB

  • 阿里云、腾讯云(收费)
  • MongoDB 官方的 MongoDB Atlas(免费 + 收费)

使用 mongoose 连接 云 MongoDB

npm i mongoose
  • app/config.js

    module.exports = {
           
      connectionStr:
        "mongodb+srv://maxiaoyu:@zhihu.irwgy.mongodb.net/?retryWrites=true&w=majority",
    };
    

    password 为你在 云MongoDB 中 Database User 密码

    dbname 为你 Cluster 中的数据库名字

  • app/index.js

    const mongoose = require("mongoose");
    
    const {
            connectionStr } = require("./config");
    
    mongoose.connect(
      connectionStr,
      {
            useNewUrlParser: true, useUnifiedTopology: true },
      () => console.log("MongoDB 连接成功了!")
    );
    mongoose.connection.on("error", console.error);
    

设计用户模块的 Schema

在 app 下新建 models 文件夹,里面写所有的 Schema 模型

  • app/models/users.js

    const mongoose = require("mongoose");
    
    const {
            Schema, model } = mongoose;
    
    const userSchema = new Schema({
           
      name: {
            type: String, required: true },
    });
    
    module.exports = model("User", userSchema);
    

    model 的第一个参数 User 是将要生成的 集合名称

    第二个参数为 Schema 的实例对象,其中定义了数据的类型等

实现用户注册

  • app/models/users.js

    const mongoose = require("mongoose");
    
    const {
            Schema, model } = mongoose;
    
    const userSchema = new Schema({
           
      __v: {
            type: String, select: false },
      name: {
            type: String, required: true },
      password: {
            type: String, required: true, select: false },
    });
    
    module.exports = model("User", userSchema);
    

    select - 是否在查询时显示该字段

  • app/controllers/users.js

    async create(ctx) {
           
        // 请求参数验证
        ctx.verifyParams({
           
            name: {
            type: "string", required: true },
            password: {
            type: "string", required: true },
        });
        const {
            name } = ctx.request.body;
        const repeatedUser = await User.findOne({
            name });
        if (repeatedUser) ctx.throw(409, "用户已经占用");
        const user = await new User(ctx.request.body).save();
        // 返回添加的用户
        ctx.body = user;
    }
    

    409 错误,表示用户已经占用

实现用户登录

  • app/controllers/users.js

    async login(ctx) {
           
        ctx.verifyParams({
           
            username: {
            type: "string", required: true },
            password: {
            type: "string", required: true },
        });
        const user = await User.findOne(ctx.request.body);
        if (!user) ctx.throw(401, "用户名或密码不正确");
        const {
            _id, username } = user;
        const token = jsonwebtoken.sign({
            _id, username }, secret, {
            expiresIn: "1d" });
        ctx.body = {
            token };
    }
    

    登录需要返回 token,可以使用第三方中间件 jsonwebtoken ,简称 JWT

    npm i jsonwebtoken --save
    

    secret - 为 token 密码

    expiresIn - 为过期时间

【自己编写】用户认证与授权

用户登录后返回 token ,从 token 中获取用户信息

  • app/routes/users.js

    // 用户认证中间件
    const auth = async (ctx, next) => {
           
      const {
            authorization = "" } = ctx.request.header;
      const token = authorization.replace("Bearer ", "");
      try {
           
        const user = jsonwebtoken.verify(token, secret);
        ctx.state.user = user;
      } catch (error) {
           
        ctx.throw(401, error.message);
      }
      await next();
    };
    

    verify - 认证 token,然后解密 token,获取到用户信息,将用户信息保存到 ctx.state.user 中

    await next() - 用户认证通过后进行下一步

用户认证通过后,进行用户授权

例如:李雷不能修改韩梅梅的信息,韩梅梅也不能修改李雷的信息

  • app/controllers/users.js

    async checkOwner(ctx, next) {
           
        if (ctx.params.id !== ctx.state.user._id) ctx.throw(403, "没有权限");
        await next();
    }
    
  • 使用(app/routes/users.js)

    // 修改用户 - patch
    router.patch("/:id", auth, checkOwner, update);
    
    // 删除用户 - delete
    router.delete("/:id", auth, checkOwner, del);
    

    认证之后再授权

【第三方】用户认证与授权 koa-jwt

安装

npm i koa-jwt --save

使用

  • app/routes/users.js
const jwt = require("koa-jwt");

const auth = jwt({
      secret });

把 auth 更改一下即可

koa-jwt 内部同样把 user 保存到了 ctx.state.user 中,并且有 await next()

使用 koa-body 中间件获取上传的文件

koa-body 替换 koa-bodyparser

npm i koa-body --save

npm uninstall koa-bodyparser --save

使用

  • app/index.js

    const koaBody = require("koa-body");
    
    app.use(
      koaBody({
           
        multipart: true, // 代表图片格式
        formidable: {
           
          uploadDir: path.join(__dirname, "/public/uploads"), // 指定文件存放路径
          keepExtensions: true, // 保留文件扩展名
        },
      })
    );
    

    这样写就可以在请求上传图片的接口时上传图片了

  • app/controllers/home.js

    upload(ctx) {
           
        const file = ctx.request.files.file;
        // console.log(file);
        ctx.body = {
            path: file.path };
    }
    

    file 为上传文件时的那个参数名

    file.path 可以获取到该图片上传好之后的绝对路径,既然已是绝对路径,那就必然不可,后面将会提供 转成 http 路径的方法

  • app/routes/home.js

    const {
            index, upload } = require("../controllers/home");
    
    router.post("/upload", upload);
    

使用 koa-static 生成图片链接

安装

npm i koa-static --save

使用

const koaStatic = require("koa-static");

app.use(koaStatic(path.join(__dirname, "public")));
  • app/controllers/home.js

    upload(ctx) {
           
        const file = ctx.request.files.file;
        const basename = path.basename(file.path);
        ctx.body = {
            url: `${
             ctx.origin}/uploads/${
             basename}` };
    }
    

    path.basename(绝对路径) - 获取基础路径

    ctx.origin - 获取URL的来源,包括 protocolhost

前端上传图片

<form action="/upload" enctype="multipart/form-data" method="POST">
    <input type="file" name="file" accept="image/*" />
    <button type="submit">上传button>
form>

action - 上传的接口

enctype - 指定上传文件

type - 文件类型 name - 上传的参数名 accept - 指定可以上传所有的图片文件

个人资料的 schema 设计

  • app/models/users.js

    const userSchema = new Schema({
           
      __v: {
            type: String, select: false },
      username: {
            type: String, required: true },
      password: {
            type: String, required: true, select: false },
      avatar_url: {
            type: String },
      gender: {
           
        type: String,
        enum: ["male", "female"],
        default: "male",
        required: true,
      },
      headline: {
            type: String },
      locations: {
            type: [{
            type: String }] },
      business: {
            type: String },
      employments: {
           
        type: [
          {
           
            company: {
            type: String },
            job: {
            type: String },
          },
        ],
      },
      educations: {
           
        type: [
          {
           
            school: {
            type: String },
            major: {
            type: String },
            diploma: {
            type: Number, enum: [1, 2, 3, 4, 5] },
            entrance_year: {
            type: Number },
            graduation_year: {
            type: Number },
          },
        ],
      },
    });
    

    注意:type: [] - 代表数组类型

    enum - 为枚举的数据

    default - 为默认值

字段过滤

把一些不需要返回的字段都加上 select: false 实现了字段隐藏

然后通过 fields 来查询指定的参数

async findById(ctx) {
     
    const {
      fields = "" } = ctx.query;
    const selectFields = fields
    .split(";")
    .filter((f) => f)
    .map((f) => " +" + f)
    .join("");
    const user = await User.findById(ctx.params.id).select(selectFields);
    if (!user) ctx.throw(404, "用户不存在");
    ctx.body = user;
}

split - 把 fileds 的值按 ; 分割成数组

filter - 把空值过滤掉

map - 改变 f 的值,+ 号前必须加上空格

最后用 join(""), 把这个数组中的每个值连接成一个字符串

把这个值传入 select() 函数中即可使用

关注与粉丝的 Schema 设计

  • app/models/users.js

    following: {
           
        type: [{
            type: Schema.Types.ObjectId, ref: "User" }],
            select: false,
    },
    

    这是 mongoose 中的一种模式类型 ,它用主键,而 ref 表示通过使用该主键保存对 User 模型的文档的引用

关注与粉丝接口

获取某人的关注列表/关注某人

  • app/controllers/users.js

    async listFollowing(ctx) {
           
        const user = await User.findById(ctx.params.id)
        .select("+following")
        .populate("following");
    
        if (!user) ctx.throw(404, "用户不存在");
        ctx.body = user.following;
    }
    async follow(ctx) {
           
        const me = await User.findById(ctx.state.user._id).select("+following");
        if (!me.following.map((id) => id.toString()).includes(ctx.params.id)) {
           
            me.following.push(ctx.params.id);
            me.save();
        } else {
           
            ctx.throw(409, "您已关注该用户");
        }
        ctx.status = 204;
    }
    
    1. populate - 代表获取该主键对应的集合数据,由于 following 主键对应的集合为 User ,所以可以获取到 User 中的数据,从而某人的关注列表的详细信息

    2. 由于 following 中的主键 id 为 object 类型(可以自行测试),所以需要使用 map 把数组中的每一项都转换为 字符串 类型,因为这样才能使用 includes 这个方法来判断这个 me.following 数组是否已经包含了你要关注的用户

  • 接口设计(app/routes/users.js)

    const {
           
      // ...
      listFollowing,
      follow,
    } = require("../controllers/users");
    
    // 获取某人的关注列表
    router.get("/:id/following", listFollowing);
    
    // 关注某人
    router.put("/following/:id", auth, follow);
    

    关注某人是在当前登录用户关注某人,所以需要登录认证

获取某人的粉丝列表

  • app/controllers/users.js

    async listFollowers(ctx) {
           
        const users = await User.find({
            following: ctx.params.id });
        ctx.body = users;
    }
    

    following: ctx.params.id - 从 following 中找到包含 查找的 id 的用户

  • app/routes/users.js

    const {
           
      // ...
      listFollowers,
    } = require("../controllers/users");
    
    // 获取某人的粉丝列表
    router.get("/:id/followers", listFollowers);
    

编写校验用户存在与否的中间件

  • app/controllers/users.js

    async checkUserExist(ctx, next) {
           
        const user = await User.findById(ctx.params.id);
        if (!user) ctx.throw(404, "用户不存在");
        await next();
    }
    
  • 使用(app/routes/users.js)

    // 关注某人
    router.put("/following/:id", auth, checkUserExist, follow);
    
    // 取消关注某人
    router.delete("/following/:id", auth, checkUserExist, unfollow);
    

话题 Schema 设计与用户 Schema 改造

  • app/models/topics.js

    const mongoose = require("mongoose");
    
    const {
            Schema, model } = mongoose;
    
    const topicSchema = new Schema({
           
      __v: {
            type: String, select: false },
      name: {
            type: String, required: true },
      avatar_url: {
            type: String },
      introduction: {
            type: String, select: false },
    });
    
    module.exports = model("Topic", topicSchema);
    
  • app/models/users.js

    const mongoose = require("mongoose");
    
    const {
            Schema, model } = mongoose;
    
    const userSchema = new Schema({
           
      __v: {
            type: String, select: false },
      username: {
            type: String, required: true },
      password: {
            type: String, required: true, select: false },
      avatar_url: {
            type: String },
      gender: {
           
        type: String,
        enum: ["male", "female"],
        default: "male",
        required: true,
      },
      headline: {
            type: String },
      locations: {
           
        type: [{
            type: Schema.Types.ObjectId, ref: "Topic" }],
        select: false,
      },
      business: {
            type: Schema.Types.ObjectId, ref: "Topic", select: false },
      employments: {
           
        type: [
          {
           
            company: {
            type: Schema.Types.ObjectId, ref: "Topic" },
            job: {
            type: Schema.Types.ObjectId, ref: "Topic" },
          },
        ],
        select: false,
      },
      educations: {
           
        type: [
          {
           
            school: {
            type: Schema.Types.ObjectId, ref: "Topic" },
            major: {
            type: Schema.Types.ObjectId, ref: "Topic" },
            diploma: {
            type: Number, enum: [1, 2, 3, 4, 5] },
            entrance_year: {
            type: Number },
            graduation_year: {
            type: Number },
          },
        ],
        select: false,
      },
      following: {
           
        type: [{
            type: Schema.Types.ObjectId, ref: "User" }],
        select: false,
      },
    });
    
    module.exports = model("User", userSchema);
    

    将 locations、business、employments 及 educations 中的 school、major 都通过外键(ref)关联到 Topic 集合,至于为什么是关联 Topic 集合,因为它们都需要返回 Topic 集合中的数据,仔细看代码还会发现,following 是 User 自己关联的自己,因为它需要返回 User 自身的数据

问题 Schema 设计

  • app/models/questions.js

    const mongoose = require("mongoose");
    
    const {
            Schema, model } = mongoose;
    
    const questionSchema = new Schema({
           
      __v: {
            type: String, select: false },
      title: {
            type: String, required: true },
      description: {
            type: String },
      questioner: {
            type: Schema.Types.ObjectId, ref: "User", select: false },
    });
    
    module.exports = model("Question", questionSchema);
    

    questioner - 提问者,每个问题只有一个提问者,每个提问者有多个问题

  • 模糊搜索 title 或 description

    async find(ctx) {
           
        const {
            per_page = 10 } = ctx.query;
        const page = Math.max(ctx.query.page * 1, 1);
        const perPage = Math.max(per_page * 1, 1);
        const q = new RegExp(ctx.query.q);
        const question = await Question.find({
           $or: [{
           title: q}, {
           description: q}]})
        .limit(perPage)
        .skip((page - 1) * perPage);
        ctx.body = question;
    }
    

    new RegExp(ctx.query.q) - 模糊搜索包含 ctx.query.q 的问题

    $or - 既能匹配 title, 也能匹配 description

进阶

一个问题下有多个话题(限制)

一个话题下也可以有多个问题(无限)

所以在设计 Schema 时应该把有限的数据放在无限的数据里,防止了数据库爆破

  • app/models/questions.js

    const questionSchema = new Schema({
           
      __v: {
            type: String, select: false },
      title: {
            type: String, required: true },
      description: {
            type: String },
      questioner: {
            type: Schema.Types.ObjectId, ref: "User", select: false },
      topics: {
           
        type: [{
            type: Schema.Types.ObjectId, ref: "Topic" }],
        select: false,
      },
    });
    

    把 topics 放在了问题里面

  • 可以直接很简单的获取到 topics 了(app/controllers/questions.js)

    async findById(ctx) {
           
        const {
            fields = "" } = ctx.query;
        const selectFields = fields
        .split(";")
        .filter((f) => f)
        .map((f) => " +" + f)
        .join("");
        const question = await Question.findById(ctx.params.id)
        .select(selectFields)
        .populate("questioner topics");
        ctx.body = question;
    }
    

    直接在 populate 中添加上 topics 即可

  • 在话题控制器中可以通过查找指定的话题来获取多个问题(app/controllers/topics.js)

    async listQuestions(ctx) {
           
        const questions = await Question.find({
            topics: ctx.params.id });
        ctx.body = questions;
    }
    

    查找出 Question 下的 topics 中包含当前查找的话题 id 的所有问题

  • app/routes/topics.js

    const {
           
      // ...
      listQuestions,
    } = require("../controllers/topics");
    
    // 获取某个话题的问题列表
    router.get("/:id/questions", checkTopicExist, listQuestions);
    

    设计获取某个话题的问题列表的接口

互斥关系的赞踩答案接口设计

  • app/models/users.js

    likingAnswer: {
           
        type: [{
            type: Schema.Types.ObjectId, ref: "Answer" }],
        select: false,
    },
    dislikingAnswer: {
           
        type: [{
            type: Schema.Types.ObjectId, ref: "Answer" }],
        select: false,
    },
    

    赞 / 踩模型设计

  • 控制器

    主要需要注意的就是以下 mongoose 语法

    $inc: {
             需要增加的字段名: 需要增加的数字值 }
    
  • 接口设计(app/routes/users.js)

    // 获取某用户的回答点赞列表
    router.get("/:id/likingAnswers", listLikingAnswers);
    
    // 赞答案(赞了之后取消踩)
    router.put(
      "/likingAnswer/:id",
      auth,
      checkAnswerExist,
      likingAnswer,
      undislikingAnswer
    );
    
    // 取消赞答案
    router.put("/unlikingAnswer/:id", auth, checkAnswerExist, unlikingAnswer);
    
    // 获取某用户的踩答案列表
    router.get("/:id/disLikingAnswers", listDisLikingAnswers);
    
    // 踩答案(踩了之后取消赞)
    router.put(
      "/dislikingAnswer/:id",
      auth,
      checkAnswerExist,
      dislikingAnswer,
      unlikingAnswer
    );
    
    // 取消踩答案
    router.put("/undislikingAnswer/:id", auth, checkAnswerExist, undislikingAnswer);
    

    赞踩互斥主要就是通过在这里写的,赞了之后取消踩,踩了之后取消赞

二级评论 Schema 设计

  • app/models/comments.js

    const commentSchema = new Schema({
           
      __v: {
            type: String, select: false },
      content: {
            type: String, required: true },
      commentator: {
            type: Schema.Types.ObjectId, ref: "User", select: false },
      questionId: {
            type: String, required: true },
      answerId: {
            type: String, required: true },
      rootCommentId: {
            type: String },
      replyTo: {
            type: Schema.Types.ObjectId, ref: "User" },
    });
    

    其实就是在一级评论的基础上添加了两行代码就实现了二级评论,并且是一级评论和二级评论共用一接口

    rootCommentId - 根评论 Id, 也就是你要回复的评论 id

    replyTo - 回复评论的用户,此字段为主键,直接关联 User 集合

  • app/controllers/comments.js

    const Comment = require("../models/comments");
    
    class UserCtl {
           
      async find(ctx) {
           
        const {
            per_page = 10 } = ctx.query;
        const page = Math.max(ctx.query.page * 1, 1);
        const perPage = Math.max(per_page * 1, 1);
        const q = new RegExp(ctx.query.q);
        const {
            questionId, answerId } = ctx.params;
        const {
            rootCommentId } = ctx.query;
        const comment = await Comment.find({
           
          content: q,
          questionId,
          answerId,
          rootCommentId,
        })
          .limit(perPage)
          .skip((page - 1) * perPage)
          .populate("commentator replyTo");
        ctx.body = comment;
      }
      async checkCommentExist(ctx, next) {
           
        const comment = await Comment.findById(ctx.params.id).select(
          "+commentator"
        );
        ctx.state.comment = comment;
        if (!comment) ctx.throw(404, "评论不存在");
        // 只有删改查答案时才检查此逻辑,赞、踩答案时不检查
        if (ctx.params.questionId && comment.questionId !== ctx.params.questionId)
          ctx.throw(404, "该问题下没有此评论");
        if (ctx.params.answerId && comment.answerId !== ctx.params.answerId)
          ctx.throw(404, "该答案下没有此评论");
        await next();
      }
      async findById(ctx) {
           
        const {
            fields = "" } = ctx.query;
        const selectFields = fields
          .split(";")
          .filter((f) => f)
          .map((f) => " +" + f)
          .join("");
        const comment = await Comment.findById(ctx.params.id)
          .select(selectFields)
          .populate("commentator");
        ctx.body = comment;
      }
      async create(ctx) {
           
        // 请求参数验证
        ctx.verifyParams({
           
          content: {
            type: "string", required: true },
          rootCommentId: {
            type: "string", required: false },
          replyTo: {
            type: "string", required: false },
        });
        const commentator = ctx.state.user._id;
        const {
            questionId, answerId } = ctx.params;
        const comment = await new Comment({
           
          ...ctx.request.body,
          commentator,
          questionId,
          answerId,
        }).save();
        // 返回添加的话题
        ctx.body = comment;
      }
      async checkCommentator(ctx, next) {
           
        const {
            comment } = ctx.state;
        if (comment.commentator.toString() !== ctx.state.user._id)
          ctx.throw(403, "没有权限");
        await next();
      }
      async update(ctx) {
           
        ctx.verifyParams({
           
          content: {
            type: "string", required: false },
        });
        const {
            content } = ctx.request.body;
        await ctx.state.comment.update(content);
        ctx.body = ctx.state.comment;
      }
      async delete(ctx) {
           
        await Comment.findByIdAndRemove(ctx.params.id);
        ctx.status = 204;
      }
    }
    
    module.exports = new UserCtl();
    

    这是评论控制器

    首先是 find

    • 实现了是查找一级评论还是查找二级评论的功能 - const { rootCommentId } = ctx.query; 在请求时你不写这个 rootCommentId 参数即是查找一级评论,写了则是查找二级评论
    • 另外在 populate 中接收了评论者(commentator)和回复者(replyTo)

    然后是检查评论是否存在

    • 如果评论不存在则做出相应的提示
    • 如果评论存在,则直接放行

    然后是根据 评论id 查找评论

    • 这个就是单纯的查找一条评论了,因为 populate 中返回了 commentator,所以不会返回 replyTo
    • 需要注意的是,如果返回结果中没有 rootCommentId, 则该条评论为一级评论,如果有 rootCommentId,则该条评论为二级评论

    然后是添加评论

    • 其中有 content 参数,为必选参数,如果在添加评论时只写了该参数,则会添加一级评论
    • 还有 rootCommentId、replyTo 两个可选参数,如果写上这俩,则会添加二级评论

    然后是检查评论者是不是自己

    • 如果不是自己,则无法修改评论和删除评论

    然后就是修改评论

    • 只能修改评论内容(content),而不能修改当前评论回复的那个评论的 id(rootCommentId) 和评论者(replyTo),否则就会导致驴唇不对马嘴的结果!

    最后就是删除评论

    • 删除当前评论,回复的评论不会被删除

mongoose 如何优雅的加上日期

只需一行代码

在 Schema 的第二个参数中加上 { timestamps: true } 即可

例如:

const commentSchema = new Schema(
  {
     
    __v: {
      type: String, select: false },
    content: {
      type: String, required: true },
    commentator: {
      type: Schema.Types.ObjectId, ref: "User", select: false },
    questionId: {
      type: String, required: true },
    answerId: {
      type: String, required: true },
    rootCommentId: {
      type: String },
    replyTo: {
      type: Schema.Types.ObjectId, ref: "User" },
  },
  {
      timestamps: true }
);

mongoose 如何返回更新后的数据

要实现这个需要使用 findByIdAndUpdate 配合 {new: true} 来完成

具体用法如下:

const comment = await Comment.findByIdAndUpdate(
    ctx.params.id,
    {
      content },
    {
      new: true }
);

总结

RESTful API 设计参考 GitHub API v3

v3 版本可谓是 API 的教科书

使用 GitHub 搜索 Koa2 资源

使用 Stack Overflow 搜索问题

比如说你不知道如何用 MongoDb 设计关注与粉丝的表结构,你就可以使用 Stack Overflow 来搜索这个问题(不过记得要翻译成英文)

  • 拓展建议
    • 使用企业级 Node.js 框架 —— Egg.js
    • 掌握多进程编程知识
    • 学习使用日志和性能监控

你可能感兴趣的:(Koa,koa)