前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >node-koa 框架 项目搭建 🏗

node-koa 框架 项目搭建 🏗

作者头像
用户4793865
发布2023-01-12 16:00:26
3.2K0
发布2023-01-12 16:00:26
举报
文章被收录于专栏:前端小菜鸡yym前端小菜鸡yym

这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

代码[链接🔗]:(https://gitee.com/yang-yiming1234/koa/tree/master) 持续更新~

简介

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

官网地址

初始化项目

生成package.json

代码语言:javascript
复制
 npm init

安装 koa

代码语言:javascript
复制
npm install koa

新建入口文件

新建src文件夹 新建一个mian.js(叫什么都可以比如index.js)

代码语言:javascript
复制
const Koa = require('koa')

const app = new Koa()

app.use((ctx, next)=>{
    ctx.body = 'hello'
})
const port = process.env.PORT || 8000
app.listen(port,()=>{
    console.log(`server is running on ${port}`)
})

node src/main.js启,在浏览器访问端口,

image.png
image.png

自动启动配置文件 nodemon

安装

代码语言:javascript
复制
npm i nodemon

添加启动命令

package.json dev替换成别的也可以

代码语言:javascript
复制
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon ./src/main.js"
  },

之后在控制台执行命令 npm run dev 就可以启动了。并且当我们改动代码,他也会自动的重启。

读取配置文件

安装 dotenv

代码语言:javascript
复制
npm i dotenv

在根目录下新建.env文件(存一些项目配置的环境变量)

image.png
image.png

然后新建 src/config/config.default.js

代码语言:javascript
复制
const dotenv = require('dotenv')

dotenv.config()
console.log(process.env.APP_PORT)
// process node的进程 
// env 环境变量
module.exports = process.env

main.js中引用

代码语言:javascript
复制
const Koa = require('koa')
// 解构出 APP_PORT
const { APP_PORT } = require('./config/config.default')
const app = new Koa()

app.use((ctx, next) => {
    ctx.body = 'hello api'
})
app.listen(APP_PORT, () => {
    console.log(`server is running on ${APP_PORT}`)
})

添加路由配置

安装 koa-router

代码语言:javascript
复制
npm i koa-router

看一下 koa-router 文档

首先去npmjs的官网查看 ,好巧不巧 ,没有写。 npmjs

image.png
image.png

不要 紧的 我们再看看github上有没有 ✈️

image.png
image.png

使用步骤:

  1. 导入包 引入koa-router
代码语言:javascript
复制
const Router = require('koa-router')
  1. 实例化对象
代码语言:javascript
复制
const indexRouter =  new Router()
  1. 编写路由
代码语言:javascript
复制
indexRouter.get('/',(ctx, next)=>{
    ctx.body = 'hello index'
})
  1. 注册中间件
  • 把router和app关联起来,通过router.routes()方法去注册中间键。
  • router 是一个对象,但是我们app.use()只能接收一个函数作为参数。// 必须是一个函数 app.use(indexRouter) app.use(indexRouter.routes())
image.png
image.png

抽离出路由文件

假如我们有一类uers的路由,我们需要新实例化一个 UserRouter,还有之前的IndexRouter。都写到main.js文件中并不是很好的写法。

新建router文件夹用于存放各种api

router/user.route.js

代码语言:javascript
复制
const Router = require('koa-router')

const router = new Router({prefix:'/users'})

// 会和 /users拼接
router.get('/',(ctx,next)=>{
    ctx.body = 'hello users'
})
// 导出
module.exports = router

main.js

代码语言:javascript
复制
const Koa = require('koa')
const { APP_PORT } = require('./config/config.default')
const userRouter = require('./router/user.route')
const app = new Koa()

// 必须是一个函数
app.use(userRouter.routes())

app.listen(APP_PORT, () => {
    console.log(`server is running on ${APP_PORT}`)
})

目录结构优化

目的:将http服务和app业务分开

新建app文件,在其下面新建index.js 将mian.js中的实例化对象和使用router的都抽离出来

代码语言:javascript
复制
const Koa = require('koa')
const userRouter = require('./router/user.route')
const app = new Koa()

// 必须是一个函数
app.use(userRouter.routes())

