Skip to content
一、koa2

官网参考:https://koa.bootcss.com/

  • express 中间件是异步回调,koa2 原生支持 async/await
  • 新开发框架和系统,都开始基于 koa2,例如 egg.js
  • express 虽然过时,但是 koa2 肯定是未来的趋势
1. async/await 语法,安装和使用 koa2
  • async/await 语法
    • await 后面可以追加 promise 对象,获取 resolve 的值
    • await 必须包裹在 async 函数里面
    • async 函数执行返回的也是一个 promise 对象
    • try-catch 截获 promise 中 reject 的值
  • koa2
    • 安装(使用脚手架)
      shell
      npm i koa-generator -g
      Koa2 koa2-test
      npm i & npm run dev
    • 初始化代码,处理路由
      • 环境配置
        shell
        npm i cross-env -S 
        
        "dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www",
        "prd": "cross-env NODE_ENV=production pm2 start bin/www",
      • app.js
        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 index = require('./routes/index')
        const users = require('./routes/users')
        
        // 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'
        }))
        
        // 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())
        app.use(users.routes(), users.allowedMethods())
        
        // error-handling
        app.on('error', (err, ctx) => {
          console.error('server error', err, ctx)
        });
        
        module.exports = app
2. 开发接口、连接数据库、实现登录、日志记录

和 express 类似 基于 koa-generic-session 和 koa-redis ioredis

  • 插件安装

    shell
    npm i koa-generic-session koa-redis ioredis -S
  • session redis 在app.js中的配置

    javascript
    const session = require('koa-generic-session')
    const redisStore = require('koa-redis')
    
      app.use(session({
      // 配置 cookie
      cookie: {
        path: '/',
        httpOnly: true,
        maxAge: 24 * 60 * 60 * 1000
      },
      // 配置 redis
      store: redisStore({
        all: '127.0.0.1:6379'
      })
    }))
  • 路由

    • 插件安装
      shell
      npm i mysql xss -S
    • 相关配置
      • mysql redis

        js
        const env = process.env.NODE_ENV;
        
        // 配置
        let MYSQL_CONF
        let REDIS_CONF
        
        if (env === 'dev') {
          MYSQL_CONF = {
            host: '127.0.0.1',
            user: 'root',
            password: '123qwe!@#',
            port: '3306',
            database: 'myblog'
          }
          REDIS_CONF = {
            port: 6379,
            // host: '139.129.240.151'
            host: '127.0.0.1'
          }
        }
        
        
        if (env === 'production') {
          MYSQL_CONF = {
            host: 'localhost',
            user: 'root',
            password: '123qwe!@#',
            port: '3306',
            database: 'myblog'
          }
          REDIS_CONF = {
            port: 6379,
            // host: '139.129.240.151',
            host: '127.0.0.1'
          }
        }
        module.exports = {
          MYSQL_CONF,
          REDIS_CONF
        }
      • mysql 工具封装

        js
        const mysql = require('mysql');
        const { MYSQL_CONF } = require('../config/db');
        
        // 创建连接对象
        const con = mysql.createConnection(MYSQL_CONF);
        
        // 开始连接
        con.connect()
        
        // 统一执行 sql 的函数
        function exec(sql) {
          const promise = new Promise((resolve, reject) => {
            con.query(sql, (err, result) => {
              if (err) {
                reject(err)
                return;
              }
              resolve(result)
            })
          })
          return promise;
        }
        
        module.exports = {
          exec,
          escape: mysql.escape
        }
        js
        const crypto = require('crypto');
        
        // 秘钥
        const SECRET_KEY = 'ASDASD_12';
        
        // md5 加密
        function md5(content) {
          let md5 = crypto.createHash('md5');
          return md5.update(content).digest('hex');
        }
        
        // 加密函数
        function genPassword(password) {
          const str = `password=${password}&key=${SECRET_KEY}`;
          return md5(str);
        }
        
        module.exports = {
          genPassword
        }
      • 中间件

        js
        const { ErrorModel } = require('../model/resModel');
        
        module.exports = async (ctx, next) => {
          if (req.session.username) {
            await next()
            return;
          }
          ctx.body = new ErrorModel('未登录')
        }
      • controller

        js
        const { exec } = require('../db/mysql')
        
        const getList = async (author, keyword) => {
          let sql = `select * from blogs where 1=1 `
          if (author) {
            sql += `and author = '${author}' `
          }
          if (keyword) {
            sql += `and title like '%${keyword}%' `
          }
          sql += `order by createtime desc;`
          return await exec(sql)
        }
        
        const getDetail = async (id) => {
          const sql = `select * from blogs where id = '${id}'`;
          const rows = await exec(sql);
          return rows[0];
        }
        
        const newBlog = async (blogData = {}) => {
          const { title, content, author } = blogData;
          const createTime = Date.now();
          const sql = `
            insert into blogs (title, content, createtime, author)
            values ('${title}', '${content}', '${createTime}', '${author}')
          `
          const data = await exec(sql);
          return {
            id: data.insertId
          }
        }
        const updateBlog = async (id, blogData = {}) => {
          const {title, content} = blogData;
          const sql = `
            update blogs set title='${title}', content='${content}' where id='${id}'
          `
          const updateData = await exec(sql);
          if (updateData.affectedRows > 0) {
            return true
          }
          return false
        }
        
        const deleteBlog = async (id, author) => {
          const sql = `delete from blogs where id = '${id}' and author='${author}'`
          const delData = await exec(sql);
          if (delData.affectedRows > 0) {
            return true
          }
          return false
        }
        
        
        module.exports = {
          getList,
          getDetail,
          newBlog,
          updateBlog,
          deleteBlog
        }
      • router

        js
        const router = require('koa-router')()
        const { getList, getDetail, newBlog, updateBlog, deleteBlog } = require('../controller/blog');
        const { SuccessModel, ErrorModel } = require('../model/resModel');
        const loginCheck = require('../middleware/loginCheck')
        router.prefix('/api/blog')
        
        router.get('/list', async function (ctx, next) {
          const author = ctx.query.author || '';
          const keyword = ctx.query.keyword || '';
          const result = await getList(author, keyword);
          ctx.body = new SuccessModel(result)
        })
        
        router.get('/detail', async function (ctx, next) {
          const result = await getDetail(ctx.query.id);
          ctx.body = new SuccessModel(result);
        })
        
        router.post('/new', loginCheck, async function (ctx, next) {
          const body = ctx.request.body
          body.author = ctx.session.username; // 假数据
          const result = await  newBlog(body);
          ctx.body = new SuccessModel(result);
        })
        router.post('/update', loginCheck, async function (ctx, next) {
          const result = await  updateBlog(ctx.query.id, ctx.request.body);
          ctx.body = result? new SuccessModel(): new ErrorModel('更新博客失败');
        })
        router.post('/del', loginCheck, async function (ctx, next) {
          const author = ctx.session.username;
          const result = deleteBlog(ctx.query.id, author);
          ctx.body = result? new SuccessModel(): new ErrorModel('删除博客失败');
        })
        
        
        module.exports = router
      • app.js

        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 index = require('./routes/index')
        const users = require('./routes/users')
        
        // 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'
        }))
        
        // 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())
        app.use(users.routes(), users.allowedMethods())
        
        // error-handling
        app.on('error', (err, ctx) => {
          console.error('server error', err, ctx)
        });
        
        module.exports = app
  • 日志

  • access log 记录,使用 morgan
  • 自定义日志使用 console.log 和 console.error
  • 日志文件拆分、日志文件分析
  • 插件安装
    shell
    npm i koa-morgan -S
  • 配置app.js
    javascript
    const path = require('path');
    const fs = require('fs');
    const morgan = require('koa-morgan');
    
    const ENV = process.env.NODE_ENV;
    if (ENV !== 'production') {
      app.use(morgan('dev') );
    } else {
      const logFileName = path.resolve(__dirname, 'logs', 'access.log')
      const writeStream = fs.createWriteStream(logFileName, {
        flags: 'a'
      })
      app.use(morgan('combined', {
        stream: writeStream
      }) );
    }
