# koa2 框架

  • express 中间件是异步回调,koa2 原生支持 async/await
  • egg.js 也是基于 koa2;

# async/await 语法

  1. await 后面可以追加 promise 对象,获取 resolve 的值
  2. await 必须包裹在 async 函数里面
  3. async 函数执行返回的也是一个 promise 对象
  4. try-catch 截获 promise 中 reject 的值
function getFileContent(fileName) {
  const promise = new Promise((resolve, reject) => {
    // 读取文件的操作
  })
  return promise
}

// promise实现
getFileContent('a.json').then(aData => {
  console.log(aData)
  return getFileContent(aData.next)
}).then(bData => {
  console.log(bData)
  return getFileContent(bData.next)
}).then(cData => {
  console.log(cData)
})


// 同步的写法
async function readFileData() {
  // promise的resolve里面的内容直接赋值给了aData
  const aData = await getFileContent('a.json')
  console.log('a data', aData)
  const bData = await getFileContent(aData.next)
  console.log('b data', bData)
  const cData = await getFileContent(bData.next)
  console.log('c data', cData)
}
readFileData()


// 下面的这种方法也是可以的  async 函数执行的时候返回的还是 promise
async function readData() {
  const aData = await getFileContent('a.json')
  return aData
}

async function test() {
  const aData = await readData()
  console.log(aData)
}
test()

# 使用 koa2

首先安装:npm install koa-generator -g, koa2 脚手架; 新建项目:koa2 project-koa2

# 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')
// post data
const bodyparser = require('koa-bodyparser')
// 日志
const logger = require('koa-logger')
// 实现登录
const session = require('koa-generic-session')
const redisStore = require('koa-redis')
const path = require('path')
const fs = require('fs')
const morgan = require('koa-morgan')

const index = require('./routes/index')
const users = require('./routes/users')
const blog = require('./routes/blog')
const user = require('./routes/user')
// 引入redis 配置
const { REDIS_CONF } = require('./config/db')

// error handler
onerror(app)

// middlewares
// 处理 postData,接收处理很多格式
app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))
// 处理 post 请求的数据,获取 content-type=applica/json 格式的数据,可以通过 req.body 获取数据
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))

app.use(views(__dirname + '/views', {
  extension: 'pug'
}))

// logger  --中间件是一个异步函数

// 所谓洋葱圈模型,是一个中间件就是一层,中间件开始就是第一层的开始,
// 如果遇到next函数,就会跳转到第二层,等到依次执行下去之后,就开始从最后一层next函数后面继续执行,直到
// 第一层执行到next后面的代码,整个结束
// 碰到next() 方法就会跳转到下一个中间件去执行。
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  // 等待下一个中间件执行完之后再继续执行下面的代码
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// 日志记录
const ENV = process.env.NODE_ENV
// https://github.com/expressjs/morgan 查看打印日志的格式
if(ENV != 'production') {
  // 对日志进行配置
  app.use(morgan('dev', {
    // 标准输出,直接输出到控制台
    stream: process.stdout
  }));
} else {
  // 线上环境使用,打印的日志比较详细--将日志写到access.log文件当中
  const logFileName = path.join(__dirname, 'logs', 'access.log')
  const writeStream = fs.createWriteStream(logFileName, {
    flags: 'a'
  })
  app.use(morgan('combined', {
    stream: writeStream
  }))
}