module.exports = app

main.js

代码语言:javascript
复制
const { APP_PORT } = require('./config/config.default')

const app = require('./app/index')
app.listen(APP_PORT, () => {
    console.log(`server is running on ${APP_PORT}`)
})

然后我们发现报错了,因为把main.js中的代码直接剪切到index.js中 涉及到了文件引用的路径层级改变的问题。

image.png
image.png
代码语言:javascript
复制
const userRouter = require('../router/user.route')

抽离出controller

image.png
image.png

route中的处理函数这块拆分出来

image.png
image.png

新建controller文件夹,在其下面新建

user.controller.js

代码语言:javascript
复制
// 写成一个类
class UserController{
    async register(ctx, next){
      ctx.body  = '用户注册成功'
    }
}

// 导出实例化的对象
module.exports = new UserController()

user.route.js

代码语言:javascript
复制
const Router = require('koa-router')
// 引入controller
const { register } = require('../controller/user.controller')
const router = new Router({prefix:'/users'})

// 注册接口
router.post('/register',register)
// 导出
module.exports = router

使用postman测试一下

image.png
image.png

koa-body

这里可以跳到其官网: koa-body

image.png
image.png

翻译一下:

一个完全特征的 koa 中间件,支持文件上传、form表单、json格式。提供了像express的bodyParser的相同功能。用于解析数据

安装

代码语言:javascript
复制
npm i koa-body

注册中间件 /app/index.js 中添加

代码语言:javascript
复制
const KoaBody = require('koa-body')
// 在注册路由前注册
app.use(KoaBody())

添加后完整的

代码语言:javascript
复制
const KoaBody = require('koa-body')
const userRouter = require('../router/user.route')
const app = new Koa()

// 在注册路由前注册
app.use(KoaBody())
// 必须是一个函数
app.use(userRouter.routes())

module.exports = app

解析请求数据

改写controller user.controller.js

代码语言:javascript
复制
//注意 createUser是异步函数 抽离出的service层
const { createUser } = require('../service/user.service')
class UserController {
    async register(ctx, next) {
        // 1.获取数据
        console.log(ctx.request.body)
        const { usesr_name, password } = ctx.request.body
        // 2.操作数据库
        const res = await createUser(usesr_name, password)
        console.log(res)
        // 3.返回给客户端
        ctx.body = ctx.request.body
    }
    async login(ctx, next) {
        ctx.body = '用户登录'
    }
}

// 导出实例化的对象
module.exports = new UserController()

抽离servcie层

用于操作数据库处理 创建 /src/service/user.service.js

代码语言:javascript
复制
class UserService {
    // 因为存入数据库 是异步的
    async createUser(user_name,password){
        return '写入数据库'
    }
}
// 导出后 在控制器controller中使用
module.exports = new UserService()
image.png
image.png
image.png
image.png

搞下数据库

什么是Sequelize?

官网

Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 PostgresMySQLMariaDBSQLite 以及 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能。

什么是ORM?

对象关系映射

  • 数据表映射(对应)一个类
  • 数据表中的数据行(记录)对应一个对象
  • 数据表字段对应对象的属性
  • 数据表的操作对应对象的方法 也就是用面向对象的方法去操作数据库。

安装两个包

可以两个一起下载

代码语言:javascript
复制
npm i mysql2 sequelize

连接到数据库

新建 db/seq.js

代码语言:javascript
复制
const { Sequelize } = require('sequelize')
/**
 * database
 * username
 * password
 */
const seq = new Sequelize('mine', 'root', 'root', {
    host: 'localhost',
    // 数据库类型
    dialect: 'mysql'
})
// 返回值是一个promise对象
seq.authenticate().then(() => {
    console.log('数据库连接成功')
}).catch(err => {
    console.log('数据库连接失败', err)
})

module.exports = seq

可以使用node命令node src/db/seq.js 试一下连接是否成功

image.png
image.png

我们应该把链接数据库的配置变量放到.env中(不知道是什么翻看上一篇) .env

代码语言:javascript
复制
APP_PORT = 8000
MYSQL_HOST = localhost
MYSQL_PORT = 3306
MYSQL_USER = root
MYSQL_PWD = root
MYSQL_DB = mine

