KOA 介绍
官网地址: https://koa.bootcss.com/
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
文章目录
- KOA 介绍
- 一. 项目的初始化
- 二. 搭建项目
- 1 安装 Koa 框架
- 2 编写最基本的 app
- 3 测试
- 三. 项目的基本优化
- 1 自动重启服务
- 2 读取配置文件
- 3 洋葱模型
- 四. 添加路由
- 1 安装 koa-router
- 2 编写路由
- 3 改写 main.js
- 4.ctx对象
- 五. 目录结构优化
- 1 将 http 服务和 app 业务拆分
- 2 将路由和控制器拆分
- 六. 解析 body
- 1 安装 koa-body
- 2 注册中间件
- 3 解析请求数据
- 4 拆分 service 层
- 5 洋葱模型
- 七. 集成 sequlize
- 1 安装 sequelize
- 2 连接数据库
- 3 编写配置文件
- 八. 创建 User 模型
- 1 拆分 Model 层
- 九. 添加用户入库
- 十. 错误处理
- 十一. 拆分中间件
- 1 拆分中间件
- 2 统一错误处理
- 3 错误处理函数
- 十二. 加密
- 1 安装 bcryptjs
- 2 编写加密中间件
- 3 在 router 中使用
- 十三. 登录验证
- 十四. 用户的认证
- 1 颁发 token
- 1) 安装 jsonwebtoken
- 2) 在控制器中改写 login 方法
- 3) 定义私钥
- 2 用户认证
- 1) 创建 auth 中间件
- 2) 改写 router
- 十五 接口练习
- 十六 koa集成 模板引擎
- EJS 的使用
- art-template
一. 项目的初始化
1 npm 初始化
npm init -y
生成package.json
文件:
- 记录项目的依赖
2 git 初始化
git init
生成’.git’隐藏文件夹, git 的本地仓库
二. 搭建项目
1 安装 Koa 框架
npm install koa
2 编写最基本的 app
创建src/main.js
const Koa = require('koa')const app = new Koa()app.use((ctx, next) => {ctx.body = 'hello world'
})app.listen(3000, () => {console.log('server is running on http://localhost:3000')
})
3 测试
在终端, 使用node src/main.js
三. 项目的基本优化
1 自动重启服务
安装 nodemon 工具
npm i nodemon -D
编写package.json
脚本
"scripts": {"dev": "nodemon ./src/main.js","test": "echo \"Error: no test specified\" && exit 1"
},
执行npm run dev
启动服务
2 读取配置文件
安装dotenv
, 读取根目录中的.env
文件, 将配置写到process.env
中
npm i dotenv
创建.env
文件
APP_PORT=8000
创建src/config/config.default.js
const dotenv = require('dotenv')dotenv.config()// console.log(process.env.APP_PORT)module.exports = process.env
改写main.js
const Koa = require('koa')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 http://localhost:${APP_PORT}`)
})
3 洋葱模型
洋葱模型,就是将数据顺序传入到多个中间件中,让它们进行处理传递,并利用函数递归的特性,让我们可以在一个中间件内先执行前半部分逻辑,再执行之后的所有中间件的完整逻辑后,再掉转方向继续执行这个中间件的后半部分。
const Koa = require('koa');
const app = new Koa();// 中间件1
app.use((ctx, next) => {console.log("<<<1");next();console.log("1>>>");
});// 中间件 2
app.use((ctx, next) => {console.log("<<<2");next();console.log("2>>>");
});// 中间件 3
app.use((ctx, next) => {console.log("<<<3");next();console.log("3>>>");
});
app.listen(8000, '0.0.0.0', () => {console.log(`Server is starting`);
});
在 koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。可以通过下图直观看出:
- 输出的结果
<<<1
<<<2
<<<3
3>>>
2>>>
1>>>
const Koa = require('koa');
const app = new Koa();// 中间件1
app.use(async(ctx, next) => {console.log("<<<1");await next();console.log("1>>>");
});// 中间件 2
app.use(async(ctx, next) => {console.log("<<<2");const axios= require('axios') const res= await axios.get('http://www.baidu.com')next();console.log("2>>>");
});// 中间件 3
app.use((ctx, next) => {console.log("<<<3");next();console.log("3>>>");
});
app.listen(8000, '0.0.0.0', () => {console.log(`Server is starting`);
});
四. 添加路由
路由: 根据不同的 URL, 调用对应处理函数
1 安装 koa-router
npm i koa-router
步骤:
- 导入包
- 实例化对象
- 编写路由
- 注册中间件
2 编写路由
创建src/router
目录, 编写user.route.js
const Router = require('koa-router')const router = new Router({ prefix: '/users' })// GET /users/
router.get('/', (ctx, next) => {ctx.body = 'hello users'
})module.exports = router
3 改写 main.js
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 http://localhost:${APP_PORT}`)
})
- mock 返回
router.get('/api/data', async (ctx) => {// 查询成功返回数据const data = {message: '查询成功',value: 'xx数据',};ctx.body = data;
});router.post('/api/data', async (ctx) => {// 创建数据成功const data = {message: '创建成功',value: 'xx数据',};ctx.body = data;
});router.put('/api/data', async (ctx) => {// 修改成功const data = {message: '修改成功',value: 'xx数据',};ctx.body = data;
});router.delete('/api/data', async (ctx) => {// 删除成功const data = {message: '删除成功',value: 'xx数据',};ctx.body = data;
});
- 链式调用
router.get('/api/data', async (ctx) => {ctx.body = {message: '查询成功',value: 'xx数据',};}).post('/api/data', async (ctx) => {ctx.body = {message: '创建成功',value: 'xx数据',};}).put('/api/data', async (ctx) => {ctx.body = {message: '修改成功',value: 'xx数据',};}).delete('/api/data', async (ctx) => {ctx.body = {message: '删除成功',value: 'xx数据',};});
// 定义处理数据的函数
const handleData = (ctx, method, message) => {const data = {message,value: 'xx数据',};ctx.body = data;
};// 定义路由处理
router.get('/api/data', async (ctx) => {handleData(ctx, '查询成功', 'xx数据');}).post('/api/data', async (ctx) => {handleData(ctx, '创建成功', 'xx数据');}).put('/api/data', async (ctx) => {handleData(ctx, '修改成功', 'xx数据');}).delete('/api/data', async (ctx) => {handleData(ctx, '删除成功', 'xx数据');});
4.ctx对象
在 Koa 框架中,ctx
是 context
的简写。context
是 Koa 内部提供的一个全局变量,它是一个带有请求和响应信息的上下文对象。ctx
对象封装了 context
,提供了访问请求和响应的属性和方法的方便方式。
通过 ctx
对象,你可以访问 ctx.request
、ctx.response
、ctx.cookies
等属性来处理请求、响应和操作 cookie 数据。例如,ctx.request.query
用于获取查询参数,ctx.response.body
用于设置响应体内容,ctx.cookies.set()
用于设置 cookie 数据等。
因此,可以将 ctx
理解为上下文对象 context
的实例化对象,它提供了更简洁和易用的方式来访问和操作请求和响应的相关信息。通过使用 ctx
,你可以在 Koa 应用程序的中间件中轻松地处理和操作请求和响应的数据。
ctx.request
:用于处理请求相关的属性和方法。
router.get('/api/data', async (ctx) => {// 获取请求参数const queryParam = ctx.request.query; // 获取查询参数const bodyParam = ctx.request.body; // 获取请求体参数// 设置响应状态码ctx.response.status = 200;// 设置响应类型ctx.response.type = 'application/json';// 设置响应头ctx.response.set('X-Custom-Header', 'Custom Value');// 发送响应ctx.response.body = { message: '请求成功' };
});
ctx.response
:用于处理响应相关的属性和方法。
router.post('/api/data', async (ctx, next) => {// 获取请求体参数const bodyParam = ctx.request.body;// 设置响应状态码ctx.response.status = 201;// 设置响应类型ctx.response.type = 'application/json';// 设置响应头ctx.response.set('X-Custom-Header', 'Custom Value');// 发送响应ctx.response.body = { message: '创建成功', data: bodyParam };await next();
});
ctx.cookies
:用于操作 cookie。
router.get('/api/cookie', async (ctx) => {// 设置 cookiectx.cookies.set('name', 'value', {httpOnly: true,maxAge: 86400000, // 设置过期时间为一天});// 获取 cookieconst name = ctx.cookies.get('name');ctx.body = { cookie: name };
});
通过这些属性,你可以方便地处理请求的参数、设置响应的状态码、类型、头部信息以及操作 cookie 数据。以上是一些简单的示例,实际使用中可能还会涉及更多的属性和方法,具体的使用取决于你的业务需求。
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');const app = new Koa();
app.use(bodyParser());app.use(async (ctx) => {if (ctx.url === '/api/data' && ctx.method === 'POST') {const bodyData = ctx.request.body;console.log(bodyData); // 获取请求体数据ctx.body = { message: '请求成功', data: bodyData };}
});app.listen(3000, () => {console.log('服务器启动,监听3000端口...');
});
ctx.response.body===ctx.body : 本质上是一个东西
五. 目录结构优化
1 将 http 服务和 app 业务拆分
创建src/app/index.js
const Koa = require('koa')const userRouter = require('../router/user.route')const app = new Koa()app.use(userRouter.routes())module.exports = app
改写main.js
const { APP_PORT } = require('./config/config.default')const app = require('./app')app.listen(APP_PORT, () => {console.log(`server is running on http://localhost:${APP_PORT}`)
})
2 将路由和控制器拆分
路由: 解析 URL, 分布给控制器对应的方法
控制器: 处理不同的业务
改写user.route.js
const Router = require('koa-router')const { register, login } = require('../controller/user.controller')const router = new Router({ prefix: '/users' })// 注册接口
router.post('/register', register)// 登录接口
router.post('/login', login)module.exports = router
创建controller/user.controller.js
class UserController {async register(ctx, next) {ctx.body = '用户注册成功'}async login(ctx, next) {ctx.body = '登录成功'}
}module.exports = new UserController()
六. 解析 body
1 安装 koa-body
npm i koa-body
2 注册中间件
改写app/index.js
3 解析请求数据
改写user.controller.js
文件
const { createUser } = require('../service/user.service')class UserController {async register(ctx, next) {// 1. 获取数据// console.log(ctx.request.body)const { user_name, password } = ctx.request.body// 2. 操作数据库const res = await createUser(user_name, password)// console.log(res)// 3. 返回结果ctx.body = ctx.request.body}async login(ctx, next) {ctx.body = '登录成功'}
}module.exports = new UserController()
4 拆分 service 层
service 层主要是做数据库处理
创建src/service/user.service.js
class UserService {async createUser(user_name, password) {// todo: 写入数据库return '写入数据库成功'}
}module.exports = new UserService()
5 洋葱模型
洋葱模型,就是将数据顺序传入到多个中间件中,让它们进行处理传递,并利用函数递归的特性,让我们可以在一个中间件内先执行前半部分逻辑,再执行之后的所有中间件的完整逻辑后,再掉转方向继续执行这个中间件的后半部分。
const Koa = require('koa');
const app = new Koa();// 中间件1
app.use((ctx, next) => {console.log("<<<1");next();console.log("1>>>");
});// 中间件 2
app.use((ctx, next) => {console.log("<<<2");next();console.log("2>>>");
});// 中间件 3
app.use((ctx, next) => {console.log("<<<3");next();console.log("3>>>");
});
app.listen(8000, '0.0.0.0', () => {console.log(`Server is starting`);
});
在 koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。可以通过下图直观看出:
- 输出的结果
<<<1
<<<2
<<<3
3>>>
2>>>
1>>>
const Koa = require('koa');
const app = new Koa();// 中间件1
app.use(async(ctx, next) => {console.log("<<<1");await next();console.log("1>>>");
});// 中间件 2
app.use(async(ctx, next) => {console.log("<<<2");const axios= require('axios') const res= await axios.get('http://www.baidu.com')next();console.log("2>>>");
});// 中间件 3
app.use((ctx, next) => {console.log("<<<3");next();console.log("3>>>");
});
app.listen(8000, '0.0.0.0', () => {console.log(`Server is starting`);
});
七. 集成 sequlize
sequelize ORM 数据库工具
当我们在开发 Node.js 应用程序时,经常需要与数据库进行交互来存储和检索数据。这时候,Sequelize就成了我们的得力助手。Sequelize是一个强大的ORM(对象关系映射)库,它帮助我们以更直观的方式操作数据库,而无需编写复杂的SQL查询语句。
简单来说,ORM是一种编程技术,让我们能够使用面向对象的方法来处理数据库。而Sequelize正是为此而生。它让我们可以用JavaScript代码定义模型和数据之间的关系,就像在创建类和对象一样简单。每个模型类对应数据库中的一张表,而实例则是表中的记录。
Sequelize的好处不胜枚举。首先,它支持多种流行的数据库,比如 PostgreSQL、MySQL、SQLite和MSSQL,让我们在选择数据库时更加灵活。其次,Sequelize允许我们在模型上设置验证规则,确保数据的合法性和完整性。这为我们避免了许多繁琐的数据验证工作,让数据操作更加安全可靠。
查询构建是Sequelize的又一个亮点。它提供了一套流畅的查询构建器,让我们可以轻松地创建复杂的数据库查询。不再需要手动编写冗长的SQL语句,Sequelize可以帮我们处理这些细节,让我们专注于业务逻辑的实现。
如果你担心数据库模式的变更会让开发变得混乱,别担心!Sequelize还支持数据库迁移,让我们可以轻松地管理数据库模式的更新和变更。这样,我们可以在应用程序的生命周期中,保持数据库结构的清晰和一致。
最后,Sequelize还提供了强大的关联功能,让我们可以轻松地定义模型之间的关联关系,比如一对一、一对多、多对多等。这样的话,我们可以轻松地进行跨表查询,从而更加高效地处理复杂的数据需求。
总的来说,Sequelize是一个非常强大且灵活的Node.js ORM库。它简化了数据库操作,提高了开发效率,并且让我们的代码更易于维护。无论是开发小型应用还是大型项目,Sequelize都能成为我们最好的朋友,让我们更加专注于业务逻辑的实现,而不用为繁杂的数据库操作而烦恼。
能让我们以面向对象的方式去操作数据库
ORM: 对象关系映射
- 数据表映射(对应)一个类
- 数据表中的数据行(记录)对应一个对象
- 数据表字段对应对象的属性
- 数据表的操作对应对象的方法
1 安装 sequelize
npm i mysql2 sequelize
2 连接数据库
src/db/seq.js
const { Sequelize } = require('sequelize')const {MYSQL_HOST,MYSQL_PORT,MYSQL_USER,MYSQL_PWD,MYSQL_DB,
} = require('../config/config.default')const seq = new Sequelize(MYSQL_DB, MYSQL_USER, MYSQL_PWD, {host: MYSQL_HOST,dialect: 'mysql',
})seq.authenticate().then(() => {console.log('数据库连接成功')}).catch((err) => {console.log('数据库连接失败', err)})module.exports = seq
3 编写配置文件
- 第一种方式
const dotenv = require('dotenv')dotenv.config()
// process.env 根据目录识别的
// ../config/config_default.js
// module.export= {key:value} 第一种方式
module.exports = {MYSQL_HOST: 'localhost',MYSQL_PORT: '3306',MYSQL_USER: 'root',MYSQL_PWD: 'root',MYSQL_DB: 'auth',APP_PORT:'3000'
};// module.exports=process.env
- 第二种方式
const dotenv = require('dotenv')dotenv.config()
// process.env 根据目录识别的
// ../config/config_default.js
// module.export= {key:value} 第一种方式
// module.exports = {
// MYSQL_HOST: 'localhost',
// MYSQL_PORT: '3306',
// MYSQL_USER: 'root',
// MYSQL_PWD: 'root',
// MYSQL_DB: 'auth',
// APP_PORT:'3000'
// };module.exports=process.env
APP_PORT=3001MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PWD=root
MYSQL_DB=auth
八. 创建 User 模型
1 拆分 Model 层
sequelize 主要通过 Model 对应数据表
创建src/model/user.model.js
const { DataTypes } = require('sequelize');
const seq = require('../db/db_sequ');const User = seq.define('zd_user', {user_name: {type: DataTypes.STRING,allowNull: false,},password: {type: DataTypes.STRING,allowNull: false,},is_admin: {type: DataTypes.INTEGER,defaultValue: 0,},
}, {tableName: 'zd_user',timestamps: false,
});module.exports = User;
- 对应sql
-- auth.zd_user definitionCREATE TABLE `zd_user` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_name` varchar(255) NOT NULL,`password` varchar(255) NOT NULL,`is_admin` int(1) DEFAULT '0',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;INSERT INTO auth.zd_user
(id, user_name, password, is_admin)
VALUES(5, 'root', 'root123', 0);
九. 添加用户入库
所有数据库的操作都在 Service 层完成, Service 调用 Model 完成数据库操作
改写src/service/user.service.js
const User = require('../model/use.model')class UserService {async createUser(user_name, password) {// 插入数据// User.create({// // 表的字段// user_name: user_name,// password: password// })// await表达式: promise对象的值const res = await User.create({ user_name, password })// console.log(res)return res.dataValues}
}module.exports = new UserService()
同时, 改写user.controller.js
const { createUser } = require('../service/user.service')class UserController {async register(ctx, next) {// 1. 获取数据// console.log(ctx.request.body)const { user_name, password } = ctx.request.body// 2. 操作数据库const res = await createUser(user_name, password)// console.log(res)// 3. 返回结果ctx.body = {code: 0,message: '用户注册成功',result: {id: res.id,user_name: res.user_name,},}}async login(ctx, next) {ctx.body = '登录成功'}
}module.exports = new UserController()
十. 错误处理
在控制器中, 对不同的错误进行处理, 返回不同的提示错误提示, 提高代码质量
const { createUser,getUerInfo } = require('../service/user.service')// 发起请求 -》 controller -》 service --》 model -》 操作数据 -》
//使用了 sequelize 完成对数据的保存操作
class UserController {// 方法被 async 修饰了,调用处必须加上awaitasync register(ctx, next) {// 1. 获取数据// console.log(ctx.request.body)const { username, password } = ctx.request.body// 合法性if (!username || !password) {console.error('用户名或密码为空', ctx.request.body)ctx.status = 400ctx.body = {code: '10001',message: '用户名或密码为空',result: '',}return}// 合理性if (await getUerInfo({ user_name:username })) {ctx.status = 409ctx.body = {code: '10002',message: '用户已经存在',result: '',}return}// 2. 操作数据库const res = await createUser(username, password)// console.log(res)// 3. 返回结果ctx.body = {code: 0,message: '用户注册成功',result: {id: res.id,user_name: res.user_name,},}}async login(ctx, next) {ctx.body = '登录成功'}}module.exports = new UserController()
在 service 中封装函数
const User = require('../model/user.model')// Service 处理了业务逻辑
class UserService {async createUser(user_name, password) {const userData = { user_name, password };const res = await User.create(userData);return res;}// 查询 用户名是否存在, trueasync getUerInfo({ id, user_name, password, is_admin }) {const whereOpt = {} console.log('user_name:',user_name)id && Object.assign(whereOpt, { id })user_name && Object.assign(whereOpt, { user_name })// User.findOne 查询 用户名是否存在const res = await User.findOne({attributes: ['id', 'user_name', 'password', 'is_admin'],where: whereOpt,})return res ? res.dataValues : null}}module.exports = new UserService()
十一. 拆分中间件
为了使代码的逻辑更加清晰, 我们可以拆分一个中间件层, 封装多个中间件函数
1 拆分中间件
添加src/middleware/user.middleware.js
const { getUerInfo } = require('../service/user.service')
const { userFormateError, userAlreadyExited } = require('../constant/err.type')const userValidator = async (ctx, next) => {const { user_name, password } = ctx.request.body// 合法性if (!user_name || !password) {console.error('用户名或密码为空', ctx.request.body)// 抛出给下方报错的地方ctx.app.emit('error', userFormateError, ctx)return}await next()
}const verifyUser = async (ctx, next) => {const { user_name } = ctx.request.bodyif (getUerInfo({ user_name })) {// 抛出给报错的地方ctx.app.emit('error', userAlreadyExited, ctx)return}await next()
}module.exports = {userValidator,verifyUser,
}
2 统一错误处理
- 在出错的地方使用
ctx.app.emit
提交错误 - 在 app 中通过
app.on
监听
编写统一的错误定义文件
module.exports = {userFormateError: {code: '10001',message: '用户名或密码为空',result: '',},userAlreadyExited: {code: '10002',message: '用户已经存在',result: '',},
}
3 错误处理函数
module.exports = (err, ctx) => {let status = 500switch (err.code) {case '10001':status = 400breakcase '10002':status = 409breakdefault:status = 500}ctx.status = statusctx.body = err
}
改写app/index.js
const errHandler = require('./errHandler')
// 统一的错误处理
app.on('error', errHandler)
// 注册接口
router.post('/register', userValidator, verifyUser, register)
十二. 加密
bcryptjs 是Node.js中比较简洁好用的一款第三方加盐(salt)加密包,并且支持跨平台的特性,用于实现在Node环境下重要资源的加密。
由它加密的文件可在所有支持的操作系统和处理器上进行转移。它的口令必须是8至56个字符,并将在内部被转化为448位的密钥。
在将密码保存到数据库之前, 要对密码进行加密处理
123123abc (加盐) 加盐加密
1 安装 bcryptjs
npm i bcryptjs
2 编写加密中间件
const crpytPassword = async (ctx, next) => {const { password } = ctx.request.bodyconst salt = bcrypt.genSaltSync(10)// hash保存的是 密文const hash = bcrypt.hashSync(password, salt)ctx.request.body.password = hashawait next()
}
const verifyUser = async (ctx, next) => {const { username } = ctx.request.body// if (await getUerInfo({ user_name })) {// ctx.app.emit('error', userAlreadyExited, ctx)// return// }try {const res = await getUerInfo({ user_name:username })if (res) {console.error('用户名已经存在', { username })ctx.app.emit('error', userAlreadyExited, ctx)return}} catch (err) {console.error('获取用户信息错误', err)ctx.app.emit('error', userRegisterError, ctx)return}await next()
}
3 在 router 中使用
改写user.router.js
const Router = require('koa-router')const {userValidator,verifyUser,crpytPassword,
} = require('../middleware/user.middleware')
const { register, login } = require('../controller/user.controller')const router = new Router({ prefix: '/users' })// 注册接口
router.post('/register', userValidator, verifyUser, crpytPassword, register)// 登录接口
router.post('/login', login)module.exports = router
十三. 登录验证
流程:
- 验证格式
- 验证用户是否存在
- 验证密码是否匹配
改写src/middleware/user.middleware.js
const bcrypt = require('bcryptjs')const { getUerInfo } = require('../service/user.service')
const {userFormateError,userAlreadyExited,userRegisterError,userDoesNotExist,userLoginError,invalidPassword,
} = require('../constant/err.type')const userValidator = async (ctx, next) => {const { user_name, password } = ctx.request.body// 合法性if (!user_name || !password) {console.error('用户名或密码为空', ctx.request.body)ctx.app.emit('error', userFormateError, ctx)return}await next()
}const verifyUser = async (ctx, next) => {const { user_name } = ctx.request.body// if (await getUerInfo({ user_name })) {// ctx.app.emit('error', userAlreadyExited, ctx)// return// }try {const res = await getUerInfo({ user_name })if (res) {console.error('用户名已经存在', { user_name })ctx.app.emit('error', userAlreadyExited, ctx)return}} catch (err) {console.error('获取用户信息错误', err)ctx.app.emit('error', userRegisterError, ctx)return}await next()
}const crpytPassword = async (ctx, next) => {const { password } = ctx.request.bodyconst salt = bcrypt.genSaltSync(10)// hash保存的是 密文const hash = bcrypt.hashSync(password, salt)ctx.request.body.password = hashawait next()
}const verifyLogin = async (ctx, next) => {// 1. 判断用户是否存在(不存在:报错)const { user_name, password } = ctx.request.bodytry {const res = await getUerInfo({ user_name })if (!res) {console.error('用户名不存在', { user_name })ctx.app.emit('error', userDoesNotExist, ctx)return}// 2. 密码是否匹配(不匹配: 报错)if (!bcrypt.compareSync(password, res.password)) {ctx.app.emit('error', invalidPassword, ctx)return}} catch (err) {console.error(err)return ctx.app.emit('error', userLoginError, ctx)}await next()
}module.exports = {userValidator,verifyUser,crpytPassword,verifyLogin,
}
定义错误类型
module.exports = {userFormateError: {code: '10001',message: '用户名或密码为空',result: '',},userAlreadyExited: {code: '10002',message: '用户已经存在',result: '',},userRegisterError: {code: '10003',message: '用户注册错误',result: '',},userDoesNotExist: {code: '10004',message: '用户不存在',result: '',},userLoginError: {code: '10005',message: '用户登录失败',result: '',},invalidPassword: {code: '10006',message: '密码不匹配',result: '',},
}
改写路由
// 登录接口
router.post('/login', userValidator, verifyLogin, login)
十四. 用户的认证
官方介绍:JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对签名。
https://jwt.io/
登录成功后, 给用户颁发一个令牌 token, 用户在以后的每一次请求中携带这个令牌.
jwt: jsonwebtoken
- header: 头部
- payload: 载荷
- signature: 签名
1 颁发 token
1) 安装 jsonwebtoken
npm i jsonwebtoken
2) 在控制器中改写 login 方法
async login(ctx, next) {const { user_name } = ctx.request.body// 1. 获取用户信息(在token的payload中, 记录id, user_name, is_admin)try {// 从返回结果对象中剔除password属性, 将剩下的属性放到res对象const { password, ...res } = await getUerInfo({ user_name })ctx.body = {code: 0,message: '用户登录成功',result: {token: jwt.sign(res, JWT_SECRET, { expiresIn: '1d' }),},}} catch (err) {console.error('用户登录失败', err)}
}
3) 定义私钥
在.env
定义
JWT_SECRET = xzd
2 用户认证
1) 创建 auth 中间件
const jwt = require('jsonwebtoken')const { JWT_SECRET } = require('../config/config.default')const { tokenExpiredError, invalidToken } = require('../constant/err.type')const auth = async (ctx, next) => {const { authorization } = ctx.request.headerconst token = authorization.replace('Bearer ', '')console.log(token)try {// user中包含了payload的信息(id, user_name, is_admin)const user = jwt.verify(token, JWT_SECRET)ctx.state.user = user} catch (err) {switch (err.name) {case 'TokenExpiredError':console.error('token已过期', err)return ctx.app.emit('error', tokenExpiredError, ctx)case 'JsonWebTokenError':console.error('无效的token', err)return ctx.app.emit('error', invalidToken, ctx)}}await next()
}module.exports = {auth,
}
- 登录生成 toekn 后, 后续任意接口都需要携带 token 进行 访问
2) 改写 router
- 具体操作之前, 验签
// 修改密码接口
router.patch('/', auth, (ctx, next) => {console.log(ctx.state.user)ctx.body = '修改密码成功'
})
十五 接口练习
- 安装依赖
{"name": "test","version": "1.0.0","main": "index.js","license": "MIT","dependencies": {"dotenv": "^16.3.1","koa": "^2.14.2","koa-router": "^11.0.2","mysql2": "^2.2.5","nodemon": "^3.0.1"},"scripts": {"dev": "nodemon ./main.js"}
}
数据库名称 test_spiritmark_db
数据库用户 admin_spiritmark
数据库密码 55726955c65bd84c
数据库地址 mysql.sqlpub.com:3306
注册邮箱 liuluchang@cathay-ins.com.cn
注意 密码只显示一次,请注意保存。
为保障数据库运行安全,密码严禁泄漏到公共环境!!!(如:csdn、zhihu、github等)发现即永久锁定。
- 测试sql
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT NOT NULL
);INSERT INTO users (name, age) VALUES
('John Doe', 25),
('Jane Smith', 30),
('Mike Johnson', 35);select * from users;
- 启动应用
const Koa = require('koa');
const Router = require('koa-router');
const mysql = require('mysql2/promise');const app = new Koa();
const router = new Router();// 创建数据库连接池
const pool = mysql.createPool({host: 'mysql.sqlpub.com',user: 'admin_spiritmark',port:3306,password: '55726955c65bd84c',database: 'test_spiritmark_db',
});// 添加数据
router.post('/api', async (ctx) => {const { name, age } = ctx.request.body;try {const [result] = await pool.query('INSERT INTO users (name, age) VALUES (?, ?)', [name, age]);ctx.body = result;} catch (error) {ctx.status = 500;ctx.body = error.message;}
});// 删除数据
router.del('/api/:id', async (ctx) => {const { id } = ctx.params;try {const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]);ctx.body = result;} catch (error) {ctx.status = 500;ctx.body = error.message;}
});// 更新数据
router.put('/api/:id', async (ctx) => {const { id } = ctx.params;const { name, age } = ctx.request.body;try {const [result] = await pool.query('UPDATE users SET name = ?, age = ? WHERE id = ?', [name, age, id]);ctx.body = result;} catch (error) {ctx.status = 500;ctx.body = error.message;}
});// 查询数据
router.get('/api', async (ctx) => {const page = parseInt(ctx.query.page) || 1;const size = parseInt(ctx.query.size) || 10;const offset = (page - 1) * size;try {const [rows] = await pool.query('SELECT * FROM users LIMIT ?, ?', [offset, size]);ctx.body = rows;} catch (error) {ctx.status = 500;ctx.body = error.message;}
});// 启动应用程序
app.use(router.routes()).use(router.allowedMethods());const PORT = 3000;
app.listen(PORT, () => {console.log(`Server is running on port ${PORT}`);
});
- 封装优化
const Koa = require('koa');
const Router = require('koa-router');
const mysql = require('mysql2/promise');const app = new Koa();
const router = new Router();// 创建数据库连接池
const createPool = () => {return mysql.createPool({host: 'mysql.sqlpub.com',user: 'admin_spiritmark',port:3306,password: '55726955c65bd84c',database: 'test_spiritmark_db',});
};// 添加数据
const addUser = async (name, age) => {const pool = createPool();try {const [result] = await pool.query('INSERT INTO users (name, age) VALUES (?, ?)', [name, age]);return result;} catch (error) {throw new Error(error.message);} finally {pool.end();}
};// 删除数据
const deleteUser = async (id) => {const pool = createPool();try {const [result] = await pool.query('DELETE FROM users WHERE id = ?', [id]);return result;} catch (error) {throw new Error(error.message);} finally {pool.end();}
};// 更新数据
const updateUser = async (id, name, age) => {const pool = createPool();try {const [result] = await pool.query('UPDATE users SET name = ?, age = ? WHERE id = ?', [name, age, id]);return result;} catch (error) {throw new Error(error.message);} finally {pool.end();}
};// 查询数据
const getUsers = async (page = 1, size = 10) => {const pool = createPool();const offset = (page - 1) * size;try {const [rows] = await pool.query('SELECT * FROM users LIMIT ?, ?', [offset, size]);return rows;} catch (error) {throw new Error(error.message);} finally {pool.end();}
};// 添加数据
router.post('/api', async (ctx) => {const { name, age } = ctx.request.body;try {const result = await addUser(name, age);ctx.body = result;} catch (error) {ctx.status = 500;ctx.body = error.message;}
});// 删除数据
router.del('/api/:id', async (ctx) => {const { id } = ctx.params;try {const result = await deleteUser(id);ctx.body = result;} catch (error) {ctx.status = 500;ctx.body = error.message;}
});// 更新数据
router.put('/api/:id', async (ctx) => {const { id } = ctx.params;const { name, age } = ctx.request.body;try {const result = await updateUser(id, name, age);ctx.body = result;} catch (error) {ctx.status = 500;ctx.body = error.message;}
});// 查询数据
router.get('/api', async (ctx) => {const page = parseInt(ctx.query.page) || 1;const size = parseInt(ctx.query.size) || 10;try {const rows = await getUsers(page, size);ctx.body = rows;} catch (error) {ctx.status = 500;ctx.body = error.message;}
});// 启动应用程序
app.use(router.routes()).use(router.allowedMethods());const PORT = 3000;
app.listen(PORT, () => {console.log(`Server is running on port ${PORT}`);
});
十六 koa集成 模板引擎
EJS 的使用
-
- 使用模板引擎
# 在项目中下载koa-views包
npm i koa-views
# 在项目中下载ejs包
npm i ejs
-
- 入口index.js
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
// 1. 引入koa-views
const views = require('koa-views');
// 2. 配置模板引擎中间件:views()第一个参数是视图模板所在的路径,第二个参数是应用ejs模板引擎
app.use(views('views', { extension: 'ejs' })); // 若这样配置,模板的后缀名是.ejs
//app.use(views('views', { map: { html: 'ejs' } })); // 若这样配置,模板的后缀名是.html// 我们需要在每个路由的render中都渲染一个公共的数据
// 写一个中间件配置公共信息
app.use(async (ctx, next) => {// 公共的数据要放在ctx.state中ctx.state = {userinfo: '张三',age: '18',}await next()
})router.get('/', async (ctx, next) => {let title = '你好ejs'// 3. 异步渲染模板await ctx.render('index.ejs', {// 绑定数据title: title})
})router.get('/news', async (ctx, next) => {let arr = ['111111', '22222', '444444']let content = "<h2>这是一个h2</h2>"let num = 123await ctx.render('news.ejs', {list: arr,content: content,num: num})
});app.use(router.routes());
app.use(router.allowedMethods());app.listen(3000, () => {console.log('starting at port 3000');
});
- /views/index.ejs:
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ejs模板引擎</title>
</head>
<body><%- include('./public/header.ejs') %>这是一个ejs模板引擎<h2><%=title%>-----<%=userinfo%></h2>
</body>
</html>
- /views/news.ejs:
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>ejs数据绑定</title>
</head>
<body><h2>引入外部模块</h2><%- include('./public/header.ejs') %><h2>ejs循环渲染数据</h2><ul><%for(var i=0;i<list.length;i++){%><li><%=list[i]%></li><%}%></ul><h2>绑定html数据</h2><%-content%><h2>条件判断</h2><% if(num>24){ %>大于24<%} else{ %> 小于24<%} %><h2>公共数据</h2><%=userinfo%>
</body>
</html>
art-template
原始语法和ejs一模一样都用<% %>,标准语法用的是{{ }},这里代码示例用的是标准语法。
- 安装所需包
npm install --save art-template
npm install --save koa-art-template
- 入口index.js
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
const path = require('path');
// 1. 引入koa-art-template
const render = require('koa-art-template');
// 2. 配置art-template模板引擎
render(app, {root: path.join(__dirname, 'views'), // 视图的位置extname: '.html', // 后缀名debug: process.env.NODE_ENV !== 'production' // 是否开启调试模式
});// 我们需要在每个路由的render中都渲染一个公共的数据
// 写一个中间件配置公共信息
app.use(async (ctx, next) => {// 公共的数据要放在ctx.state中ctx.state = {userinfo: '张三',age: '18',}await next()
})router.get('/', async (ctx, next) => {let title = '你好koa-art-template'// 3. 异步渲染模板await ctx.render('index', {// 绑定数据title: title})
})router.get('/news', async (ctx, next) => {let arr = ['111111', '22222', '444444']let content = "<h2>这是一个h2</h2>"let num = 123await ctx.render('news', {list: arr,content: content,num: num})
});app.use(router.routes());
app.use(router.allowedMethods());app.listen(3000, () => {console.log('starting at port 3000');
});
- /views/news.html:
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>数据绑定</title>
</head>
<body><h2>引入外部模块</h2>{{include './public/header.html'}}<h2>绑定数据</h2>{{list.name}}<h2>公共数据</h2>{{userinfo}}<h2>绑定html数据</h2>{{@list.h}}<h2>条件判断</h2>{{if num>20}}<span>大于20</span>{{else}}<span>小于20</span>{{/if}}<h2>循环渲染数据</h2><ul>{{each list.data}}<li>{{$index}}--{{$value}}</li>{{/each}}</ul>
</body>
</html>