# Koa
解决问题:
- callback嵌套问题
- 异步函数中可能同步调用callback返回数据,带来不一致性
使用koa2之前,确认node.js版本高于v7.6,才能完全支持async/await。
简单使用:
const koa = require('koa')
const app = new koa()
// 配置路由
// 中间件
/* express写法
app.use((req,res,next) =>{
res.send('返回数据')
})
*/
app.use(async (ctx,next) => {
ctx.body = '你好 koa2'
})
app.listen(3000)
# koa路由
路由就是根据不同的URL地址,加载不同的页面实现不同的功能。
koa中的路由和Express有所不同,在Express中直接引入Express就可以配置路由,但是在Koa中我们需要安装对应的koa-router路由模块来实现
npm i --save koa-router
const Koa = require('koa')
/*引入方式一:
const Router = require('koa-router')
const router = new Router()
*/
/*引入方式二:*/
const router = require('koa-router')()
const app = new Koa()
// ctx包含了request和response等信息
router.get('/',async (ctx,next)=>{
ctx.body = 'hello koa'/*返回数据 相当于原生里面的res.writeHead() res.end()*/
}).get('/news',async (ctx,next)=>{
ctx.body = '新闻页面'
})
app.use(router.routes())// 作用:启动路由
.use(router.allowedMethods())// 作用:这是官方文档的推荐用法,我们可以看到router.allowedMethods()用在了路由匹配router.routes()之后,所以在当所有路由中间件执行之后调用,此时根据ctx.status设置response响应头。(可以配置也可以不配置;建议配置)(我的理解:自动处理请求状态响应头)
app.listen(3000,()=>{
console.log('start at port 3000');
})
# Get传值
在koa2中GET传值通过request接受,但是接收的方法有两种:query和querystring
- query:返回的是格式化好的参数对象
- querystring:返回的是请求字符串
// GET传值
router.get('/newscontent',async (ctx) => {
// 访问地址http://localhost:3000/newscontent?aid=233&name=zhangsan
// 方式一:从ctx中读取get传值
console.log(ctx.query)// { aid:'233', name:'zhangsan'}获取的是对象,用的最多的方式
console.log(ctx.querystring)// aid=233&name=zhangsan获取的是一个字符串
console.log(ctx.url)// /newscontent?aid=233&name=zhangsan
// 方式二:ctx里面的request里面获取get传值
console.log(ctx.request.query)// { aid:'233', name:'zhangsan' }
console.log(ctx.request.querystring)// aid=233&name=zhangsan
console.log(ctx.request.url)// /newscontent?aid=233&name=zhangsan
ctx.body = '新闻详情'
})
# 动态路由
// 动态路由里面可以传多个值
router.get('/article/:aid/:cid', async (ctx) => {
// 访问地址http://localhost:3000/newscontent/123/456
// 获取动态路由的传值
console.log(ctx.params)// { aid:'123',cid:'456'}
})
# koa中间件
中间件的功能:(通过next来实现)
- 执行任何代码
- 修改请求和响应对象
- 终结请求-响应循环
- 调用堆栈中的下一个中间件
app.use(async (ctx,next)=>{
console.log(new Date());
await next()// 当前路由匹配完成以后继续向下匹配
})
// 匹配任何路由的时候都会执行
路由级中间件:
router.get('/news',async (ctx,next)=>{
console.log('这是一个新闻1');
await next();// 继续向下匹配
})
router.get('/news',async (ctx)=>{
ctx.body = '这是一个新闻'
})
错误处理中间件:(先去执行app.use再去匹配路由,与代码顺序无关)
app.use(async (ctx,next)=>{
console.log('这是一个中间件01')
next();
if(ctx.status === 404){// 如果页面找不到
ctx.status = 404;
ctx.body = '这是一个404页面'
}
})
# koa洋葱模型
koa中间件执行流程区别于express:类似于洋葱模型的执行方式。
而express是根据代码顺序执行。
const router = require('koa-router')()
app.use(async (ctx,next)=>{
console.log('1、这是第一个中间件01');
await next();
console.log('5、匹配路由完成以后又会返回来执行中间件')
})
app.use(async (ctx,next)=>{
console.log('2、这是第一个中间件02');
await next();
console.log('4、匹配路由完成以后又会返回来执行中间件')
})
router.get('/',async (ctx)=>{
console.log('3、匹配到首页这个路由');
ctx.body='这是首页'
})
app.use(router.routes())
上面的代码最终打印顺序是:
1、这是第一个中间件01
2、这是第一个中间件02
3、匹配到首页这个路由
4、匹配路由完成以后又会返回来执行中间件
5、匹配路由完成以后又会返回来执行中间件
# koa中使用ejs模板
1、安装koa-views和ejs
npm i koa-views --save
npm i ejs --save
2、引入koa-views配置中间件
const views = require('koa-views');
app.use(views('views',{map: {html: 'ejs'}}));
//这样配置也可以:注意如果这样配置的话,模板的后缀名是.html
app.use(views('views', { extension: 'ejs' }));
//这样配置也可以:注意如果这样配置的话,模板的后缀名是.ejs
实操:
const koa = require('koa')
const app = new koa()
/*
ejs模板的使用:
1、npm i koa-views --save
2、npm i ejs --save
3、app.use(views('views',{ extension: 'ejs'}))// 第一个参数是目录名,此时的项目下有一个views目录/index.ejs
4、await ctx.render('index')
*/
const views = require('koa-views');
//配置模板引擎中间件--第三方中间件
//app.use(views('views',{map: {html: 'ejs'}}));//这样配置也可以:注意如果这样配置的话,模板的后缀名是.html
app.use(views('views', { extension: 'ejs' }));
const router = require('koa-router')()
router.get('/', async (ctx) => {
await ctx.render('index')// 使用ejs模板
})
app.use(router.routes())// 作用:启动路由
.use(router.allowedMethods())
app.listen(3000)
# koa-bodyparser中间件的使用
作用:koa(post)提交数据
# 1】原生nodejs获取post提交数据
// 异步获取数据
const getPostData = (ctx) => {
return new Promise((resolve, reject) => {
try {
let str = '';
ctx.req.on('data', (chunk) => {
str += chunk;
})
ctx.req.on('end', () => {
resolve(str)
})
} catch (error) {
reject(error)
}
})
}
// 原生获取post提交的数据
router.post('/doAdd', async (ctx) => {
// 获取表单提交的数据
const data = await getPostData(ctx);
console.log(data)
ctx.body = data
})
# 2】koa中koa-bodyparser中间件使用
①安装npm i --save koa-bodyparser
②引入模块const bodyParser=require(koa-bodyparser)
③app.use(bodyParser())
④ctx.request.body
获取表单post传值
const Koa = require('koa')
const bodyParser = require('koa-bodyparser');
const app = new Koa();
app.use(bodyParser())
app.use(async ctx => {// 中间件给ctx的body更换属性
ctx.body = ctx.request.body;
})
# koa-static静态资源中间件
让路由优先匹配处理返回静态资源,找不到再返回逻辑接口。
①npm i koa-static --save
②const static = require('koa-static')
③配置中间件app.use(static('static'))
const static = require('koa-static')
app.use(static('./static'))// 去项目目录下的static目录下找
app.use(static('./public'))// 可配置多个静态目录,继续去public目录下面找
# koa中cookie的使用
// 设置cookie
ctx.cookies.set(name,value,[options])
// 获取cookie
ctx.cookies.get('name')
options名称 | options值 |
---|---|
maxAge | 一个数字表示从Date.now()得到的毫秒数 |
exprires | cookie过期的Date |
path | cookie路径,默认是'/' |
domain | cookie域名 |
secure | 安全cookie默认false,设置成true表示,只有https可以访问 |
httpOnly | 是否只是服务器可访问cookie,默认是true |
overwrite | 一个布尔值,表示是否覆盖以前设置的同名的cookie(默认是false)如果是true在同一个请求中设置相同的cookie(不管路径或域)是否在设置此cookie时从set-cookie标头中过滤掉。 |
router.get('/',async (ctx)=>{
// 正常这样配置就可以使用
ctx.cookies.set('userInfo','zhangsan',{// 注意:koa中没法直接设置中文的cookie
maxAge:60*1000*60// 一小时后过期
})
})
router.get('/shop',async (ctx)=>{
const userInfo = ctx.cookies.get('userInfo')
console.log(userInfo)// zhangsan
})
# koa中设置中文cookie
(nodejs中才有Buffer对象可以使用)
// 首先将中文 转成 base64字符串
console.log(new Buffer('我是雷浩').toString('base64'))// 5oiR5piv6Zu35rWp
//再将base64字符串 还原成 中文
console.log(new Buffer('5oiR5piv6Zu35rWp','base64').toString())// 我是雷浩
实际项目中使用场景:
router.get('/cookie', async (ctx) => {
// 正常这样配置就可以使用
// 注意:koa中没法直接设置中文的cookie
const data = new Buffer('我是雷浩').toString('base64')
ctx.cookies.set('userInfo', data, {
maxAge: 60 * 1000 * 60// 一小时后过期
})
})
router.get('/cookie/getcookie', async (ctx) => {
const data = ctx.cookies.get('userInfo')
console.log(new Buffer(data, 'base64').toString())// 我是雷浩
})
# koa中session的使用
session保存在服务器端
session的工作流程:(基于cookie)
当浏览器访问服务器并发送第一次请求时,服务器端会创建一个seesion对象,生成一个类似于key、value的键值对,然后将key(cookie)返回到浏览器端,浏览器下次再访问时,携带key(cookie),找到对应的session(value)。客户的信息都保存在session中。
- 安装koa-session
npm i koa-session --save
- 引入koa-session
const session = require('koa-session')
- 设置官方文档提供的中间件
app.keys = ['some secret hurr'];
const CONFIG = {
key: 'koa.sess',
maxAge: 86400000,
autoCommit: true,
overwrite: true,
httpOnly: true,
signed: true,
rolling: false,
renew: false,
secure: true,
sameSite: null,
};
app.use(session(CONFIG, app));
- 使用
// 设置值
ctx.session.username = '张三'
// 获取值
ctx.session.username
# mongoDB可视化工具
MongoDB Compass Community(官方提供的)
# 封装MongoDB
单例模式:(es6类静态方法实现单例)
- 应用场景:想要实现只创建一次数据库连接再来操作数据库
class Db {
static getInstance(){/*实现单例(只实例化一次)*/
if(!Db.instance){
Db.instance = new Db()
}
return Db.instance
}
constructor {
console.log('实例化会触发构造函数')
this.connect()
}
connect() {
console.log('连接数据库')
}
find() {
console.log('查询数据库')
}
}
var myDb1 = Db.getInstance()
var myDb2 = Db.getInstance()
var myDb3 = Db.getInstance()
var myDb4 = Db.getInstance()
# 官方提供的nodejs使用mongoDB的操作:
const http = require('http')
const app = require('./module/route')
const { MongoClient } = require('mongodb')
const url = 'mongodb://localhost:27017'
const dbName = 'rayhomie'
//此时,每次请求都是用的相同的实例client
const client = new MongoClient(url, { useUnifiedTopology: true })
//注册web服务
http.createServer(app).listen(3000);
//配置路由
app.get('/',(req,res)=>{
client.connect((err)=>{
if(err){
console.log(err);
return;
}
const db =client.db(dbName)
//查询数据
db.collection('user').find({}).toArray((err,data)=>{
if(err) return;
console.log(data)
client.close()//操作数据库之后一定要关闭连接
res.send(data)
})
})
})
# 实际项目中需要需要对数据库的操作进行封装:
// 封装db类库
const { MongoClient } = require('mongodb')
const { dbUrl, dbName } = require('./config')
class Db {
static getInstance() {// 为了实现单例模式(多次实例化实例不共享的问题)
if (!Db.instance) {
Db.instance = new Db()
}
return Db.instance
}
constructor() {
this.dbClient = '';/*构造函数中存属性放db对象(解决数据库多次连接问题而提升效率)*/
this.connect()
}
connect() {/*连接数据库*/
return new Promise((resolve, reject) => {
if (!this.dbClient) {
MongoClient.connect(dbUrl, { useUnifiedTopology: true }, (err, client) => {
if (err) {
reject(err);
return;
}
const db = client.db(dbName)
this.dbClient = db// 存放在构造函数中(为了解决数据库多次连接问题)
resolve(this.dbClient)
})
} else {
// 如果构造函数中有dbClient对象
resolve(this.dbClient)
}
})
}
find(collectionName, json) {
return new Promise((resolve, reject) => {
this.connect().then((db) => {
const result = db.collection(collectionName).find(json)
result.toArray((err, docs) => {
if (err) {
reject(err);
return;
}
resolve(docs)
})
})
})
}
insert(collectionName, json) {
return new Promise((resolve, reject) => {
this.connect().then((db) => {
db.collection(collectionName).insertOne(json, (err, docs) => {
if (err) {
reject(err);
return;
}
resolve(docs)
})
})
})
}
update(collectionName, json1, json2) {
return new Promise((resolve, reject) => {
this.connect().then((db) => {
db.collection(collectionName).updateOne(
json1,
{ $set: json2 },
(err, docs) => {
if (err) {
reject(err);
return;
}
resolve(docs)
})
})
})
}
delete(collectionName, json) {
return new Promise((resolve, reject) => {
this.connect().then((db) => {
db.collection(collectionName).removeOne(json, (err, docs) => {
if (err) {
reject(err);
return;
}
resolve(docs)
})
})
})
}
}
module.exports = Db.getInstance()
// const myDB1 = Db.getInstance()
// setTimeout(() => {
// console.time('time1')
// myDB1.find('user', {}).then(data => {
// // console.log(data)
// console.timeEnd('time1');
// })
// }, 100)
// setTimeout(() => {
// console.time('time2')
// myDB1.find('user', {}).then(data => {
// // console.log(data)
// console.timeEnd('time2');
// })
// }, 1000)
// const myDB2 = Db.getInstance()
// setTimeout(() => {
// console.time('time3')
// myDB2.find('user', {}).then(data => {
// // console.log(data)
// console.timeEnd('time3');
// })
// }, 1500)
// setTimeout(() => {
// console.time('time4')
// myDB2.find('user', {}).then(data => {
// // console.log(data)
// console.timeEnd('time4');
// })
// }, 2000)
注意:如果需要查询id的话
db.user.find({"_id":ObjectId("5aad299bc16236421c99d229")})
# koa脚手架级路由模块化
- 全局安装脚手架
npm i koa-generator -g
- 创建项目
koa koa_demo
- 安装依赖
npm i
// 目录层级
|-routes
|-admin.js
|
|-app.js
|-package.json
——————————————————————————————————————————————————————————————
// app.js
const Koa = require('koa')
const app = new Koa()
const router = require('koa-router')()
const admin = require('./routes/admin')
router.get('/', async (ctx) => {
ctx.body = '这是一个首页'
})
// 配置admin 层级子路由
router.use('/admin', admin)
app.use(router.routes())// 作用:启动路由
.use(router.allowedMethods())
app.listen(3000, () => {
console.log('已启动3000端口')
})
————————————————————————————————————————————————————————————
// routes/admin.js
// 后台管理模块
const router = require('koa-router')()
router.get('/', async (ctx) => {
ctx.body = '后台管理模块首页'
})
router.get('/add', async (ctx) => {
ctx.body = '添加数据'
})
router.get('/delete', async (ctx) => {
ctx.body = '删除数据'
})
router.get('/update', async (ctx) => {
ctx.body = '更新数据'
})
module.exports = router.routes()
# 使用JWT对Koa2进行token认证
npm install jsonwebtoken koa-jwt
/jwt.js
const crypto = require("crypto"),
jwt = require("jsonwebtoken"),
user = require("./model/user/index"),
secret = require('./secret.json');
// TODO:使用数据库
// 这里应该是用数据库存储,这里只是演示用
// let userList = [{ name: 'rayhomie', password: crypto.createHash('md5').update('123456').digest('hex') }];
class UserController {
// 用户登录
static async login(ctx, callback) {
const data = ctx.request.query;
if (!data.username || !data.password) {
return ctx.body = {
code: "000002",
message: "参数不合法"
}
}
const result = await user.findOne({ username: data.username, password: crypto.createHash('md5').update(data.password).digest('hex') })
if (result) {
const token = jwt.sign(
{
name: result.username
},
secret.sign, // secret
{ expiresIn: 60 * 60 } // 60 * 60 s
);
return ctx.body = {
code: "0",
message: "登录成功",
data: {
token
}
};
} else {
return ctx.body = {
code: "000002",
message: "用户名或密码错误"
};
}
}
}
module.exports = UserController;
/app.js
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
// const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
const jwtKoa = require('koa-jwt')
const secret = require('./secret.json');
const index = require('./routes/index')
// error handler
// onerror(app)
// middlewares
app.use(bodyparser({
enableTypes: ['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(views(__dirname + '/views', {
extension: 'pug'
}))
// 除去一些没必要通过jwt验证
app.use(jwtKoa({ secret: secret.sign }).unless({
path: [/^\/api\/login/, /^\/api\/register/]
}))
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// routes
app.use(index.routes(), index.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
module.exports = app
/routes/api/index.js
const router = require('koa-router')()
const jwt = require('../../jwt')
const crypto = require("crypto")
const user = require('../../model/user/index')
router.get('/', async (ctx, next) => {
await ctx.render('index', {
title: 'Hello Koa 2!'
})
})
// 登录
router.get('/login', async (ctx, next) => {
await jwt.login(ctx)
})
// 注册
router.get('/register', async (ctx, next) => {
const value = await user.register({
username: 'zzz1',
password: crypto.createHash('md5').update('123456').digest('hex'),
avatar_url: 's',
gender: 1,
mobile_number: '123213'
})
ctx.body = value
})
module.exports = router.routes()
请求头设置为:'Authorization': 'Bearer token'
# koa中获取form-data传来的数据
之前的koa-bodyparser中间件只支持获取post传值的JSON等格式数据。
而我们才用更新的koa-body中间件来配置时我们可以获取multipart/form-data的数据
app.use(koaBody({ multipart: true }))
// 全局使用中间件,传入配置项(即支持文件传输)
一般使用axios时需要使用form-data传值时,我们不自己设置content-type:multipart/form-data,而是交给axios自动设置content-type
multipart/form-data; boundary=something
//对于多部分实体,boundary 是必需的,其包括来自一组字符的1到70个字符,已知通过电子邮件网关是非常健壮的,而不是以空白结尾。它用于封装消息的多个部分的边界。
解决bug:使用axios发送form-data数据的时候,不需要设置'Content-Type': 'multipart/form-data'
,如果强制设置content-type的话,multipart/form-data
后面就会自动添加boundary的解析。
# koa中使用阿里云oss图片上传:
首先我们需要在koa项目中安装sdk依赖:ali-oss
const router = require('koa-router')()
const OSS = require('ali-oss');
const moment = require('moment')
const client = new OSS({
region: 'oss-cn-chengdu',//
accessKeyId: 'LTAI5tALXBEYXwDqEprXEqDT',
accessKeySecret: 'jqFq4W8l8jj4KUH1jMDzewg8vZpFQm',
bucket: 'personal-financ'// 使用oss需要创建一个bucket(类似于文件夹的东西)
});
// 上传图片到oss
router.post('/picture', async (ctx, next) => {
const data = ctx.request.body//使用koa-body才能获取到form-data格式的传参
console.log(data._parts[0]);
var options = {
// progress: progress, //可以拿到文件上传进度;用于写进度条
partSize: 500 * 1024,
meta: {
people: 'rayhomie'
},
timeout: 60000,
}
//objectKey, file, options三个参数分别是:objectKey阿里云上buket中的虚拟文件地址(String);file是读取的文件,注意这里是一个文件;options见上定义的options
const res = await client.multipartUpload(
`${moment().format('YYYYMMDDHHmmss.png')}`,//上传的文件名
data._parts[0][1].path,//formdata中的path属性值
options)//配置项
if (res.res.status === 200 && res.res.statusMessage === 'OK') {
ctx.body = { res: res.res, code: 0 }
} else {
ctx.body = { err: res.res, code: 1, info: '上传失败' }
}
})
module.exports = router.routes()