然后再使用node命令node src/db/seq.js 试一下连接是否成功

创建模型

模型是要和表对应的,建立一个映射关系。 我们还是需要使用sequelize,这是它官网关于model的 📟介绍

代码语言:javascript
复制
// 解构出
const { Sequelize, Model, DataTypes } = require("sequelize");
// 创建的实例   参数是数据库的类型
const sequelize = new Sequelize("sqlite::memory:");
// 创建模型 define:定义      第一个参数是数据库的表名         
const User = sequelize.define("user", {
  // 表的字段名 DataTypes.数据类型
  name: DataTypes.TEXT,
  // 如果是多项配置,比如还要加表的默认值 就要写成对象
  favoriteColor: {
    type: DataTypes.TEXT,
    defaultValue: 'green'
  },
  age: DataTypes.INTEGER,
  cash: DataTypes.INTEGER
},{
// 这是其他模型参数 
sequelize, // 我们需要传递连接实例 
modelName: 'User' // 我们需要选择模型名称
});

(async () => {
  // 执行创建数据库
  await sequelize.sync({ force: true });
  // 这里是代码
})();

sequelize.define的三个参数

  • 表名 但是有时候它会在表前加前缀,可以在第三个对象参数中添加属性modelName: 'User'
  • 数据model,是一个对象。 首先,说一下常用的数据类型,这是针对【MySQL】数据库的。
代码语言:javascript
复制
// 字符串
DataTypes.STRING // VARCHAR(255)
DataTypes.STRING(1234) // VARCHAR(1234)
DataTypes.STRING.BINARY // VARCHAR BINARY
DataTypes.TEXT // TEXTDataTypes.TEXT('tiny') // TINYTEXT
// 布尔
DataTypes.BOOLEAN // TINYINT(1)
// 数值
DataTypes.INTEGER // INTEGER
DataTypes.BIGINT // BIGINT
DataTypes.BIGINT(11) // BIGINT(11) 
DataTypes.FLOAT // FLOAT
DataTypes.FLOAT(11) // FLOAT(11)
DataTypes.FLOAT(11, 10) // FLOAT(11,10) 
DataTypes.DOUBLE // DOUBLE
DataTypes.DOUBLE(11) // DOUBLE(11)
DataTypes.DOUBLE(11, 10) // DOUBLE(11,10) 
DataTypes.DECIMAL // DECIMAL
DataTypes.DECIMAL(10, 2) // DECIMAL(10,2)
// 日期
DataTypes.DATE // DATETIME 适用于 mysql / sqlite, 带时区的TIMESTAMP 适用于 postgresDataTypes.DATE(6) // DATETIME(6) 适用于 mysql 5.6.4+. 支持6位精度的小数秒DataTypes.DATEONLY // 不带时间的 DATE
// UUID 
{ type: DataTypes.UUID, defaultValue: Sequelize.UUIDV4 // 或 Sequelize.UUIDV1}

然后再看一些常用参数

代码语言:javascript
复制
// 是否为空
allowNull: false, 
// 默认值
defaultValue: true
// 主键
primaryKey: true
// 自动增长
autoIncrement: true
  • 一些数据库的配置 可选的 对象 如:
代码语言:javascript
复制
{
 timestamps: false // 默认会为数据表 创建时间戳字段,如果添加次属性 则在创建时不添加时间戳字段
 modelName: 'User' // 我们需要选择模型名称
}
 

可以看到多了两个字段,是时间戳。

image.png
image.png

模型同步

定义模型时,你要告诉 Sequelize 有关数据库中表的一些信息. 但是,如果该表实际上不存在于数据库中怎么办? 如果存在,但具有不同的列,较少的列或任何其他差异,该怎么办?

这就是模型同步的来源.可以通过调用一个异步函数(返回一个Promise)model.sync(options). 通过此调用,Sequelize 将自动对数据库执行 SQL 查询. 请注意,这仅更改数据库中的表,而不更改 JavaScript 端的模型.

  • User.sync() - 如果表不存在,则创建该表(如果已经存在,则不执行任何操作)
  • User.sync({ force: true }) - 将创建表,如果表已经存在,则将其首先删除
  • User.sync({ alter: true }) - 这将检查数据库中表的当前状态(它具有哪些列,它们的数据类型等),然后在表中进行必要的更改以使其与模型匹配.编写我们的model 新建model文件夹 在其下面新建 user.model.js

我们的model对象User中一共定义了三个字段user_name、password、is_admin(sequelize创建表会自动创建id)

代码语言:javascript
复制
// 解构出sequelize的DataTypes
const { DataTypes } = require('sequelize')
// 这是我们创建好的 连接数据库的
const seq = require('../db/seq')

// 创建模型   可以给表加前缀因为其自动化推断表名称,也可以让他不推断
const User = seq.define('User', {
    // id 自动创建
    user_name: {
        // 去问档查看
        type: DataTypes.STRING,
        // 约束是否为空
        allowNull: false,
        // 唯一
        unique: true,
        comment: '用户名 唯一'
    },
    password: {
        type: DataTypes.CHAR(64),
        allowNull: false,
        comment: '密码'
    },
    is_admin: {
        // boolean 就是 tinity(1)
        type: DataTypes.BOOLEAN,
        allowNull: false,
        defaultValue: 0,
        comment: '是否为管理员 0不是管理员'
    }
})
// force如果之前存在这张表 会删了重建 文档:模型重建  用过后要注释掉
// User.sync({ force: true })

module.exports = User

创建表

在终端中执行命令

代码语言:javascript
复制
node src/model/user.model.js

其实是执行了一个建表语句

image.png
image.png

去看一下我们新建成的数据库

image.png
image.png

操作数据库

我们已经抽离出了 service层,用于写操作数据库的逻辑。

注册接口的实现

先插进去

user.service.js

代码语言:javascript
复制
// 引入我们写好的model
const User = require('../model/user.model')
class UserService {
    // 因为存入数据库 是异步的
    async createUser(user_name,password)
        // 当我们属性名和传过来的值一致的时候可以简写
        // await表达式: 返回成功promise对象的值
       const res = await User.create({user_name, password})
       console.log(res)
       //    返回到controller
       return res
    }
}
// 导出后 在控制器controller中使用
module.exports = new UserService()
  • sequelize中的create()方法 就相当于我们执行sqlINSERT INTO Users(id,name) VALUES (?,?)
  • 因为是存入数据库的操作,所以是异步执行的。所以用到了 async await
  • 当属性名和传过来的参数的属性名一致的时候可以简写