3. 分析koa2中间件原理
js
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);

// 执行顺序 5 6 12 14 20 15 7
  • 分析

    • app.use 用来注册中间件,先收集起来
    • 实现 next 机制,即上一个通过 next 触发下一个
    • 不涉及 method 和 path 的判断
  • 模拟实现

    js
    const http = require('http');
    
    // 组合中间件
    function compose(middlewareList) {
      return function (ctx) {
        function dispatch(i) {
          const fn = middlewareList[i];
          try {
            return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err);
          }
        }
        return dispatch(0)
      }
    }
    
    class LikeKoa2 {
      constructor() {
        this.middlewareList = [];
      }
    
      use(fn) {
        this.middlewareList.push(fn);
        return this;
      }
    
      createContext(req, res) {
        const ctx = {
          req,
          res
        }
        return ctx
      }
    
      handlerRequest(ctx, fn) {
        return fn(ctx)
      }
    
      callBack() {
        const fn = compose(this.middlewareList);
        return (req, res) => {
          const ctx = this.createContext(req, res);
          return this.handlerRequest(ctx, fn);
        }
      }
    
      listen(...args) {
        const server = http.createServer(this.callBack());
        server.listen(...args);
      }
    }
    
    module.exports = LikeKoa2
    js
    const Koa = require('./like-koa2');
    const app = new Koa();
    
    // 非常典型的洋葱圈模型
    // logger
    app.use(async (ctx, next) => {
      console.log('第一层洋葱 - 开始')
      await next();
      const rt = ctx['X-Response-Time'];
      console.log(`${ctx.req.method} ${ctx.req.url} - ${rt}`);
      console.log('第一层洋葱 - 结束')
    });
    
    // x-response-time
    app.use(async (ctx, next) => {
      console.log('第二层洋葱 - 开始')
      const start = Date.now();
      await next();
      const ms = Date.now() - start;
      ctx['X-Response-Time'] = `${ms}ms`;
      console.log('第二层洋葱 - 结束')
    });
    
    // response
    app.use(async ctx => {
      console.log('第三层洋葱 - 开始')
      ctx.res.end('This is like koa2')
      console.log('第三层洋葱 - 结束')
    });
    
    app.listen(8004);

Released under the MIT License.