node-koa2入门
中间件:https://github.com/koajs/koa/wiki#middleware
学习教程:
项目地址:https://github.com/Datura35422/nodejs-demo-jiudao
概念
Koa2 被推出的原因
express 和 koa 框架都是相同的作者 TJ 写的。
express 框架内置了很多中间件,主要面对是企业级开发,底层采用的是 ES5 的方式。
koa2 出现主要是小而精,没有内置中间件,所以中间件自己安装,底层采用的是 ES7 的方式编写的。
两个框架的相关 api 大同小异。
koa2 与 express 区别
express
- 中间件调用呈直线型,即串行。使用 callback 的方式进行调用中间件,next() 函数只能在中间件底部使用,中间件一个一个往下执行。
 - 基础Connect中间件,自身封装了路由、视图处理等功能。
 - 弊端是callback回调方式,不可组合、异常不可捕获。
 - express 的路由是自身集成的;
 - express 采用传统的函数形式 function 进行启动;
 
koa2
- 中间件呈U型,即“洋葱模型”。使用 Promise 的方式进行顺序调用中间件。所以中间件的 next() 函数可以使用 
async / await的方式进行调用,中间件一层一层往下执行,到达中心后再一层一层往外执行,因此是洋葱模型。 - 利用co作为底层运行框架,利用Generator的特性,实现“无回调”的异步处理。
 - 利用 async 函数、Koa2丢弃回调函数,增强错误处理。
 - 低级中间件层中提供高级“语法糖”,提高了互操性、稳健性。
 - Koa2新增了一个Context对象,用来代替Express的Request和Response,作为请求的上下文对象。 还有Node原生提供的req、res、socket等对象。
 - Koa的需要引入中间件Koa-router。
 - koa采用new Koa()方式启动;
 
使用区别
参考链接:https://juejin.cn/post/6844903968041091080
| koa(Router = require(‘koa-router’)) | express(假设不使用app.get之类的方法) | |
|---|---|---|
| 初始化 | const app = new koa() | const app = express() | 
| 实例化路由 | const router = Router() | const router = express.Router() | 
| app级别的中间件 | app.use | app.use | 
| 路由级别的中间件 | router.get | router.get | 
| 路由中间件挂载 | app.use(router.routes()) | app.use(‘/‘, router) | 
| 监听端口 | app.listen(3000) | app.listen(3000) | 
koa2 中间件洋葱模型

koa2 中间件处理逻辑 koa-compose
'use strict'
/**
 * Expose compositor.
 */
