Koa学习笔记

RESTful API

请求规范
  • URI 使用名词,尽量使用复数,如/users

  • URI 使用嵌套表示关联关系 如/users/12/repos/5

  • 使用正确的 HTTP 方法,如 GET、POST、PUT、DELETE

响应规范
  • 查询
  • 状态码
  • 分页
  • 字段过滤
  • 错误处理
安全
  • HTTPS
  • 鉴权
  • 限流
CURD
  • GET 返回 200

  • POST 返回新建对象,PATCH 更新部分属性,返回修改对象

  • PUT 返回修改对象

  • DELETE 返回 204

断点调试

先打上断点,然后按 fn + f5 运行项目

对于常用的变量可以add to watch方便以后查找

Koa请求参数

Koa 获取参数有两种方式,一种是从上下文对象中直接获取,另一种是从上下文的request对象中获取。请求参数主要包含以下内容

  • Query String,如q=100

从上下文中获取query参数

获取请求对象 ctx.query返回{q: 100}

获取请求字符串 ctx.querystring返回q=100

从上下文的request对象中获取参数

获取请求对象 ctx.request.query返回{q: 100}

获取请求字符串 ctx.request.querystring返回{q: 100}

  • Router Params,如/user/:id

获取路由中的参数ctx.request.params.id

  • Body

获取请求体可以借助第三方中间件来,如koa-body或者koa-bodyparser,使用过程可以参考这篇文章

  • Header, 如 Accept、Cookie

获取请求头可以通过ctx.request.headers

Koa发送响应

status

设置响应状态码使用ctx.response.status = 200,如果状态码没有被设置,那么koa会自动设置为200204

各种场景对应的状态码如下

100 "continue"
101 "switching protocols"
102 "processing"
200 "ok"
201 "created"
202 "accepted"
203 "non-authoritative information"
204 "no content"
205 "reset content"
206 "partial content"
207 "multi-status"
208 "already reported"
226 "im used"
300 "multiple choices"
301 "moved permanently"
302 "found"
303 "see other"
304 "not modified"
305 "use proxy"
307 "temporary redirect"
308 "permanent redirect"
400 "bad request"
401 "unauthorized"
402 "payment required"
403 "forbidden"
404 "not found"
405 "method not allowed"
406 "not acceptable"
407 "proxy authentication required"
408 "request timeout"
409 "conflict"
410 "gone"
411 "length required"
412 "precondition failed"
413 "payload too large"
414 "uri too long"
415 "unsupported media type"
416 "range not satisfiable"
417 "expectation failed"
418 "I'm a teapot"
422 "unprocessable entity"
423 "locked"
424 "failed dependency"
426 "upgrade required"
428 "precondition required"
429 "too many requests"
431 "request header fields too large"
500 "internal server error"
501 "not implemented"
502 "bad gateway"
503 "service unavailable"
504 "gateway timeout"
505 "http version not supported"
506 "variant also negotiates"
507 "insufficient storage"
508 "loop detected"
510 "not extended"
511 "network authentication required"
message

响应状态消息一般和状态码有关,设置方式是ctx.response.message = 'something was wrong.'

body

ctx.response.body = {},其中响应体可以是以下类型

  • String 类型
  • Buffer 流
  • Stream 管道
  • Object || Array JSON
  • null 无内容
Body类型 Content-Type
String text/html 或 text/plain
Buffer application/octet-stream
Stream application/octet-stream
Object || Array application/json
header

set设置响应头,也可以使用.语法

ctx.set({
  'Etag': '1234',
  'Last-Modified': date
});

设置响应的Content-Type

ctx.type = 'text/plain; charset=utf-8';
ctx.type = 'image/png';
ctx.type = '.png';
ctx.type = 'png';

编写控制器最佳实践

处理请求、处理业务、返回响应

  • 每个资源的控制器放在不同的文件里

  • 尽量使用类+类方法的形式编写控制器

  • 严谨的错误处理

目录结构

  • 按照功能??榻星?/li>
  • 路由压缩 koa-combine-routers
  • 静态资源 koa-static
.
|____LICENSE
|____README.md
|____.gitignore
|____package-lock.json
|____package.json
|____src
| |____middlewares
| | |____check.js
| |____models
| | |____Question.js
| | |____User.js
| | |____Answer.js
| | |____Comment.js
| | |____Topic.js
| |____config.js
| |____public
| | |____index.html
| | |____uploads
| | | |____.gitkeep
| |____controllers
| | |____questions.js
| | |____users.js
| | |____comments.js
| | |____common.js
| | |____topics.js
| | |____answers.js
| |____routes
| | |____questions.js
| | |____users.js
| | |____index.js
| | |____comments.js
| | |____common.js
| | |____topics.js
| | |____answers.js
| |____app.js

