Koa2 仿知乎服务端部分笔记记录(基于RESTful API)

仿知乎服务端源码地址:
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
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值