// session 配置
app.keys = ['jiegiser_#812*']
app.use(session({
  // 配置 cookie
  cookie: {
    path: '/',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000
  },
  // 配置 redis
  store: redisStore({
    // all: '127.0.0.1:6379', // 写死本地的 redis
    all: `${REDIS_CONF.host}:${REDIS_CONF.port}`
  })
}))
// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
// 注册blog路由
app.use(blog.routes(), blog.allowedMethods())
app.use(user.routes(), user.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

module.exports = app

可以使用 corss-env 来设置环境变量:npm install cross-env --save-dev ; 然后在命令中添加:cross-env NODE_ENV=dev:

  "scripts": {
    "start": "node bin/www",
    "dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www",
    "prd": "cross-env NODE_ENV=production pm2 start pm2.conf.json"
  },

# 路由写法

const router = require('koa-router')()
// get 请求
router.get('/string', async (ctx, next) => {
  ctx.body = 'koa2 string'
})

router.get('/json', async (ctx, next) => {
  // 获取 query 参数
  const query = ctx.query
  ctx.body = {
    title: 'koa2 json'
  }
})
// post 请求
// 异步 async
router.post('/login', async (ctx, next) => {
  // 获取到 post 中 data
  const { username, password } = ctx.request.body
  const data = await login(username, password)
  if(data.username) {
    ctx.body = {
      username,
      password
    }
    return
  }
  ctx.body = new ErrorModel('登录失败')
})
module.exports = router

# 中间件机制

  • app.use(callback) 注册中间件;callback 都是一个 async 格式的函数,返回 promise 对象
  • next 机制;返回一个 promise 对象。

# koa2 中实现 session

express 一样,基于 koa-generic-sessionkoa-redis; 首先进行安装:npm install koa-generic-session koa-redis redis --save

使用:

// 实现登录
const session = require('koa-generic-session')
const redisStore = require('koa-redis')
// 引入redis 配置
const { REDIS_CONF } = require('./config/db')
// session 配置
app.keys = ['jiegiser_#812*']
app.use(session({
  // 配置 cookie
  cookie: {
    path: '/',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000
  },
  // 配置 redis
  store: redisStore({
    // all: '127.0.0.1:6379', // 写死本地的 redis
    all: `${REDIS_CONF.host}:${REDIS_CONF.port}`
  })
}))

./config/db 文件:

const env = process.env.NODE_ENV //环境变量

// 配置
let MYSQL_CONF
let REDIS_CONF

if(env === 'dev') {
  MYSQL_CONF = {
    host: 'localhost',
    user: 'root',
    password: '',
    port: '3306',
    database: 'myblog'
  }

  // redis
  REDIS_CONF = {
    port: 6379,
    host: '127.0.0.1'
  }
}

if(env === 'production') {
  MYSQL_CONF = {
    host: 'localhost',
    user: 'root',
    password: '',
    port: '3306',
    database: 'myblog'
  }
  // dedis
  REDIS_CONF = {
    port: 6379,
    host: '127.0.0.1'
  }
}
module.exports = {
  MYSQL_CONF,
  REDIS_CONF
}

使用:存储用户信息

// 存储用户信息 session
// 异步 async
router.post('/login', async (ctx, next) => {
  const { username, password } = ctx.request.body
  const data = await login(username, password)
  if(data.username) {
    ctx.session.username = data.username
    ctx.session.realname = data.realname
    ctx.body = new SuccessModel()
    return
  }
  ctx.body = new ErrorModel('登录失败')
})
// 读取 session 用户信息

const { ErrorModel } = require('../model/resModel')
// 写一个中间件
module.exports = async (ctx, next) => {
  if(ctx.session.username) {
    // 已经登录了
    await next()
    return
  }
  ctx.body = new ErrorModel('未登录')
}

实现一个完整的登录:

// app.js 中注册路由
const user = require('./routes/user')
app.use(user.routes(), user.allowedMethods())


// './routes/user' 文件---注意这里的 await 的 login 方法。
const router = require('koa-router')()
const { login } = require('../controller/user')
const { SuccessModel, ErrorModel } = require('../model/resModel')
// 路由前缀
router.prefix('/api/user')
// 异步 async
router.post('/login', async (ctx, next) => {
  const { username, password } = ctx.request.body
  const data = await login(username, password)
  if(data.username) {
    ctx.session.username = data.username
    ctx.session.realname = data.realname
    ctx.body = new SuccessModel()
    return
  }
  ctx.body = new ErrorModel('登录失败')
})
module.exports = router


// '../controller/user'   注意这里的 login 方法为 async 异步
const { exec, escape } = require('../db/mysql')
const { genPassword } = require('../utils/cryp')
const login = async (username, password) => {
  // 防止sql注入
  username = escape(username)
  // 生成加密密码 ---密码没有进行加密,所以取消验证加密后的密码---需要在登录的时候进行密码的加密存储到数据库中
  // password = genPassword(password)
  password = escape(password)
  console.log(username, password)
  // 删除单引号
  const sql = `select username,realname from users where username =${username} and password =${password}`
  const rows = await exec(sql)
  return rows[0] || {}
}
module.exports = {
  login
}

# 中间件的使用

自定义一个中间件,用于检查是否登录:

const { ErrorModel } = require('../model/resModel')
// 写一个中间件
module.exports = async (ctx, next) => {
  if(ctx.session.username) {
    // 已经登录了
    await next()
    return
  }
  ctx.body = new ErrorModel('未登录')
}

使用:

const loginCheck = require('../middleware/loginCheck')
router.post('/new', loginCheck, async (ctx, next) => {
  const body = ctx.request.body
  body.author = ctx.session.username
  const data = await newBlog(body)
  ctx.body = new SuccessModel(data)
})

# 处理日志

还是使用 morgan 实现对日志的处理:

需要安装 npm install koa-morgan --save 插件;

// 注意这里的 koa-logger 只是美化了在控制台打印日志的效果。并不处理日志文件
// const logger = require('koa-logger')

const path = require('path')
const fs = require('fs')
const morgan = require('koa-morgan')

// 日志记录
const ENV = process.env.NODE_ENV
// https://github.com/expressjs/morgan 查看打印日志的格式
if(ENV != 'production') {
  // 对日志进行配置
  app.use(morgan('dev', {
    // 标准输出,直接输出到控制台
    stream: process.stdout
  }));
} else {
  // 线上环境使用,打印的日志比较详细--将日志写到access.log文件当中
  const logFileName = path.join(__dirname, 'logs', 'access.log')
  const writeStream = fs.createWriteStream(logFileName, {
    flags: 'a'
  })
  app.use(morgan('combined', {
    stream: writeStream
  }))
}

# koa2 中间件原理

# 执行过程

如下面代码: 不使用 koa 脚手架,直接安装 npm install koa --save

const Koa = require('koa');
const app = new Koa();

// logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

上面的执行过程,首先执行 logger 遇到 await next() 跳转到下一个中间件 x-response-time,又遇到 await next() 跳转到下一个中间件 response ,然后 response 中间件执行完成,继续返回到 x-response-time 中间件,执行 await next() 后面的语句,执行完之后,继续返回到 logger 中间件的 await next() 后面,继续执行,至此整个请求过程完成。

这就是洋葱圈模型:

在这里插入图片描述

# 如何实现

  • app.use() 用来注册中间件,先收集注册的中间件
  • 实现 next 机制,用来触发下一个中间件的执行

实现一个简单的 koa:

const http = require('http')
// 组合中间件
function compose(middlewareList) {
  return ctx => {
    // 中间件调用
    function dispath(i) {
      const fn = middlewareList[i]
      try {
        return Promise.resolve(
          // 第二个参数这样实现了 next 的机制
          fn(ctx, dispath.bind(null, i + 1))
        )
      } catch(err) {
        return Promise.reject(err)
      }
    }
    return dispath(0)
  }
}

class LikeKoa2 {
  constructor() {
    // 中间件存储的地方
    this.middlewareList = []
  }
  use(fn) {
    this.middlewareList.push(fn)
    // 可以一直.use()进行注册中间件
    return this
  }
  createContext(req, res) {
    const ctx = {
      req,
      res
    }
    ctx.query = req.query
    return ctx
  }
  handleRequest(ctx, fn) {
    return fn(ctx)
  }
  callback() {
    const fn = compose(this.middlewareList)
    return (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }
  }
  listen(...args) {
    const server = http.createServer(this.callback())
    server.listen(...args)
  }
}

module.exports = LikeKoa2

评 论:

更新: 11/21/2020, 7:00:56 PM