代码语言:javascript
复制
   User.create({
          user_name: user_name,
          password: password
    })
    
    User.create({user_name,password})
  • 最后把这个res再返回到controller层。

controller 那么controller负责什么呐?

路由找到对应的controller后,接收传来的参数,然后调用我们的service层。

代码语言:javascript
复制
//注意 createUser是异步函数
const { createUser, getUserInfo } = require('../service/user.service')
class UserController {
    async register(ctx, next) {
        // 1.获取数据   注意一定传的要是JSON结构才能对request.body解构
         const { user_name, password } = ctx.request.body
        // 2.操作数据库 将参数传到service层 
         const res = await createUser(user_name, password)
         console.log(res)
        // 3.返回给客户端
            ctx.body = {
                code: 0,
                messgae: '用户注册成功',
                user_name: res.user_name
            }
        
    }
}
// 导出实例化的对象
module.exports = new UserController()

做到这里我们可以在postman里试一试

image.png
image.png

成功插入后会在控制台自动打印

image.png
image.png

我也打印了一下service中的res,可以看到它是这么样的一个对象

image.png
image.png

那么这就做好了吗?当然没有,根本没考虑到用户存在的情况。

错误处理

一些小问题

再插试试

再一次插入相同的值,100%会报错

image.png
image.png
image.png
image.png

Internal Server Error服务器内部错误。

插入空值

正常来说,没填写用户名肯定是不能存到数据库中的,这又是一个问题

image.png
image.png

少传一个参数

image.png
image.png

Internal Server Error服务器内部错误。

因此 我们需要做一些处理。

校验

合法性

