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 请求

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

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

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

获取 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

更合理的目录结构

主页

  • 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:<password>@zhihu.irwgy.mongodb.net/<dbname>?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 --savenpm 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
    • 掌握多进程编程知识
    • 学习使用日志和性能监控

koa2 仿知乎笔记相关推荐

  1. Koa2仿知乎服务端项目:Webpack配置

    项目简介 该项目为一个后端项目,该项目仿"知乎",模拟实现了: JWT用户认证.授权模块 上传图片模块 个人资料模块 关注与粉丝模块 话题模块 问题模块 答案模块 评论模块 共计4 ...

  2. Koa 2 基础(仿知乎)

    Koa 2 基础 接口文档 Postman仿知乎在线测试 REST 简介 REST是什么 REST是Resource Representational State Transfer的缩写,是一种Web ...

  3. node.js仿知乎

    所需: restful API理论 koa2 mongodb jwt postman 1.restful API的6个限制和若干最佳实践 2.Koa2,Postman,MongoDB,JWT等技术 3 ...

  4. android仿知乎按钮动效,Android仿知乎客户端关注和取消关注的按钮点击特效实现思路详解...

    先说明一下,项目代码已上传至github,不想看长篇大论的也可以先去下代码,对照代码,哪里不懂点哪里. 代码在这https://github.com/zgzczzw/ZHFollowButton 前几 ...

  5. Android 仿知乎创意广告

    代码地址如下: http://www.demodashi.com/demo/14904.html ###一.概述 貌似前段时间刷知乎看到的一种非常有特色的广告展现方式,即在列表页,某一个Item显示背 ...

  6. canvasnest 移动距离_GitHub - XiaoxinJiang/canvas-nest: 仿知乎登录页面canvas-nest

    canvas-nest 仿知乎登录页面canvas-nest 首先上效果图: 因为使用gif图片的原因,线条不是很清晰,大家可以到我的博客观看效果:http://cherryblog.site/ ,( ...

  7. java开发社交网站_仿知乎问答社交平台网站

    zhihu仿知乎问答社交平台简介 这是一个仿知乎的问答社交平台网站,界面与基本功能均仿照知乎.目前实现包括注册,提问,回答,点赞,关注,私信等功能. 技术选型 后端 核心框架:Spring Frame ...

  8. 仿知乎客户端的白天黑夜主题切换

    仿知乎客户端的白天黑夜主题切换 转载请注明出处 作者:AboutJoke ( http://blog.csdn.net/u013200308 ) 原文链接:http://blog.csdn.net/u ...

  9. 高仿知乎android,Android高仿知乎首页Behavior

    Android自定义Behavior实现跟随手势滑动,显示隐藏标题栏.底部导航栏及悬浮按钮 Android Design包下的CoordinatorLayout是相当重要的一个控件,它让许多动画的实现 ...

最新文章

  1. SQLServer中Case的用法
  2. int p 和int p
  3. IDEA中如何设置方法注释格式
  4. WPF 重要新概念读书笔记(转)
  5. centos7根据端口查进程_记录一次CentOs7下Nginx+WSGI部署Django项目(超详细)
  6. 2017年Spring发布了30个新的Android库,值得您关注
  7. mysql 自定义序列号_在mysql中怎样设置,才能自动添加序列号
  8. Java如何实现后端分页
  9. netty的channel介绍
  10. React入门基础+练习(二)
  11. 揭秘2017双11背后的网络-双11的网络产品和技术概览
  12. 通过angularjs的directive以及service来实现的列表页加载排序分页
  13. 费率转换成利率的计算器_存款利率计算器
  14. Proteus仿真:简易独立式键盘
  15. linux安装CAS认证服务器
  16. Python将base64编码转换为图片并存储
  17. 深圳腾讯地图地铁站经纬度
  18. JasperReport导出Excel锁定行或列
  19. 台当局死磕美国Uber
  20. dp 最佳加法表达式

热门文章

  1. hikari数据源配置类_SpringBoot多数据源配置详解
  2. java确认rabbitmq_RabbitMQ的消息确认模式
  3. centos7 mysql 5.5.27_centos7上安装mysql-5.7.27
  4. 渗透测试入门27之渗透测试学习建议
  5. 计算机能帮助我学英语翻译,英语翻译以下几个句子,帮忙把汉语翻译成英语,请不要用软件翻!1、计算机能帮助人们从事复杂的计算.几十年前可能需要数月完成...
  6. double 保留两位小数
  7. nginx 4层代理配置
  8. day3—python——字符串常用方法
  9. Eclipse创建SpringMVC,Spring, Hibernate项目
  10. [转载]手工安全测试方法修改建议