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 的作用
- 響應 options 的方法,告訴它所支援的請求方法
app.use(router.allowedMethods());
加上它,使該介面支援了 options 請求
- 相應地返回 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, name } = user; const token = jsonwebtoken.sign({ _id, name }, secret, { expiresIn: "1d" }); ctx.body = { token }; }
登入需要返回 token,可以使用第三方中介軟體
jsonwebtoken
,簡稱 JWTnpm 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/upload"), // 指定檔案存放路徑 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的來源,包括
protocol
和host
。
前端上傳圖片
<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; }
-
populate - 代表獲取該主鍵對應的集合資料,由於 following 主鍵對應的集合為 User ,所以可以獲取到 User 中的資料,從而某人的關注列表的詳細資訊
-
由於 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
- 掌握多程式程式設計知識
- 學習使用日誌和效能監控
數,則會新增一級評論
- 還有 rootCommentId、replyTo 兩個可選引數,如果寫上這倆,則會新增二級評論
然後是檢查評論者是不是自己
- 如果不是自己,則無法修改評論和刪除評論
然後就是修改評論
- 只能修改評論內容(content),而不能修改當前評論回覆的那個評論的 id(rootCommentId) 和評論者(replyTo),否則就會導致驢脣不對馬嘴的結果!
最後就是刪除評論
- 刪除當前評論,回覆的評論不會被刪除
相關文章
- 仿知乎分享介面
- 邏輯學筆記-知乎筆記
- koa2學習筆記筆記
- 仿知乎日報androidAndroid
- Koa2進階學習筆記筆記
- Vue3.0+TS仿知乎專欄Vue
- 仿知乎首頁學習 CoordinateLayout
- Android 仿知乎分享控制元件Android控制元件
- Vue知乎日報的學習筆記Vue筆記
- 仿知乎拖動廣告的實現iOSiOS
- WPF製作的小型筆記本-仿有道雲筆記筆記
- swift實現仿知乎搖一搖彈出框Swift
- 登陸註冊頁面html程式碼(仿知乎)HTML
- 仿Windows計算器--學習筆記Windows筆記
- koa2實戰個人筆記(二)——模板渲染引擎ejs筆記JS
- Android 仿知乎創意廣告 廣告還能這麼玩?Android
- Python爬蟲學習筆記(1)爬取知乎使用者資訊Python爬蟲筆記
- Vue3.0(正式版) + TS 仿知乎專欄企業級專案Vue
- Android仿微信介面--使用Fragment實現(慕課網筆記)AndroidFragment筆記
- 仿知乎+微博系統原始碼 帶打賞功能+內容付費問答原始碼
- 安卓日記——可快取的知乎日報安卓快取
- 尋找工作仿騙記錄
- 仿Windows畫板噴漆筆刷效果Windows
- Flutter學習筆記--仿閒魚底部導航欄帶有中間凸起圖示Flutter筆記
- Koa2基礎
- 知乎iOS面試題iOS面試題
- 帶你走進 koa2 的世界(koa2 原始碼淺談)原始碼
- 印象筆記 --- 方法分享筆記筆記
- koa2原始碼解析原始碼
- 知乎引流軟體?小帥說說自己對知乎軟體的理解
- 知乎如何複製自己的答案?知乎複製文字的方法教程
- Android知乎廣告效果Android
- Laravel+VueJs 知乎LaravelVueJS
- 筆記筆記
- 使用webpack打包koa2 appWebAPP
- 知乎運營怎麼搞?開展知乎營銷的流程和形式
- CUUG筆記 ORACLE索引學習筆記筆記Oracle索引
- 主動筆記與被動筆記筆記