首先是合法性,什么是合法 就是传过来的参数不为空。我们需要判断传过来的参数是否为空,那么写在哪个文件里?先写到controller。 解构出参数,判断它们如果为空,返回状态码 400,并在body中返回code码(自己定义),和错误信息。

代码语言:javascript
复制
        const { user_name, password } = ctx.request.body
        // 合法性
        if (!user_name || !password) {
            console.log('用户名或密码为空', ctx.request.body)
            ctx.status = 400
            ctx.body = {
                code: '10001',
                messgae: '用户名或密码为空',
                result: ''
            }
        }

合理性

如果我们已经有了这个用户名的存在,就不可以再注册了。

那这里其实就需要走一个查询接口,看数据库中是否存在这个用户名的用户

在service中写一个新方法

user.service.js

  • User.findOne()查找一条
代码语言:javascript
复制
// 先把参数预留出来 虽然我们这次只需根据user_name查找
 async getUserInfo({id,user_name,password,is_admin}){
        const whereOpt = {}
        // 短路运算
        id && Object.assign(whereOpt,{ id })  // 不为空则添加到whereOpt中
        user_name && Object.assign(whereOpt,{ user_name })
        password && Object.assign(whereOpt,{ password })
        is_admin && Object.assign(whereOpt,{ is_admin })

        const res = await User.findOne({
            attributes:['id', 'user_name', 'password','is_admin'],
            where : whereOpt
        })
        // 可以看一下上面res 的结构
        return res ? res.dataValues : null
    }

controller controller中再新增一个判断,如果返回的这个对象不为空。则说明数据库中存在这个用户。注意调用getUserInfo()需要加await关键字(异步)

代码语言:javascript
复制
 else if (await getUserInfo({ user_name })) {
            ctx.status = 409
            ctx.body = {
                code: '10002',
                messgae: '用户已存在',
                result: ''
            }
        }

controller完整代码

代码语言:javascript
复制
//注意 createUser是异步函数
const { createUser, getUserInfo } = require('../service/user.service')
class UserController {
    async register(ctx, next) {
        // 1.获取数据
        console.log(ctx.request.body)
        const { user_name, password } = ctx.request.body
        // 合法性
        if (!user_name || !password) {
            console.log('用户名或密码为空', ctx.request.body)
            ctx.status = 400
            ctx.body = {
                code: '10001',
                messgae: '用户名或密码为空',
                result: ''
            }
        }
        // 合理性
        else if (await getUserInfo({ user_name })) {
            ctx.status = 409
            ctx.body = {
                code: '10002',
                messgae: '用户已存在',
                result: ''
            }
        }
        else {
            // 2.操作数据库
            console.log(user_name, password, "user_name, password")
            const res = await createUser(user_name, password)
            console.log(res)
            // 3.返回给客户端
            ctx.body = {
                code: 0,
                messgae: '用户注册成功',
                user_name: res.user_name
            }
        }
    }
    async login(ctx, next) {
        ctx.body = '用户登录'
    }
}

// 导出实例化的对象
module.exports = new UserController()

service完整代码

代码语言:javascript
复制
const User = require('../model/user.model')
class UserService {
    // 因为存入数据库 是异步的
    async createUser(user_name,password){
        console.log(user_name,password,"service")
    // 对象
        // User.create({
        //     user_name: user_name,
        //     password: password
        // })
        // 当我们属性名和传过来的值一致的时候可以简写
        // await表达式: 返回成功promise对象的值
       const res = await User.create({user_name, password})
       console.log(res,"res")
    //    返回到controller
       return res
    }
    async getUserInfo({id,user_name,password,is_admin}){
        const whereOpt = {}
        // 短路运算
        id && Object.assign(whereOpt,{ id })  // 不为空则添加到whereOpt中
        user_name && Object.assign(whereOpt,{ user_name })
        password && Object.assign(whereOpt,{ password })
        is_admin && Object.assign(whereOpt,{ is_admin })

        const res = await User.findOne({
            attributes:['id', 'user_name', 'password','is_admin'],
            where : whereOpt
        })
        return res ? res.dataValues : null
    }
}
// 导出后 在控制器controller中使用
module.exports = new UserService()

抽离成中间件

