Lesson-1 话题模块需求分析
话题模块功能点
- 话题的增改查
- 分页、模糊搜索
- 用户属性中的话题引用·
Lesson-2&3 RESTful 风格的话题增改查接口
操作步骤
- 设计 Schema
- 实现 RESTful 风格的增改查接口
设计 Schema
// models/topics.js
const mongoose = require('mongoose');
const { Schema, model } = mongoose;
const topicSchema = new Schema({
__v: { type: Number, select: false },
name: { type: String, required: true },
avatar_url: { type: String },
introduction: { type: String, select: false } // 简介
});
module.exports = model('Topic', topicSchema);
实现 RESTful 风格的增改查接口
// controllers/topics.js
const Topic = require('../models/topics');
class TopicsCtl {
async find (ctx) {
ctx.body = await Topic.find();
}
async findById (ctx) {
const { fields = '' } = ctx.query;
const selectFields = fields.split(';').filter(item => item).map(item => ' +'+item).join('');
const topic = await Topic.findById(ctx.params.id).select(selectFields);
if(!topic) ctx.throw(404, '用户不存在');
ctx.body = topic;
}
async create (ctx) {
ctx.verifyParams({
name: { type: 'string', required: true },
avatar_url: { type: 'string', required: false },
introduction: { type: 'string', required: false }
});
const { name } = ctx.request.body;
const requesteTopic = await Topic.findOne({ name });
if(requesteTopic) ctx.throw(409, '话题已经存在');
const topic = await new Topic(ctx.request.body).save();
ctx.body = topic;
}
async update (ctx) {
ctx.verifyParams({
name: { type: 'string', required: false },
avatar_url: { type: 'string', require: false },
introduction: { type: 'string', require: false }
});
// findByIdAndUpdate,第一个参数为要修改的数据id,第二个参数为修改的内容
const topic = await Topic.findByIdAndUpdate(ctx.params.id, ctx.request.body);
if(!topic) ctx.throw(404, '话题不存在');
ctx.body = topic;
}
}
module.exports = new TopicsCtl();
// routes/topics.js
const jwt = require('koa-jwt');
const Router = require('koa-router');
const router = new Router({prefix: '/topics'});
const { find, findById, create, update } = require('../controllers/topics');
const { secret } = require('../config');
// 认证中间件
const auth = jwt({ secret });
// 获取话题列表
router.get('/', find);
// 增加话题
router.post('/', auth, create);
// 获取特定话题
router.get('/:id',checkTopicExist, findById);
// 修改特定话题
router.patch('/:id', auth, checkTopicExist, update);
module.exports = router;
说一下为什么没有删除方法,因为话题涉及的模块特别多,比如普通的话题,用户属性里,用户的回答/收藏/关注等等,一旦删除,对到很多用户而言都不好处理,所以并不存在删除话题
Lesson-4 RESTful API 最佳实践 -- 分页
实现分页逻辑
实现的是查询链接上获取page和pre_page的参数,比如localhost:3000/topics?page=1&per_page=1
,这样就是获取第一页第一条,需要用到mongoose的limit和skip这两个方法,具体就不再贴文档了,自行查阅
// controllers/topics.js
async find (ctx) {
const { per_page = 10 } = ctx.query;
const page = Math.max(+ctx.query.page, 1) - 1;
const perPage = Math.max(+ctx.query.per_page, 1);
ctx.body = await Topic.find().limit(perPage).skip(page * perPage); // limit: 返回多少数量,skip:跳过多少数量
}
Lesson-5 RESTful API 最佳实践 -- 模糊搜索
实现模糊搜索逻辑
其实mongoose的find方法用个正则表达式匹配就可以实现了
// controllers/topics.js
async find (ctx) {
const { per_page = 10 } = ctx.query;
const page = Math.max(+ctx.query.page, 1) - 1;
const perPage = Math.max(+ctx.query.per_page, 1);
ctx.body = await User.find({ name: new RegExp(ctx.query.q) }).limit(perPage).skip(page * perPage);
}
Lesson-6 用户属性中的话题引用
使用话题引用替代部分用户属性
注意一下,因为之前这些用户属性都是设置的String类型,所以需要更改过来,并且之前数据库已经存储过了,所以需要添加话题,手动修改特定用户各项属性,才能验证成功,这里postman就不贴图了
// models/users.js
const userSchema = new Schema({
__v: { type: Number, select: false },
name: { 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 }, // enum 可枚举,性别
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, // 用户ID
ref: 'User' // 引用 User = require('../models/users') 数据库模型
}],
select: false
}
});
// controllers/topics.js
async findById (ctx) {
const { fields = '' } = ctx.query;
const selectFields = fields.split(';').filter(item => item).map(item => ' +'+item).join('');
const populateStr = fields.split(';').filter(item => item).map(item => {
if (item === 'employments') return 'employments.company employments.job';
if (item === 'educations') return 'educations.school educations.major';
return item;
}).join(' ');
const user = await User.findById(ctx.params.id).select(selectFields).populate(populateStr);
if(!user) ctx.throw(404, '用户不存在');
ctx.body = user;
}
Lesson-7&8 RESTful 风格的关注话题接口
实现关注话题逻辑(用户-话题多对多关系)
与前面粉丝列表,关注某人等操作基本一致,唯一需要说的一点是:比如获取某话题下的关注者,那么这个方法不应该写在users.js里,因为它是属于话题方法的一部分,所以像这种需要写在topics里
// models/users.js
followingTopics: { // 关注的话题
type: [{
type: Schema.Types.ObjectId, // 话题ID
ref: 'Topic' // 引用 Topic = require('../models/topics') 数据库模型
}],
select: false
}
四个接口的控制器
// controllers/users.js
// 话题关注列表
async listFollowingTopics (ctx) {
const user = await User.findById(ctx.params.id).select('+followingTopics').populate('followingTopics');
console.log(user);
if(!user) ctx.throw(404);
ctx.body = user.followingTopics;
}
// 关注某个话题
async followTopic (ctx) {
const me = await User.findById(ctx.state.user._id).select('+followingTopics');
if(!me.followingTopics.map(id => id.toString()).includes(ctx.params.id)) {
me.followingTopics.push(ctx.params.id);
me.save();
}
ctx.status = 204;
}
// 取消关注某个话题
async unfollowTopic (ctx) {
const me = await User.findById(ctx.state.user._id).select('+followingTopics');
const index = me.followingTopics.map(id => id.toString()).indexOf(ctx.params.id);
if(index > -1) {
me.followingTopics.splice(index, 1);
me.save();
}
ctx.status = 204;
}
// controllers/topics.js
const User = require('../models/users'); // 数据库模型导出
// 话题关注者
async listTopicsFollowers (ctx) {
const users = await User.find({ followingTopics: ctx.params.id }); // 查找followingTopics包含该话题id的用户
ctx.body = users;
}
async checkTopicExist (ctx, next) {
const topic = await Topic.findById(ctx.params.id);
if(!topic) ctx.throw(404, '话题不存在');
await next();
}
四个接口对应的路由
// routes/topics.js
const { find, findById, create, update, checkTopicExist, listTopicsFollowers } = require('../controllers/topics');
// 获取当前话题下的关注者
router.get('/:id/followers', checkTopicExist, listTopicsFollowers)
// routes/users.js
const { find, findById, create, update, delete: del, login,
checkOwner, listFollowing, follow, unfollow, listFollowers, checkUserExist,
followTopic, unfollowTopic, listFollowingTopics } = require('../controllers/users');
const { checkTopicExist } = require('../controllers/topics');
// 获取关注话题列表
router.get('/:id/listFollowingTopics', listFollowingTopics);
// 关注某话题
router.put('/followingTopic/:id', auth, checkTopicExist, followTopic);
// 取消关注某话题
router.put('/unfollowingTopic/:id', auth, checkTopicExist, unfollowTopic);
网友评论