Koa高级

中间件
const Koa = require('koa')
const app = new Koa()
const KoaRouter = require('koa-router')
const router = new KoaRouter()

const middleware1 = async function (ctx, next) {
  await next()
  console.log('this is middleware1')
}
const middleware2 = async function (ctx, next) {
  console.log('this is middleware2')
  await next()
}
const middleware3 = async function (ctx, next) {
  console.log('this is middleware3')
  await next()
}
router.get('/', middleware1, middleware2, middleware3, (ctx) => {
  ctx.body = 'ok'
})

app.use(router.routes()).use(router.allowedMethods())

app.listen(5000)

middleware出现次序依次执行,遇到next()直接执行下一个中间件,执行完成后返回到之前代码继续执行

异常处理

常见错误类型

  • 运行时错误,500
  • 逻辑错误,找不到404、先决条件失败412,如缺少参数,无法处理实体422,如参数格式不对

为什么要有错误信息

  • 防止程序挂掉

  • 告诉用户错误信息

  • 便于开发调试

koa自带错误处理

koa会自动处理类似404,500等错误,并返回错误信息

自定义中间件处理错误
// 支持捕获主动抛出错误和运行时错误,不能捕获 404
app.use(async (ctx, next) => {
  try {
    // 如果 next 的代码出错,会被 catch 捕获
    await next();
  } catch (error) {
    ctx.status = error.status || error.statusCode || 500;
    ctx.body = {
      message: error.message,
    };
  }
});
koa-json-error处理错误

安装

npm i koa-json-error

区分生产环境和开发环境

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "serve": "nodemon src/app",
  "start": "NODE_ENV=production node src/app"
},

windows环境可能需要安装cross-env来配置环境变量

配置koa-json-error,让其在生产环境下不打印堆栈信息

const error = require("koa-json-error");
// 在生产环境不返回错误堆栈信息
app.use(
  error({
    postFormat: (error, { stack, ...rest }) =>
      process.env.NODE_ENV === "production" ? rest : { stack, ...rest },
  })
);
参数校验

koa-parameter校验参数

安装

npm i koa-parameter 

使用

app.js

app.use(parameter(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;
}
解析请求体

安装

npm i koa-body

使用

app.js

const KoaBody = require("koa-body");
// 解析 body
app.use(
  KoaBody({
    multipart: true,
    formidable: {
      uploadDir: path.join(__dirname, "public/uploads"),
      keepExtensions: true,
    },
  })
);
跨域
npm i @koa/cors

app.js

const KoaCors = require('@koa/cors')
app.use(KoaCors())
格式化响应
npm i koa-json

app.js

const KoaJson = require('koa-json')
// 默认关闭 pretty , 只有当 query 参数中含有 pretty 时才会返回格式化参数
app.use(KoaJson({ pretty: false, param: 'pretty' }))
合并路由
npm i koa-combine-routers

app.js

const Koa = require('koa')
const router = require('./routes')
const app = new Koa()
app.use(router())

routes/index.js

const Router = require('koa-router')
const combineRouters = require('koa-combine-routers')
 
const dogRouter = new Router()
const catRouter = new Router()
 
dogRouter.get('/dogs', async ctx => {
  ctx.body = 'ok'
})
 
catRouter.get('/cats', async ctx => {
  ctx.body = 'ok'
})
 
const router = combineRouters(
  dogRouter,
  catRouter
)
 
module.exports = router
安全
npm i koa-helmet

app.js

const Koa = require('koa')
const KoaHelmet = require('koa-helmet')
const app = new Koa()
app.use(KoaHelmet())
静态文件
npm i koa-static

app.js

const Koa = require('koa')
const path = require('path')
const KoaStatic = require('koa-static')
const app = new Koa()
app.use(KoaStatic(path.join(__dirname, '../public')))
es6&webpack
npm i -D webpack webpack-cli
npm i -D clean-webpack-plugin webpack-node-externals @babel/core @babel/node @babel/preset-env babel-loader cross-env
  • clean-webpack-plugin 清理 dist 文件夹
  • webpack-node-externals 排除 node_modules
  • babel 支持es6语法

webpack.config.js

const path = require('path')
const nodeExternals = require('webpack-node-externals')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

const webpackconfig = {
  target: 'node',
  mode: 'development',
  entry: {
    server: path.join(__dirname, 'src/app.js')
  },
  output: {
    filename: '[name].bundle.js',
    path: path.join(__dirname, 'dist')
  },
  devtool: 'eval-source-map',
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: {
          loader: 'babel-loader'
        },
        exclude: [path.join(__dirname, 'node_modules')]
      }
    ]
  },
  externals: [nodeExternals()],
  plugins: [new CleanWebpackPlugin()],
  node: {
    console: true,
    global: true,
    process: true,
    Buffer: true,
    __filename: true,
    __dirname: true,
    setImmediate: true,
    path: true
  }
}