我们的校验都写在了controller中,我们想将这部分抽离出来,抽离成中间件

代码语言:javascript
复制
 // 合法性
        if (!user_name || !password) {
            console.log('用户名或密码为空', ctx.request.body)
            ctx.status = 400
            ctx.body = {
                code: '10001',
                messgae: '用户名或密码为空',
                result: ''
            }
        }
        // 合理性
        else if (await getUserInfo({ user_name })) {
            ctx.status = 409
            ctx.body = {
                code: '10002',
                messgae: '用户已存在',
                result: ''
            }
        }

将用户或密码为空抽离出来

新建文件夹 middleware/user.middle.js

编写函数再把函数导出

代码语言:javascript
复制
const userValidator = async(ctx,next)=>{
    const {user_name, password} = ctx.request.body
      // 合法性
      if (!user_name || !password) {
        console.log('用户名或密码为空', ctx.request.body)
        ctx.status = 400
        ctx.body = {
            code: '10001',
            messgae: '用户名或密码为空',
            result: ''
        }
        // 如果用户名或密码为空 就返回
       return
    }
    // 否则放行向下执行
    await next()
} 
module.exports = {
    userValidator,
}

导出了这个中间件,我们去哪使用那? 去router中

代码语言:javascript
复制
const Router = require('koa-router')
const { register, login } = require('../controller/user.controller')
// 引入中间件
const  {userValidator} = require('../middleware/user.middleware')
const router = new Router({prefix:'/users'})

// 注册接口 先交给userValidator去验证,验证通过再交由register
router.post('/register',userValidator,register)
// 登录接口
router.post('/login', login)
// 导出
module.exports = router

然后测试一下

image.png
image.png

将用户是否存在抽离出来

middleware/user.middle.js 新增一个方法,并且它用到了service的getUserInfo(),所以需要引入service。最后不要忘记导出

代码语言:javascript
复制
const { getUserInfo }  = require('../service/user.service')
const verifyUser = async (ctx, next) => {
    // 合理性
    const { user_name } = ctx.request.body
    if (await getUserInfo({ user_name })) {
        ctx.status = 409
        ctx.body = {
            code: '10002',
            messgae: '用户已存在',
            result: ''
        }
        return
    }
    await next()
}
module.exports = {
    userValidator,verifyUser
}

然后去我们的route文件

代码语言:javascript
复制
const Router = require('koa-router')
const { register, login } = require('../controller/user.controller')
const  {userValidator,verifyUser} = require('../middleware/user.middleware')
const router = new Router({prefix:'/users'})

// 注册接口 先交给userValidator 和 verifyUser去验证,验证通过再交由register
router.post('/register',userValidator,verifyUser,register)

// 登录接口
router.post('/login', login)
// 导出
module.exports = router

再次测试一下

image.png
image.png

将错误信息抽离出来

新建一个constant常量文件夹

err.type.js

代码语言:javascript
复制
module.exports = {
    userFormateError:{
        code : '10001',
        message : '用户名或密码为空',
        result : ''
    },
    userAlreadyExisted:{
        code : '10002',
        message : '用户已存在',
        result : ''
    }
}

然后我们的中间件中 user.middleware.js 再去引入这个 错误信息

代码语言:javascript
复制
const { userFormateError, userAlreadyExisted } = require('../constant/err.type')

将 ctx.body 替换成 ctx.app.emit 方法

代码语言:javascript
复制
        ctx.status = 400
        ctx.body = {
            code: '10001',
            messgae: '用户名或密码为空',
            result: ''
        }
        替换为
        
        // 封装了错误信息 ctx.app.emit在提示错误信息时使用
        ctx.app.emit('error', userFormateError, ctx)

替换后为

代码语言:javascript
复制
const { getUserInfo } = require('../service/user.service')
const { userFormateError, userAlreadyExisted } = require('../constant/err.type')
const userValidator = async (ctx, next) => {
    const { user_name, password } = ctx.request.body
    // 合法性
    if (!user_name || !password) {
        console.log('用户名或密码为空', ctx.request.body)
        
        // 封装了错误信息 ctx.app.emit在提示错误信息时使用
        ctx.app.emit('error', userFormateError, ctx)
        return
    }
    await next()
}

