node-koa2入门

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

中间件:https://github.com/koajs/koa/wiki#middleware

学习教程:

koa2进阶学习笔记

项目地址:https://github.com/Datura35422/nodejs-demo-jiudao

概念

Koa2 被推出的原因

express 和 koa 框架都是相同的作者 TJ 写的。

express 框架内置了很多中间件,主要面对是企业级开发,底层采用的是 ES5 的方式。

koa2 出现主要是小而精,没有内置中间件,所以中间件自己安装,底层采用的是 ES7 的方式编写的。

两个框架的相关 api 大同小异。

koa2 与 express 区别

express
  1. 中间件调用呈直线型,即串行。使用 callback 的方式进行调用中间件,next() 函数只能在中间件底部使用,中间件一个一个往下执行。
  2. 基础Connect中间件,自身封装了路由、视图处理等功能。
  3. 弊端是callback回调方式,不可组合、异常不可捕获。
  4. express 的路由是自身集成的;
  5. express 采用传统的函数形式 function 进行启动;
koa2
  1. 中间件呈U型,即“洋葱模型”。使用 Promise 的方式进行顺序调用中间件。所以中间件的 next() 函数可以使用 async / await 的方式进行调用,中间件一层一层往下执行,到达中心后再一层一层往外执行,因此是洋葱模型。
  2. 利用co作为底层运行框架,利用Generator的特性,实现“无回调”的异步处理。
  3. 利用 async 函数、Koa2丢弃回调函数,增强错误处理。
  4. 低级中间件层中提供高级“语法糖”,提高了互操性、稳健性。
  5. Koa2新增了一个Context对象,用来代替Express的Request和Response,作为请求的上下文对象。 还有Node原生提供的req、res、socket等对象。
  6. Koa的需要引入中间件Koa-router。
  7. 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');
})