module.exports = webpackconfig

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

package.json

"scripts": {
  "serve": "nodemon --exec babel-node src/app.js"
},

调试webpack配置

  • Chrome 调试

运行

node --inspect-brk ./node_modules/.bin/webpack --inline --progress

浏览器访问chrome://inspect/#devices,点击inspect进入调试页面

为了调试方便,可以在package.json文件中加入如下命令

"scripts": {
  "webpack:debug": "node --inspect-brk ./node_modules/.bin/webpack --inline --progress"
},

之后调试webpack只需要运行npm run webpack:debug

  • VSCode 调试

configurations中输入nodemon,选择nodemon setup快速生成配置样板

.vscode/launch.json

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "name": "nodemon",
      "program": "${workspaceFolder}/src/app.js",
      "request": "launch",
      "restart": true,
      "runtimeExecutable": "nodemon",
      "skipFiles": ["<node_internals>/**"],
      "type": "pwa-node",
      "runtimeArgs": ["--exec", "babel-node"]
    }
  ]
}

program指定入口文件,指定runtimeArgs是因为使用了es6语法

数据库
连接数据库

app.js

const mongoose = require("mongoose");
const { connectionStr } = require("./config");

mongoose.connect(
  connectionStr,
  { useUnifiedTopology: true, useNewUrlParser: true },
  () => console.log("connect")
);
mongoose.connection.on("error", console.error);
定义Schema

models/User.js

const mongoose = require("mongoose");

const { Schema, model } = mongoose;
const userSchema = new Schema(
  {
    name: { type: String, required: true },
    // select 为 false 时查询时默认隐藏
    password: {
      type: String,
      required: true,
      select: false,
      set(val) {
        return require("bcrypt").hashSync(val, 10);
      },
    },
    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,
    },
    // 关注话题
    followingTopics: {
      type: [{ type: Schema.Types.ObjectId, ref: "Topic" }],
      select: false,
    },
    // 关注问题
    followingQuestions: {
      type: [{ type: Schema.Types.ObjectId, ref: "Question" }],
      select: false,
    },

    likingAnswers: {
      type: [
        {
          type: Schema.Types.ObjectId,
          ref: "Answer",
        },
      ],
      select: false,
    },
    dislikingAnswers: {
      type: [
        {
          type: Schema.Types.ObjectId,
          ref: "Answer",
        },
      ],
      select: false,
    },
    collectingAnswers: {
      type: [
        {
          type: Schema.Types.ObjectId,
          ref: "Answer",
        },
      ],
      select: false,
    },
  },
  {
    timestamps: true,
  }
);

module.exports = model("User", userSchema);
字段过滤

让一些字段默认不显示,方法是修改schema,在默认不显示的字段上添加select:false。在查询时如果需要这个字段,需要加上select("+fieldName")

权限认证

自定义中间件完成权限认证

controllers/users.js

const jwt = require("jsonwebtoken");
const User = require("../models/User");
const { secret } = require("../config");
const bcrypt = require("bcrypt");
class UsersCtl {

  // 登录
  async login(ctx) {
    const { name, password } = ctx.request.body;
    const user = await User.findOne({ name }).select("+password");
    if (!user) {
      ctx.throw(422, "用户不存在");
    }
    const isValid = bcrypt.compareSync(password, user.password);
    if (!isValid) {
      ctx.throw(422, "用户密码错误");
    }

    const token = jwt.sign({ _id: user._id, name }, secret, {
      expiresIn: "1d",
    });
    ctx.body = { token };
  }

  // 检查权限,只允许修改本人信息
  async checkOwner(ctx, next) {
    if (ctx.params.id !== ctx.state.user._id) {
      ctx.throw(403, "没有权限");
    }
    await next();
  }
}
module.exports = new UsersCtl();

routes/users.js

const Router = require("koa-router");
const router = new Router({ prefix: "/users" });
const jwt = require("jsonwebtoken");
const { secret } = require("../config");
const {
  find,
  findById,
  create,
  update,
  del,
  login,
  checkOwner,
} = require("../controllers/users");

const auth = async (ctx, next) => {
  const { authorization = "" } = ctx.request.header;
  const token = authorization.replace("Bearer ", "");
  try {
    const user = jwt.verify(token, secret);
    ctx.state.user = user;
  } catch (error) {
    ctx.throw(401, error.message);
  }
  await next();
};

router.get("/", find);
router.get("/:id", findById);
router.post("/", create);
router.patch("/:id", auth, checkOwner, update);
router.delete("/:id", auth, checkOwner, del);
router.post("/login", login);