const verifyUser = async (ctx, next) => {
    // 合理性
    const { user_name } = ctx.request.body
    const isExist = await getUserInfo({ user_name })
    if (isExist) {
        ctx.app.emit('error', userAlreadyExisted, ctx)
        return
    }
    await next()
}
module.exports = {
    userValidator, verifyUser
}

emit的错误事件 需要在我们的app的index.js文件中通过app.on进行监听

代码语言:javascript
复制
app.on('error',(err,ctx)=>{

})

刚才我们在user.middleware.js文件中 也将状态码也删掉了,那我们在这个地方再加上。 我们在app文件夹下新建一个errHandler.js 来处理状态码。

代码语言:javascript
复制
module.exports = (err,ctx)=>{
    let status =500
    switch (err.code){
        case '10001':
            status = 400
        break
        case '10002':
            status = 409
        break
        default:
            status = 500
    }
    ctx.status = status
    ctx.body = err
}

然后再在app/index.js中引入

代码语言:javascript
复制
const Koa = require('koa')
const KoaBody = require('koa-body')
const userRouter = require('../router/user.route')
const errHandler = require('./errHandler')
const app = new Koa()

// 在注册路由前注册
app.use(KoaBody())
// 必须是一个函数
app.use(userRouter.routes())

app.on('error', errHandler)
module.exports = app

密码加密

我们之前存入数据库中的密码是没有经过加密的。这篇文章我们对密码进行一下加密处理。其实如果要求不是很高的话,可以使用md5进行加密。

我们这里使用另一个库bycrptjs,📢 是bycrptjs,在npmjs官网还有一个bycrpt,二者区别是bycrptjs进过了c++的编译。不需要其他的依赖,而bycrpt需要其他依赖。

最好对应项目代码进行学习:持续更新中

bycrptjs

npmjs中 bycrptjs 相关内容

安装

代码语言:javascript
复制
npm i bycrptjs

文档

可以看到它分为同步 和 异步。To hash a password:生成密码。To check a password:去验证这个生成的密码

image.png
image.png
image.png
image.png

这段生成密码的代码中 genSaltSync 方法,我们称它为 “加盐”。 它的加密方式大概是:

[密码:abc]-加上字符->[123abc]-通过算法加密一次->[具体变成什么样我们看不到]--再进行加盐--> 下面参数中的10就是进行了十次的加盐。

代码语言:javascript
复制
var bcrypt = require('bcryptjs');
var salt = bcrypt.genSaltSync(10);
var hash = bcrypt.hashSync("B4c0//", salt)

写到哪里?

按照我们的封装,在user.router.js中经过校验之后才会进入到register,因此我们想要加密就需要在进入register前,进行加密。我们将加密也写到 处理中间件的文件中 :user.middleware.js

user.route.js

代码语言:javascript
复制
const  {userValidator,verifyUser,cryptPassword} = require('../middleware/user.middleware')
// 注册接口 先交给userValidator去验证,验证通过再交由register
router.post('/register',userValidator,verifyUser,cryptPassword,register)

user.middleware.js

代码语言:javascript
复制
const cryptPassword = async (ctx, next) => {
    // 解构出密码
    const {password} = ctx.request.body 
    const salt = bcrypt.genSaltSync(10)
    // 保存的是密文
    const hash = bcrypt.hashSync(password,salt) 
    ctx.request.body.password = hash
    await next()
}
// 导出
module.exports = {
    userValidator, verifyUser, cryptPassword
}

然后我们测试一下,注册一个新用户

image.png
image.png

可以看到数据库的相应数据第密码已经经过了加密处理

image.png
image.png

密码的解密我们在下一篇文章再说,也就是在登录时进行解密。

实现登录

接下来实现一下登录的验证。最好先把代码下下来再看,因为我们进行了封装处理。流程大概如下

首先,我们项目的目录结构如下

路由

进入到路由文件中 user.router.js。因为我们的项目结构,都是先进行校验后才会进入到login的controller中。所以在login的路由中新增 用户名和密码是否为空的校验 和 密码是否正确的校验。也就是 userValidator 和 verifyLogin。这两个方法我们写到中间件中。