module.exports = compose
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */
function compose (middleware) {
  // 判断中间件数组
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  // 遍历 检测 中间件数组中的函数类型
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */
  return function (context, next) {
    // last called middleware #
    // 传入当前内容和下一个中间件函数 闭包记录传入的中间件数组
    // 初始化 index
    let index = -1
    return dispatch(0)
    // 定义 dispatch 函数
    function dispatch (i) {
      // 如果 当前位置小于等于 index 则表示 next() 重复调用 
      // next 会传入索引值 如果索引值没有改变则表示重复调用
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 记录当前中间件的索引值
      index = i
      // 赋值当前中间件函数
      let fn = middleware[i]
      // 如果当前位置超出了中间件的最后一个索引 则表示进入到了最里面的中间件 赋值为最里面中间件的next()
      if (i === middleware.length) fn = next
      // 如果下一个函数不存在 则返回完成状态
      if (!fn) return Promise.resolve()
      try {
        // 执行当前函数 将下一个中间件函数赋值给当前中间件next参数
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
jest 简单的测试用例
利用事件循环机制,将后续的内容加到微任务队列。
it('should work', async () => {
    const arr = []
    const stack = []
    // 中间件 1
    stack.push(async (context, next) => {
        arr.push(1)
        await wait(1)
        await next() // 进入执行中间件2
        await wait(1)
        arr.push(6)
    })
    // 中间件 2
    stack.push(async (context, next) => {
        arr.push(2)
        await wait(1)
        await next() // 进入执行中间件3
        await wait(1)
        arr.push(5)
    })
    // 中间件 3
    stack.push(async (context, next) => {
        arr.push(3)
        await wait(1)
        await next() // 已经到洋葱中心 继续向下执行 回到上层中间件
        await wait(1)
        arr.push(4)
    })
    await compose(stack)({})
    expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
1-1 koa2导学
遇到的问题
Koa框架到底为我们做了什么
他的内部到底是什么样子
他向下一直到Nodejs底层,到底是怎么处理事件循环的
一个异步的HTTP过程,到底是怎么进行的
技术栈:MongoDB、Puppeteer、AntDesign、Bootstrap、Parcel
实战
上传图片 base64
// 教程:https://www.huaweicloud.com/articles/2687012349fabd74e9cc9da11c0ca640.html
const path = require('path');
const { writeFileSync } = require('fs');
const { Buffer } = require('buffer');
const Koa = require('koa');
// https://github.com/koajs/router/blob/master/API.md
const Router = require('@koa/router');
// https://github.com/koajs/koa-body
const koaBody = require('koa-body');
// https://github.com/koajs/static
const koaStatic = require('koa-static');
// https://github.com/koajs/cors
// const cors = require('@koa/cors');
const Crypto = require('crypto');
const SizeOf = require('image-size');
const images = require('images');
const app = new Koa();
const router = new Router();
// app.use(cors({
//   'Access-Control-Allow-Methods': 'POST',
//   'Access-Control-Allow-Origin': '*'
// }))
function _getHash(salt = Date.now().toString()) {
  return Crypto.createHash('md5').update(salt).digest('hex');
}
app.use(koaStatic(path.join(__dirname, 'public')));
router.post('/img', 
  koaBody({
    multipart: true, 
    // https://github.com/node-formidable/formidable
    formidable: {
      uploadDir: path.resolve(__dirname, './public/uploads'),
      keepExtensions: true,
      hash: 'sha1',
    }
  }),
  (ctx, next) => {
    const file = ctx.request.files.file;
    const basename = path.basename(file.path);
    ctx.body = { 
      url: `${ctx.origin}/uploads/${basename}`,
      hash: file.hash
    }
  }
);
router.post('/pixel', 
  koaBody(),
  (ctx, next) => {
    const data = ctx.request.body
    // => POST body
    const pixel = data.pixel
    if (pixel && Array.isArray(pixel)) {
      const sizeOld = pixel.length / 4
      console.log(sizeOld)
      const base64 = pixel.reduce((base, item) => {
        const reg = /rgba\(([\d]+), ([\d]+), ([\d]+), 255\)/g;
        const match = reg.exec(item).slice(1);
        match.forEach(item => {
          if (item !== 0) {
            base += String.fromCharCode(item)
          }
        })
        return base
      }, '');
      // 转图片
      const buf = Buffer.from(base64, 'base64');
      const unit = new Uint8Array(buf); // 返回一个被 string,编码格式是base64(默认编码格式是utf-8)的值初始化的新的 Buffer 实例
      const salt = Date.now().toString();
      const hash = _getHash(salt);
      const imgPath = `${hash}.jpg`;
      // 计算图片大小
      const { width, height } = SizeOf(buf);
      console.log(width, height)
      const sizeNew = width * height;
      const multiple = Math.abs((sizeOld / sizeNew));
      const newWidth = multiple * width;
      const newHeight = multiple * height;
      const newHash = _getHash(salt + 'new');
      console.log(multiple, newHeight, newWidth)
      try {
        // 用fs写入文件
        const uploadDir = path.resolve(__dirname, `./public/uploads`);
        writeFileSync(`${uploadDir}/${imgPath}`, unit);
        images(buf).size(newWidth, newHeight).save(`${uploadDir}/${newHash}.jpg`);
        ctx.body = {
          url: `${ctx.origin}/uploads/${imgPath}`,
          hash
        };
      } catch(err) {
        console.log(err);
      }
    }
  }
);
app
  .use(router.routes())
  .use(router.allowedMethods());
app.listen(3000, () => {
  console.log('启动成功: http://localhost:3000');
})