module.exports = router;
基于koa-jwt实现权限认证

安装

npm i koa-jwt

使用

const Router = require("koa-router");
const router = new Router({ prefix: "/users" });
// const jwt = require("jsonwebtoken");
const jwt = require("koa-jwt");
const { secret } = require("../config");
const {
  find,
  findById,
  create,
  update,
  del,
  login,
  checkOwner,
} = require("../controllers/users");

const auth = jwt({ secret });

// 获取用户列表
router.get("/", find);
// 获取用户详情
router.get("/:id", findById);
// 创建用户
router.post("/", create);
// 更新用户 patch 可以更新部分
router.patch("/", auth, update);
// 删除用户
router.delete("/", auth, del);
// 登录
router.post("/login", login);

module.exports = router;

上传图片

主要功能

基础功能:图片上传、生成图片链接

附加功能:限制上传图片的大小与类型、生成高中低三种分辨率的图片链接、生成CDN

代码实现

安装

npm i koa-body
npm i koa-static

app.js

const KoaBody = require("koa-body");
const serve = require("koa-static");

// 静态资源目录
app.use(serve(path.join(__dirname, "public")));
// 解析 body
app.use(
  KoaBody({
    multipart: true,
    formidable: {
      uploadDir: path.join(__dirname, "public/uploads"),
      keepExtensions: true,
    },
  })
);

controllers/common.js

// Content-Type 为 multipart/form-data 才会被解析
upload(ctx) {
  const file = ctx.request.files.file;
  const basename = path.basename(file.path);
  ctx.body = {
    url: `${ctx.origin}/uploads/${basename}`,
  };
}

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <form action="/upload" enctype="multipart/form-data" method="POST">
      <!-- 必须指定 name 否则后端获取不到文件 -->
      <input type="file" name="file" accept="image/*" />
      <button type="submit">上传</button>
    </form>
  </body>
</html>

accept可以指定允许上传的图片类型,比如image/gif, image/jpeg,多种类型用逗号隔开

分页和模糊搜索

controllers/questions.js

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

分页搜索的关键是skiplimit这两个查询参数,模糊搜索则使用了正则表达式

功能列表

用户
  • 用户列表
  • 用户详情
  • 创建用户
  • 更新用户
  • 删除用户
  • 登录
  • 获取已关注用户列表
  • 获取用户粉丝列表
  • 关注用户
  • 取消关注
  • 用户关注话题列表
  • 用户关注话题
  • 用户取消关注话题
  • 用户关注问题列表
  • 用户问题列表(用户-问题的一对多关系)
  • 用户关注问题
  • 用户取消关注问题
  • 用户赞同答案列表
  • 用户赞同答案
  • 用户取消赞同
  • 用户不赞同答案列表
  • 用户不赞同答案
  • 用户取消不赞同
  • 用户收藏答案列表
  • 用户收藏答案
  • 用户取消收藏
话题
  • 获取话题列表
  • 话题详情
  • 创建话题
  • 修改话题
  • 删除话题
  • 获取话题粉丝列表
  • 获取话题的问题列表
问题
  • 问题列表

  • 创建问题

  • 修改问题

  • 删除问题

  • 问题详情

  • 问题的话题列表(问题-话题多对多关系)

  • 问题的关注列表(问题-用户多对多关系)

回答
  • 回答列表
  • 创建回答(问题-答案/用户-答案一对多)
  • 回答详情
  • 修改回答
  • 删除回答
评论
  • 获取评论列表
  • 发表评论(答案-评论/问题-评论/用户-评论一对多、多级评论)
  • 获取评论详情
  • 修改评论
  • 删除评论

API文档