代码语言:javascript
复制
const Router = require('koa-router')
const { register, login ,getTest} = require('../controller/user.controller')
const  {userValidator,verifyUser,cryptPassword,verifyLogin} = require('../middleware/user.middleware')
const router = new Router({prefix:'/users'})

// 注册接口 先交给userValidator去验证,验证通过再交由register
router.post('/register',userValidator,verifyUser,cryptPassword,register)

// 登录接口
router.post('/login',userValidator,verifyLogin,login)
router.get('/test', getTest),
// 导出
module.exports = router

中间件

middleware/user.middleware.js

用户密码是否为空(这是我们之前写注册就已经写好的)

代码语言:javascript
复制
const userValidator = async (ctx, next) => {
    const { user_name, password } = ctx.request.body
    // 合法性
    if (!user_name || !password) {
        console.log('用户名或密码为空', ctx.request.body)

        // 封装了错误信息 ctx.app.emit在提示错误信息时使用
        ctx.app.emit('error', userFormateError, ctx)
        return
    }
    await next()
}

用户是否存在 存在验证密码是否正确

其中bcrypt.compareSync传入两个密码作为参数,返回值为true/false。

还有具体的报错,我们写到了constant/err.type.js

代码语言:javascript
复制
const verifyLogin = async (ctx, next) => {
    // 1.判断用户是否存在 不存在报错
    const { user_name, password } = ctx.request.body
    try {
        const res = await getUserInfo({ user_name })
        if (!res) {
            console.error('用户不存在', { user_name })
            ctx.app.emit('error', userUnExist, ctx)
            return
        }
        // 2.密码是否匹配 不匹配报错
        // compareSync 返回值是 true false
        if (!bcrypt.compareSync(password, res.password)) {
             ctx.app.emit('error', invalidPassword, ctx)
             return
        }
    } catch (err) {
        console.error(err)
        return ctx.app.emit('error', userLoginError)
    }

    await next()
}

错误提示

constant/err.type.js

代码语言:javascript
复制
     userUnExist:{
        code:'10004',
        message:'用户不存在',
        result:''
    },
    userLoginError:{
        code:'10005',
        message:'用户登录失败',
        result:''
    },
    invalidPassword:{
        code:'10006',
        message:'密码不匹配',
        result:''
    }

controller中

因为我们的错误处理都已经写过了,并且只有错误处理都通过了,我们才会进入到login的controller中。所以只需要在这里写成功的即可

代码语言:javascript
复制
  async login(ctx, next) {
        const {user_name} = ctx.request.body
        ctx.body = `欢迎回来${user_name}`
    }
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-11-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 初始化项目
    • 生成package.json
      • 安装 koa
        • 新建入口文件
          • 自动启动配置文件 nodemon
            • 添加启动命令
              • 读取配置文件
              • 添加路由配置
                • 安装 koa-router
                  • 看一下 koa-router 文档
                    • 抽离出路由文件
                    • 目录结构优化
                      • 抽离出controller
                      • koa-body
                        • 安装
                          • 解析请求数据
                          • 抽离servcie层
                          • 搞下数据库
                            • 什么是Sequelize?
                              • 什么是ORM?
                                • 安装两个包
                                  • 连接到数据库
                                  • 创建模型
                                    • sequelize.define的三个参数
                                      • 模型同步
                                        • 创建表
                                        • 操作数据库
                                          • 注册接口的实现
                                            • 先插进去
                                        • 错误处理
                                          • 一些小问题
                                            • 再插试试
                                            • 插入空值
                                            • 少传一个参数
                                          • 校验
                                            • 合法性
                                            • 合理性
                                            • controller完整代码
                                            • 将用户或密码为空抽离出来
                                            • 将用户是否存在抽离出来
                                            • 将错误信息抽离出来
                                        • 抽离成中间件
                                        • 密码加密
                                          • bycrptjs
                                            • 安装
                                            • 文档
                                            • 写到哪里?
                                        • 实现登录
                                          • 路由
                                            • 中间件
                                              • 错误提示
                                                • controller中
                                                相关产品与服务
                                                数据库
                                                云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                                                领券
                                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档