# 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'

什么是jwt?

# 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图片上传:

最全阿里云node.js上传

首先我们需要在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()

在这里插入图片描述

在这里插入图片描述

Last Updated: 8/1/2021, 1:43:20 PM