仿知乎服务端源码地址:
https://github.com/lybinweb/ZhihuAPI/tree/master
文章目录
什么是错误处理
编程语言或计算机硬件里的一种机制
处理软件或信息系统中出现的异常状况
异常状况有哪些?
运行时错误,都返回500
逻辑错误,如找不到(404)、先决条件失败(412)、无
法处理的实体(参数格式不对, 422 )等
为什么要用错误处理?
防止程序挂掉
告诉用户错误信息
便于开发者调试
koa自带错误处理
请求接口不存在:返回404
先决条件失败:返回412
运行时错误:返回500
使用koa-json-error进行错误处理
使用koa-parameter进行效验参数
NoSQL数据库
列存储( HBase )
图存储( FIockDB )
文档存储( MongoDB )
对象存储( db4o )
Key-value存储( Redis )
XML 存储( BaseX )
为什么使用NoSQL?
简单(没有原子性、一致性、 隔离性等复杂规范)
便于横向拓展
适合超大规模数据的存储
很灵活地存储复杂结构的数据( Schema Free )
MongoDB数据库
面向文档存储的开源数据库
为什么使用?
性能好(内存计算)
大规模数据存储(可拓展性)
可靠安全(本地复制、自动故障转移)
方便存储复杂数据结构( Schema Free )
开启MongoDB数据库
个人操作命令:
//第一步
D:\MongoDB\mongodb-win32-x86_64-2012plus-4.2.8\bin>mongod.exe
//第二步
mongod --dbpath=D:\MongoDB\mongodb-win32-x86_64-2012plus-4.2.8\data --port=27018
//一般前两步运行完服务就启动了
mongod --dbpath=C:\Users\ADMIN\Desktop\html作业\warehouse\mongodb --port=27019
Session
Session的优势
相比JWT,最大的优势就在于可以主动清除session了
session保存在服务器端,相对较为安全
结合cookie使用,较为灵活,兼容性较好
Session的劣势
cookie + session 在跨域场景表现并不好
如果是分布式部署,需要做多机共享session机制
基于cookie的机制很容易被CSRF
查询session信息可能会有数据库查询操作
Session相关的概念介绍
session:主要存放在服务器端,相对安全
cookie:主要存放在客户端,并且不是很安全
sessionStorage:仅在当前会话下有效,关闭页面或浏览器后被清
除
localstorage:除非被清除,否则永久保存
JWT
什么是JWT
JSON Web Token是一个开放标准(RFC 7519)
定义了一种紧凑且独立的方式,可以将各方之间的信息作为JSON
对象进行安全传输
该信息可以验证和信任,因为是经过数字签名的
JWT构成
头部( Header )
- typ : token的类型,这里固定为JWT
- alg:使用的hash算法,例如:HMACSHA256或者RSA
//Header编码前后:
{" alg":"HS256","typ":"JWT"}
' eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9'
有效载荷( Payload )
- 存储需要传递的信息,如用户ID、用户名等
- 还包含元数据,如过期时间、发布人等
- 与Header不同,Payload可以加密
签名( Signature )
在nodejs中使用JWT
-
安装jsonwebtoken
npm i jsonwebtoken
-
签名
-
验证
//在终端操作:
千@LYBprivate MINGW64 /f/Change Class/Homework/zhihu-api
$ node
Welcome to Node.js v12.16.0.
Type ".help" for more information.
> jwt = require('jsonwebtoken')
{
decode: [Function],
verify: [Function],
sign: [Function],
JsonWebTokenError: [Function: JsonWebTokenError],
NotBeforeError: [Function: NotBeforeError],
TokenExpiredError: [Function: TokenExpiredError]
}
> token = jwt.sign({name:'lyb'},'sectet');
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibHliIiwiaWF0IjoxNjA4NTk4MTYwfQ
.9hwrlG1hMNFNtGcqHLwIUfZ13U01VY2IrRkAC-WGQdU'
> jwt.decode(token)
{ name: 'lyb', iat: 1608598160 }
> jwt.verify(token,'sectet')
{ name: 'lyb', iat: 1608598160 }
实现用户登录
// 登录接口
async login(ctx) {
ctx.verifyParams({
name: { type: 'string', require: true },
password:{type:'string',require:true}
})
// 请求数据库获取用户名密码
const user = await User.findOne(ctx.request.body)
if (!user) { ctx.throw(401, '用户名或密码不正确') }
const { _id, name } = user
/*
使用数字签名实现token
secret:密码
expiresIn:过期时间
*/
const token = jsonwebtoken.sign({ _id, name }, secret, { expiresIn: '1d' })
// 返回给客户端
ctx.body = {token}
}
自己编写koa中间件实现用户认证与授权
// 判断是否有权限的中间件(授权中间件)
async checkOwner(ctx, next) {
if (ctx.params.id !== ctx.state.user._id) { ctx.throw(403, '没有权限操作') }
await next()
}
然后在接口中使用,例如:
router.patch('/:id',auth,checkOwner,update)
使用koa-jwt来实现用户登录认证授权
const auth = jwt({secret})
// 判断是否有权限的中间件(授权中间件)
async checkOwner(ctx, next) {
if (ctx.params.id !== ctx.state.user._id) { ctx.throw(403, '没有权限操作') }
await next()
}
使用koa-body上传图片
安装koa-body
,替换koa-bodyparser
app.use(koaBody({
multipart: true,
formidable: {
//图片路径
uploadDir: path.join(__dirname, '/public/uploads'),
//图片是否有扩展名
keepExtensions: true
},
}));
使用koa-static中间件生成图片
- 安装
koa-static
- 引入
app.use(koaStatic(path.join(__dirname,'public')))
直接访问http://localhost:3333/uploads/upload_265f08eafbde490711a081dd7d89d0fe.jpg
就可以获得图片
个人资料schema设计
模型设计:
const userSchema = new Schema({
_v:{type:Number,select:false},
name: { type: String, required: true },
// 设置密码,select字段是为了密码不暴露出去
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 },
jog:{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}
}]
}
})
参数效验:
// 修改用户
async update(ctx) {
ctx.verifyParams({
name: { type: 'string', required: false },
password: { type: 'string', required: false },
avatar_url: { type: 'string', required: false },
gender: { type: 'string', required: false },
headline: { type: 'string', required: false },
locations: { type: 'array', itemType: 'string', required: false },
business: { type: 'string', required: false },
employments: { type: 'array', itemType: 'object', required: false },
educations: { type: 'array', itemType: 'object', required: false },
})
const user = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
if (!user) { ctx.throw(404) }
ctx.body = user
}
关注与粉丝
schema设计
// 关注
following: {
type: [{
type:Schema.Types.ObjectId,ref:'User'
}],
select:false
},
关注与粉丝接口
- 获取关注接口
- 获取粉丝接口
- 关注某人接口
- 取消关注接口
代码如下:
// 获取关注接口
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 listFollowers(ctx) {
const users = await User.find({ following: ctx.params.id })
ctx.body = users
}
// 关注某人接口
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();
}
ctx.status = 204
}
// 取消关注接口
async unfollow(ctx) {
const me = await User.findById(ctx.state.user._id).select('+following')
// 取消关注的人的索引
const index = me.following.map(id=>id.toString()).indexOf(ctx.params.id)
if (index > -1) {
me.following.splice(index,1);
// 保存到数据库
me.save();
}
ctx.status = 204
}
与之对应的接口路由:
// 获取关注接口
router.get('/:id/following', listFollowing)
// 获取粉丝接口
router.get('/:id/followers',listFollowers)
// 关注某人
router.put('/following/:id',auth,follow)
// 取消关注
router.delete('/following/:id', auth, unfollow)
编写用户存在与否的中间件
// 判断用户是否存在中间件
async checkUserExist(ctx, next) {
const user = await User.findById(ctx.params.id)
if (!user) { ctx.throw(404, '用户不存在') }
await next()
}
话题
schema设计
// 话题
const topicSchema = new Schema({
_v: { type: Number, select: false },
name: { type: String, required: true },
// 头像
avatar_url: { type: String },
introduction:{type:String,select:false},
})
接口设计
- 查找话题接口
- 查找特定话题接口
- 创建话题接口
- 更新话题接口
代码如下:
// 查找话题接口
async find(ctx) {
ctx.body = await Topic.find();
}
// 查找特定话题接口
async findById(ctx) {
const { fields = '' } = ctx.query;
const selectFields = fields.split(';').filter(f => f).map(f => ' +' + f).join('');
const topic = await Topic.findById(ctx.params.id).select(selectFields)
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 topic = await new Topic(ctx.request.body).save();
ctx.body = topic;
}
// 更新话题接口
async update(ctx) {
ctx.verifyParams({
name: { type: 'string', required: true },
avatar_url: { type: 'string', required: false },
introduction: { type: 'string', required: false },
});
const topic = await Topic.findByIdAndUpdate(ctx.params.id, ctx.request.body)
ctx.body = topic;
}
接口路由:
router.get('/',find)
router.post('/',auth, create)
router.get('/:id',findById)
// patch可以更新多个字段
router.patch('/:id',auth,update)
分页
// 查找话题接口
async find(ctx) {
//默认一页10条
const { per_page = 10 } = ctx.query;
const page = Math.max(ctx.query.page * 1) - 1;
// 接收每页有多少项 ,如果是 0 或者 -1 则使用Math函数来修正为 1
const perPage = Math.max(ctx.query.per_page * 1,1)
/*
limit(10):只返回10项
skip(10):跳过10项
合起来:返回第二页
*/
ctx.body = await Topic.find().limit(perPage).skip(page * perPage)
}
模糊搜索
.find({name: new RegExp(ctx.query.q)})
实现话题的模糊搜索
ctx.body = await Topic
// 实现话题的模糊搜索
.find({name: new RegExp(ctx.query.q)})
.limit(perPage).skip(page * perPage)
用户属性的话题引用
schema更新
const userSchema = new Schema({
_v:{type:Number,select:false},
name: { type: String, required: true },
// 设置密码,select字段是为了密码不暴露出去
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' },
jog:{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
},
})
查询特定用户接口更新
// 查询特定用户
async findById(ctx) {
// 根据用户输入想查询的字段来获取
const { fields = '' } = ctx.query
const selectFields = fields.split(';').filter(f => f).map(f => ' +' + f).join('');
// 动态获取query里传入的参数
const populateStr = fields.split(';').filter(f => f).map(f => {
if (f === 'employments') {
return 'employments.company employments.job'
}
if (f === 'education') {
return 'education.school education.major'
}
return f;
})
const user = await User.findById(ctx.params.id).select(selectFields).populate(populateStr)
if (!user) { ctx.throw(404, '用户不存在') }
ctx.body = user
}