登录
接口:
POST /users/login
参数:
参数名 参数类型 参数位置 描述
name string body 用户名,必填
password string body 密码,必填
响应:
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZjcxZDNkNDk4ZDRkYzBkZTEzMjAwZjUiLCJuYW1lIjoiYWxmYWxmYXciLCJpYXQiOjE2MDE0NjExNTMsImV4cCI6MTYwMTU0NzU1M30.HairtegFaKmtiFOyIZ7iWche11L8dlZ614bjynRhOWs"
}
获取用户列表
接口:
GET /users
参数:
参数名 参数类型 参数位置 描述
per_page number query 可选,每页大小
page number query 可选,当前所在页码
响应:
[
  {
    "gender": "male",
    "_id": "5f71d3d498d4dc0de13200f5",
    "name": "alfalfaw",
    "avatar_url": "test_url",
    "__v": 3
  }
]
获取用户详情
接口:
GET /users/:id
参数:
参数名 参数类型 参数位置 描述
id string path 必选,用户id
fields string query 可选,需要增加到返回结果中的字段??裳∠?code>locations business employments educations following followingTopics likingAnswers dislikingAnswers collectingAnswers
响应:
{
  "gender": "male",
  "locations": [],
  "following": [],
  "followingTopics": [
    {
      "_id": "5f71ddf84557410e40278a2e",
      "name": "水木年华",
      "avatar_url": "test_url",
      "__v": 0
    }
  ],
  "likingAnswers": [
    {
      "voteCount": 0,
      "_id": "5f771b1b1755574dbb6f5b0b",
      "content": "这是答案",
      "answerer": "5f71d3d498d4dc0de13200f5",
      "questionId": "5f734af88519d353e0dcea03",
      "createdAt": "2020-10-02T12:20:43.530Z",
      "updatedAt": "2020-10-02T12:30:28.505Z",
      "__v": 0
    }
  ],
  "dislikingAnswers": [],
  "collectingAnswers": [],
  "_id": "5f71d3d498d4dc0de13200f5",
  "name": "alfalfaw",
  "avatar_url": "test_url",
  "__v": 10,
  "updatedAt": "2020-10-02T13:00:53.472Z",
  "createdAt": "2020-10-02T10:52:46.202Z"
}
创建用户
接口:
POST /users
参数:
参数名 参数类型 参数位置 描述
name string body 必须且唯一,用户名
password string body 必须,用户密码
响应:
{
  "gender": "male",
  "locations": [],
  "following": [],
  "followingTopics": [],
  "likingAnswers": [],
  "dislikingAnswers": [],
  "collectingAnswers": [],
  "_id": "5f76d22e58edc146378be327",
  "name": "alfalfaw2",
  "password": "$2b$10$jNfWo9MYKKJs9N4xUzv9WeumlSProJxQ7L5TBMDPBrQ9VjJXm1ZD.",
  "employments": [],
  "educations": [],
  "createdAt": "2020-10-02T07:09:35.017Z",
  "updatedAt": "2020-10-02T07:09:35.017Z",
  "__v": 0
}
修改用户资料
接口:
PATCH /users
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必须,token参数
name string body 可选,用户名
password string body 可选,密码
avatar_url string body 可选,头像
gender enum body 可选,性别。'male'或者'female'
headline string body 可选,简介
locations array body 可选,地点
business object body 可选,行业
employments array body 可选,职业经历
educations array body 可选,教育经历
following array body 可选,关注的人
followingTopics array body 可选,关注的话题
likingAnswers array body 可选,喜欢的答案
dislikingAnswers array body 可选,不喜欢的答案
collectingAnswers array body 可选,收藏的答案
响应:
{
  "gender": "male",
  "_id": "5f71d3d498d4dc0de13200f5",
  "name": "alfalfaw",
  "avatar_url": "test_url",
  "__v": 10,
  "updatedAt": "2020-10-02T13:05:26.961Z",
  "createdAt": "2020-10-02T10:52:46.202Z"
}
删除用户
接口:
DELETE /users
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必须,token参数
响应:
status 204
获取已关注用户列表
接口:
GET /users/:id/following
参数:
参数名 参数类型 参数位置 描述
id string path 必选,用户id
响应:
[
  {
    "gender": "male",
    "_id": "5f7705281d073d487d312571",
    "name": "tom",
    "createdAt": "2020-10-02T10:47:04.147Z",
    "updatedAt": "2020-10-02T10:47:04.147Z",
    "__v": 0
  }
]
关注用户
接口:
PUT /users/following/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必须,token参数
id string path 必须,被关注人的id
响应:
status 204
获取用户粉丝列表
接口:
GET /users/:id/followers
参数:
参数名 参数类型 参数位置 描述
id string path 必选,用户id
响应:
[
  {
    "gender": "male",
    "_id": "5f71d3d498d4dc0de13200f5",
    "name": "alfalfaw",
    "avatar_url": "test_url",
    "__v": 4,
    "updatedAt": "2020-10-02T10:52:46.202Z",
    "createdAt": "2020-10-02T10:52:46.202Z"
  }
]
取消关注用户
接口:
DELETE /users/following/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,取消关注的用户id
响应:
status 204
获取用户关注话题列表
接口:
GET /users/:id/followingTopics
参数:
参数名 参数类型 参数位置 描述
id string path 必选,用户id
响应:
[
  {
    "_id": "5f71ddf84557410e40278a2e",
    "name": "水木年华",
    "avatar_url": "test_url",
    "__v": 0
  }
]
用户关注话题
接口:
PUT /users/followingTopic/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,话题id
响应:
status 204
用户取消关注话题
接口:
DELETE /users/followingTopic/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,话题id
响应:
status 204
获取用户问题列表
接口:
GET /users/:id/questions
参数:
参数名 参数类型 参数位置 描述
id string path 必选,用户id
响应:
[
  {
    "_id": "5f734af88519d353e0dcea03",
    "title": "如何看待鸿蒙应用开发框架采用JavaScript作为开发语言?",
    "description": "",
    "questioner": "5f71d3d498d4dc0de13200f5",
    "__v": 0
  }
]
用户喜欢的答案列表
接口:
GET /users/:id/likingAnswers
参数:
参数名 参数类型 参数位置 描述
id string path 必选,答案id
响应:
[
  {
    "voteCount": 1,
    "_id": "5f771b1b1755574dbb6f5b0b",
    "content": "这是回答",
    "answerer": "5f71d3d498d4dc0de13200f5",
    "questionId": "5f734af88519d353e0dcea03",
    "createdAt": "2020-10-02T12:20:43.530Z",
    "updatedAt": "2020-10-02T12:21:19.528Z",
    "__v": 0
  }
]
用户喜欢答案
接口:
PUT /users/likingAnswers/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,答案id
响应:
status 204
用户取消喜欢答案
接口:
DELETE /users/likingAnswers/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,答案id
响应:
status 204
用户不喜欢的答案列表
接口:
GET /users/:id/dislikingAnswers
参数:
参数名 参数类型 参数位置 描述
id string path 必选,用户id
响应:
[
  {
    "voteCount": 0,
    "_id": "5f771b1b1755574dbb6f5b0b",
    "content": "这是回答",
    "answerer": "5f71d3d498d4dc0de13200f5",
    "questionId": "5f734af88519d353e0dcea03",
    "createdAt": "2020-10-02T12:20:43.530Z",
    "updatedAt": "2020-10-02T12:30:28.505Z",
    "__v": 0
  }
]
用户不喜欢答案
接口:
PUT /users/dislikingAnswers/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,答案id
响应:
status 204
用户取消不喜欢回答
接口:
DELETE /users/dislikingAnswers/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,回答id
响应:
status 204
用户收藏答案列表
接口:
GET /users/:id/collectingAnswers
参数:
参数名 参数类型 参数位置 描述
id string path 必选,用户id
响应:
[
  {
    "voteCount": 0,
    "_id": "5f771b1b1755574dbb6f5b0b",
    "content": "这是回答",
    "answerer": "5f71d3d498d4dc0de13200f5",
    "questionId": "5f734af88519d353e0dcea03",
    "createdAt": "2020-10-02T12:20:43.530Z",
    "updatedAt": "2020-10-02T12:30:28.505Z",
    "__v": 0
  }
]
用户收藏答案
接口:
PUT /users/collectingAnswers/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,答案id
响应:
status 204
用户取消收藏答案
接口:
DELETE /users/collectingAnswers/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,答案id
响应:
status 204
创建回答
接口:
POST /questions/:questionId/answers
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
questionId string path 必选,问题id
响应:
{
  "topics": [],
  "voteCount": 0,
  "_id": "5f77177b90d3df4c46def772",
  "content": "这是回答",
  "answerer": "5f71d3d498d4dc0de13200f5",
  "questionId": "5f734af88519d353e0dcea03",
  "createdAt": "2020-10-02T12:05:15.146Z",
  "updatedAt": "2020-10-02T12:05:15.146Z",
  "__v": 0
}
获取回答列表
接口:
GET /questions/:questionId/answers
参数:
参数名 参数类型 参数位置 描述
questionId string path 必选,问题id
响应:
[
  {
    "voteCount": 0,
    "_id": "5f77177b90d3df4c46def772",
    "content": "这是回答",
    "answerer": "5f71d3d498d4dc0de13200f5",
    "questionId": "5f734af88519d353e0dcea03",
    "createdAt": "2020-10-02T12:05:15.146Z",
    "updatedAt": "2020-10-02T12:05:15.146Z",
    "__v": 0
  }
]
获取回答详情
接口:
GET /questions/:questionId/answers/:id
参数:
参数名 参数类型 参数位置 描述
questionId string path 必选,问题id
id string path 必选,答案id
响应:
{
  "voteCount": 0,
  "_id": "5f77177b90d3df4c46def772",
  "content": "这是答案",
  "answerer": {
    "gender": "male",
    "_id": "5f71d3d498d4dc0de13200f5",
    "name": "alfalfaw",
    "avatar_url": "test_url",
    "__v": 10,
    "updatedAt": "2020-10-02T13:05:26.961Z",
    "createdAt": "2020-10-02T10:52:46.202Z"
  },
  "questionId": "5f734af88519d353e0dcea03",
  "createdAt": "2020-10-02T12:05:15.146Z",
  "updatedAt": "2020-10-02T12:05:15.146Z",
  "__v": 0
}
修改回答
接口:
PATCH /questions/:questionId/answers/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
questionId string path 必选,问题id
id string path 必选,答案id
响应:
{
  "n": 1,
  "nModified": 1,
  "ok": 1
}
删除回答
接口:
DELETE /questions/:questionId/answers/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
questionId string path 必选,问题id
id string path 必选,答案id
响应:
status 204
获取问题列表
接口:
GET /questions
参数:
参数名 参数类型 参数位置 描述
per_page number query 可选,每页大小
page number query 可选,当前所在页码
响应:
[
  {
    "_id": "5f734af88519d353e0dcea03",
    "title": "这是一个问题",
    "description": "这是问题的描述",
    "questioner": "5f71d3d498d4dc0de13200f5",
    "__v": 0
  }
]
获取问题详情
接口:
GET /questions/:id
参数:
参数名 参数类型 参数位置 描述
id string path 必选,问题id
响应:
{
  "topics": [],
  "_id": "5f734af88519d353e0dcea03",
  "title": "这是一个问题",
  "description": "这是问题的描述",
  "questioner": {
    "gender": "male",
    "_id": "5f71d3d498d4dc0de13200f5",
    "name": "alfalfaw",
    "avatar_url": "test_url",
    "__v": 10,
    "updatedAt": "2020-10-02T13:05:26.961Z",
    "createdAt": "2020-10-02T10:52:46.202Z"
  },
  "__v": 0
}
创建问题
接口:
POST /questions
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
title string body 必选,标题
description string body 可选,问题描述
响应:
{
  "topics": [],
  "_id": "5f7730043312fc52b064db4c",
  "title": "问题1",
  "description": "描述1",
  "questioner": "5f71d3d498d4dc0de13200f5",
  "createdAt": "2020-10-02T13:49:56.776Z",
  "updatedAt": "2020-10-02T13:49:56.776Z",
  "__v": 0
}
修改问题
接口:
PATCH /questions/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,问题id
响应:
{ "n": 1, "nModified": 1, "ok": 1 }
删除问题
接口:
DELETE /questions/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,问题id
响应:
status 204
获取问题的关注列表
接口:
GET /questions/:id/followers
参数:
参数名 参数类型 参数位置 描述
id string path 问题id
响应:
[
  {
    "gender": "male",
    "_id": "5f71d3d498d4dc0de13200f5",
    "name": "alfalfaw",
    "avatar_url": "test_url",
    "__v": 11,
    "updatedAt": "2020-10-02T14:33:05.207Z",
    "createdAt": "2020-10-02T10:52:46.202Z"
  }
]
获取用户问题列表
接口:
GET /users/:id/questions
参数:
参数名 参数类型 参数位置 描述
id string path 用户id
响应:
[
  {
    "_id": "5f734af88519d353e0dcea03",
    "title": "问题122s",
    "description": "描述1",
    "questioner": "5f71d3d498d4dc0de13200f5",
    "__v": 0,
    "updatedAt": "2020-10-02T14:06:18.276Z"
  },
  {
    "_id": "5f7730043312fc52b064db4c",
    "title": "问题1",
    "description": "描述1",
    "questioner": "5f71d3d498d4dc0de13200f5",
    "createdAt": "2020-10-02T13:49:56.776Z",
    "updatedAt": "2020-10-02T13:49:56.776Z",
    "__v": 0
  }
]
获取用户关注问题列表
接口:
GET /users/:id/followingQuestions
参数:
参数名 参数类型 参数位置 描述
id string path 必选,用户id
响应:
[
  {
    "_id": "5f734af88519d353e0dcea03",
    "title": "问题122s",
    "description": "描述1",
    "questioner": "5f71d3d498d4dc0de13200f5",
    "__v": 0,
    "updatedAt": "2020-10-02T14:06:18.276Z"
  }
]
用户关注问题
接口:
PUT /users/followingQuestion/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,问题id
响应:
status 204
用户取消关注问题
接口:
DELETE /users/followingQuestion/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 必选,问题id
响应:
status 204
获取问题的话题列表
接口:
GET /questions/:id/topics
参数:
参数名 参数类型 参数位置 描述
id string path 必选,问题id
响应:
[
  {
    "_id": "5f71ddf84557410e40278a2e",
    "name": "水木年华",
    "avatar_url": "test_url",
    "__v": 0
  }
]
获取话题列表
接口:
GET /topics
参数:
参数名 参数类型 参数位置 描述
per_page number query 可选,每页大小
page number query 可选,当前所在页码
响应:
[
  {
    "_id": "5f71ddf84557410e40278a2e",
    "name": "水木年华",
    "avatar_url": "test_url",
    "__v": 0
  }
]
获取话题详情
接口:
GET /topics/:id
参数:
参数名 参数类型 参数位置 描述
id string path 必选,话题id
响应:
{
  "_id": "5f71ddf84557410e40278a2e",
  "name": "水木年华",
  "avatar_url": "test_url",
  "__v": 0
}
创建话题
接口:
POST /topics
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
name string body 必选,标题
avatar_url string body 可选,话题封面
introduction string body 可选,话题描述
响应:
{
  "_id": "5f77402928281f57f19bb665",
  "name": "水木年华11",
  "avatar_url": "test_url",
  "introduction": "测试话题",
  "createdAt": "2020-10-02T14:58:49.727Z",
  "updatedAt": "2020-10-02T14:58:49.727Z",
  "__v": 0
}
删除话题
接口:
DELETE /topics/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
id string path 话题id
响应:
status 204
获取话题粉丝列表
接口:
GET /topics/:id/followers
参数:
参数名 参数类型 参数位置 描述
id string path 话题id
响应:
[
  {
    "gender": "male",
    "_id": "5f71d3d498d4dc0de13200f5",
    "name": "alfalfaw",
    "avatar_url": "test_url",
    "__v": 11,
    "updatedAt": "2020-10-02T14:33:05.207Z",
    "createdAt": "2020-10-02T10:52:46.202Z"
  }
]
获取话题的问题列表
接口:
GET /topics/:id/questions
参数:
参数名 参数类型 参数位置 描述
id string path 话题id
响应:
[
  {
    "_id": "5f734af88519d353e0dcea03",
    "title": "问题122s",
    "description": "描述1",
    "questioner": "5f71d3d498d4dc0de13200f5",
    "__v": 0,
    "updatedAt": "2020-10-02T15:13:42.624Z"
  }
]
获取评论列表
接口:
GET /questions/:questionId/answers/:answerId/comments
参数:
参数名 参数类型 参数位置 描述
questionId string path 必选,问题id
answerId string path 必选,回答id
响应:
[
  {
    "_id": "5f7749065aabfe5ac4d5c4d0",
    "content": "评论1",
    "commentator": {
      "gender": "male",
      "_id": "5f71d3d498d4dc0de13200f5",
      "name": "alfalfaw",
      "avatar_url": "test_url",
      "__v": 11,
      "updatedAt": "2020-10-02T14:33:05.207Z",
      "createdAt": "2020-10-02T10:52:46.202Z"
    },
    "answerId": "5f771b1b1755574dbb6f5b0b",
    "questionId": "5f734af88519d353e0dcea03",
    "createdAt": "2020-10-02T15:36:38.362Z",
    "updatedAt": "2020-10-02T15:36:38.362Z",
    "__v": 0
  }
]
发表评论
接口:
POST /questions/:questionId/answers/:answerId/comments
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
questionId string path 必选,问题id
answerId string path 必选,回答id
content string body 必选,评论内容
parentId string body 可选,父评论id
replyTo string body 可选,被评论的人
响应:
{
  "_id": "5f7749065aabfe5ac4d5c4d0",
  "content": "评论1",
  "commentator": "5f71d3d498d4dc0de13200f5",
  "answerId": "5f771b1b1755574dbb6f5b0b",
  "questionId": "5f734af88519d353e0dcea03",
  "createdAt": "2020-10-02T15:36:38.362Z",
  "updatedAt": "2020-10-02T15:36:38.362Z",
  "__v": 0
}
获取评论详情
接口:
GET /questions/:questionId/answers/:answerId/comments/:id
参数:
参数名 参数类型 参数位置 描述
questionId string path 必选,问题id
answerId string path 必选,回答id
id string path 必选,评论id
响应:
{
  "_id": "5f7749065aabfe5ac4d5c4d0",
  "content": "评论1",
  "commentator": {
    "gender": "male",
    "_id": "5f71d3d498d4dc0de13200f5",
    "name": "alfalfaw",
    "avatar_url": "test_url",
    "__v": 11,
    "updatedAt": "2020-10-02T14:33:05.207Z",
    "createdAt": "2020-10-02T10:52:46.202Z"
  },
  "answerId": "5f771b1b1755574dbb6f5b0b",
  "questionId": "5f734af88519d353e0dcea03",
  "createdAt": "2020-10-02T15:36:38.362Z",
  "updatedAt": "2020-10-02T15:36:38.362Z",
  "__v": 0
}
修改评论
接口:
PATCH /questions/:questionId/answers/:answerId/comments/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
questionId string path 必选,问题id
answerId string path 必选,回答id
id string path 必选,评论id
content string body 必选,评论内容
响应:
status 204
删除评论
接口:
DELETE /questions/:questionId/answers/:answerId/comments/:id
参数:
参数名 参数类型 参数位置 描述
Authorization string header 必选,token参数
questionId string path 必选,问题id
answerId string path 必选,回答id
id string path 必选,评论id
响应:
status 204
最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351