专栏首页与前端沾边Koa 中间件实现
原创

Koa 中间件实现

前面我们介绍过了,Koa 的核心就是中间件机制,起服务的话都是千篇一律的。中间件从上至下决定了执行顺序,我们可以在路由之前做权限认证等自己的操作,本篇分享下 koa 几个中间件的实现,也就是把 use 的回调函数单独提出去重写,由于我们会传递参数,所以不会直接返回一个函数,而是一个高阶函数

static 中间件

这个函数作用是读取本地的静态文件,提供一下目录,服务启动就可以直接在浏览器访问内部文件了,主要是用了 fs 模块

const path = require('path') // 路径处理

// fs 方法可以直接使用 promise 调用,我们下次分享下 node 内部链式函数的实现
// 对大家有帮助的话,可以点下赞、关注下哈
const fs = require('fs').promises

// 高阶函数返回函数(闭包), 导出的函数可以传参。如果直接导出 async(ctx, next) => {}  没有拓展操作了
module.exports = function static(staticPath) {
  return async (ctx, next) => {
    // 输入的路径可能不存在
    try {
      // 拼接,路径里面可能有 / ,node 会默认根路径,这里使用 join
      let filePath = path.join(staticPath, ctx.path)
      // 这里不要使用 fs.exists 判断文件了, 使用 stat 或者 lstat
      let statObj = awiat fs.stat(filePath)
      if (statObj.isDirection()) {
        // 文件夹默认访问内部的 index.html
        filePath = path.join(filePath, 'index.html')
      }
      可以用流的形式,这里就直接读取了
      ctx.body = await fs.readFile(filePath, 'utf-8')
    } catch(error) {
      return await next()
    }
  }
}
  • 使用
app.use(static(__dirname))

http://localhost:3000/form.html

koa-rotuer 中间件

我们先看下官方的使用

const Router = require('koa-router')
// 导出的是一个类
let router = new Router()

// 有 get  post 等等方法
router.get('/login', async (ctx, next) => {
  await next()
})

// 一个高阶函数
app.use(router.routes())

实现下我们的 demo

// 基础架子
class Router {
  constructor() {}
  get(path) {}
  routes() {
    return async (ctx, next) => {
    
    }
  }
}

我们使用路由一个方法时可以多次调用

router.get('/', fn)
router.get('/', fn2)
router.get('/', fn3)
router.post('/', fn1)

所以我们想到使用栈的形式存储,执行的时候依次遍历,我们想到洋葱模型的执行机制,最后统一调用。那么存到栈中的就是个对象
{
  path: '',// 遍历执行要匹配
  callback: , // 匹配到了要依次执行
  method: , // 同一个路径可能不同的方法
}

这里我们使用类的形式

改写

// 定义一个存储对象的格式

// 单纯的用 对象也可以
class Layer {
  constructor(path, method, callback) {
    this.path = path
    this.method = method
    this.callback = callback
  }
  // 从栈中筛选出当前匹配。 方法调用绑定在自己身上,避免属性暴露外面对比,拓展性不好,暴露一个方法传参即可,有特殊情况直接在 该方法内部添加即可
  match(path, method) {
    return this.path == path && this.method == method.toLowerCase()
  }
}

class Router {
  constructor() {
    // 存储所有的路由,执行的时候过滤
    this.stack = []
  }
  
  compose(layers, ctx, next) {
    const dispatch = i => {
      if (i === layers.length) reutrn next()
      let cb = layers[i].callback
      return Promise.resolve(cb(ctx, () => dispatch(i + 1)))
    }
    return dispatch(0)
  }
  
  // 在入口中 调用
  routes() {
    // 真实的中间件执行
    return async (ctx, next) => {
      let path = ctx.path
      let method = ctx.method
      // 做筛选
      let layers = this.stack.filter(layer => {
        return layer.match(path, method)
      })
      
      // 筛选出要执行的 layer 了,仿照中间件执行模式,依次执行
      this.compose(layers, ctx, next)
    }
  }
}

// 这里可以下载 methods 库,里面包含了 所有方法名,不用单独在类中配置方法,因为内部的结构是一样的。直接循环
['get', 'post', ...].forEach(method => {
  Router.prototype[method] = function(path, callback) {
    let layer = new Layer(path, method, callback)
    this.stack.push(layer)
  }
})

body-parser 中间件

get 请求我们都知道从 url 中获取参数,使用 url.parse 解析后,直接在 query 参数中,使用 ctx.query 就能获取。对于 post 这种参数在 body 中的形式,我们需要用流的形式读取 buffer 做拼接,而且对于上传图片的形式,还要了解 mulpart/form-data 这种形式。

提交格式类型

简单介绍下几个常见类型

  • application/x-www-form-urlencoded

传递的表单数据

  • application/json

传递的是普通的 json 数据

  • mulpart/form-data

上传文件

Content-Type: multipart/form-data; boundary=你的自定义boundary

分隔符中间夹着的就是传输的数据,还记得我们的 form 表单提交会有一个 name 属性

<input name="username" /> , 匹配找到 username 对应的值转为对象形式

有些头部类型是 application/json;charset=utf-8,分号后面还有个描述,为了判断数据类型方便起见,我们用头部匹配判断 startsWith

接收数据

使用我们上面写的 static 中间件,访问本地的简单的一个页面,写个表单吧,为后面准备,我们先写个 post 请求普通对象形势

script 脚本中直接执行下我们的请求

fetch('/json', {
  method: 'post',
  headers: {
    // 默认是 text/plain
    'content-type': 'application/json'
  },
  body: JSON.stringify({a: 456})
}).then(res => res.json()).then(data => {
  console.log(data)
})

服务端接收数据

router.post('/json', (ctx, next) => {
  let arr = []
  // 这里使用原生的数据监听,流的形势
  ctx.req.on('data', (chunk) => {
    // 二进制形式
    arr.push(chunk)
  })

  ctx.req.on('end', () => {
    console.log(Buffer.concat(arr).toString())
  })
  ctx.body = '456'
})

接口打印

但是我们的监听流的传递是异步的,当我们返回 ctx.body 时,还没有拿到,所以这里需要改成 await promise 形势,然后我们针对不同的 content-type, 组不同的数据处理,把得到的 body 中的值,绑定到 ctx.req.body 上,这样后面执行的中间件就都能获取到了。

实现 bodyParser 中间件

备注使用都在代码中做了标记,大家可以从上往下看,应该很好理解

// dir 如果传文件 存储目录
function bodyParser({ dir } = {}) {
  return async (ctx, next) => {
    // 同步处理返回结果,才好赋值,后面的中间件都可以拿到
    ctx.request.body = await new Promise((resolve, reject) => {  
      // 获取 数据流  on  data获取数据
      let arr = []
      
      ctx.req.on('data', function(chunk){
        arr.push(chunk) // chunk buffer数据
      })
      
      // 数据接收完毕, 尽量不要直接使用 原生的req, res 操作
      ctx.req.on('end', () => {
        // 获取用户传递的数据格式 Content-Type
        let type = ctx.get('content-type') // ctx 中已经做了封装 ,或者 res.headers['content-type']
        
        // 拼装 数据
        let body = Buffer.concat(arr)
        
        // 数据类型判断
        if (type === 'application/x-www-form-urlencoded') {
        // 普通表单数据
        // 设置响应头  数据类型  根据实际情况返回的设置
          ctx.set('Content-Type', 'application/json;charset-utf-8')
          
          // 转成对象返回, querystring node自带的内置库
          resolve(querystring.parse(body.toString()))
        } else if (type.startsWith('text/plain')) {
          // 文本类型
          resolve(body.toString())
        } else if (type.startsWith(application/json)) {
          resolve(JSON.parse(body.toString()))
        } else if (type.startsWith('multipaer/form-data')) {
          // 我们上面介绍了 multipaer 类型的请求数据格式,使用 boundary 分割,分割的每一组的头和体是以 \r\n\r\n 做的分割(http 协议规定的,我们可以直接查找替换) (看下面图我们的页面和node接收到的结果)
          
          let boundary = '--' + type.split('=')[1]
          
          // 获取组数。这里因为我们的 body 是 buffer格式,buffer 没有内置的 split 方法,我们需要自己拓展一下类似 数组的 split 方法,看下面
          let lines = body.split(boundary).slice(1, -1) // 掐头去尾,打印后可以看到,标志的开始和结束没有实际意义
          
          // 定义我们要获取的数据
          let formData = {}
          
          lines.forEach(line => {
            let [head, body] = line.split('\r\n\r\n')// 规定的分割方式
            
            head = head.toString()
            // 我们从下面截图可以看到 格式都是 name=xxx
            let key = head.match(/name="(.+?)"/)[1]
            
            // 文件
            if (head.includes('filename')) {
              // 如果收到 文件,我们存到服务器上
              // 获取文件内容
              let content = line.slice(head.length + 4, -2) // +4 因为 \r\n\r\n 分割的,去掉尾部的 \r\n  -2
              
              // 创建上传目录  目录我们可以在执行的时候上传, 默认 upload目录
              dir = dir || path.join(__dirname, 'upload')
              
              // 随机产生文件名
              let filePath = uuid.v4() //uuid 第三方库 生成随机
              let uploadUrl = path.join(dir, filePath)
              
              fs.writeFileSync(uploadUrl, content)
              formData[key] = {
                filename: uploadUrl,
                size: content.length,
                ..... 还可以自己添加需要的属性
              }
            } else {
              let value = body.toString()
              // 去掉后面的 \r\n
              formData[key] = value.slice(0, -2)
            }
          })
          resolve(formData)
        } else {
          // 默认空对象
          resolve({})
        }
      })
    })
    await next() // 继续执行后面的中间件, 请求体的值已经存储了
  }
}


// 拓展 buffer 的 split 方法
Buffer.prototype.split = function(sep) { // 分隔符
  let arr = []
  let offset = 0
  // 分隔符可能是中文,或者特殊符号,所以我们统一转成 buffer 获取长度
  let len = Buffer.from(sep).length
  // 使用 indexof 获取 sep 的位置,放到数组中,返回数组
  while (-1 !== (index = this.indexOf(sep, offset))) { // indexOf第二个参数标识从哪里开始搜索,不会每次从索引 0 往后遍历
    arr.push(this.slice(offset, index))
    offset = index + len
  }
  // 最后一段可能没有分隔符 剩多少放多少  a|b|c  放 c
  arr.push(offset)
  return arr
}

(打印 body.toString())

本篇分享了三个 koa 中比较常见的中间件,其实中间件的形式都是通用的,高阶函数返回,写的都是简单版本,如果大家感兴趣可以自己看源码详细了解。下次计划跟大家分享下 express 的实现机制,比较复杂,会梳理通顺后再写成文章分享给大家。本文有任何疑问可以评论留言。

如果感兴趣的话可以给波关注哈!

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 如何手写一款KOA的中间件来实现断点续传

    这几天在认认真真地学习KOA框架,了解它的原理以及KOA中间件的实现方法。在研究KOA如何处理上传的表单数据的时候,我灵光一闪,这是不是可以用于断点续传?

    小美娜娜
  • Koa - 中间件(理解中间件、实现一个验证token中间件)

    Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。

    WahFung
  • koa中间件与async

    相比express的保守,koa则相对激进,目前Node Stable已经是v7.10.0了,async&await是在v7.6加入豪华午餐的,这么好的东西必须...

    ayqy贾杰
  • 高吞吐koa日志中间件

    Midlog中间件 node服务端开发中少不了日志打点,而在koa框架下的日志打点在多进程环境中日志信息往往无法对应上下文,而且在高并发下直接进行写buffer...

    欲休
  • Koa日志中间件封装开发

    keyWords
  • express, koa, redux三者中间件对比

    Link: http://yoursite.com/2018/09/14/express-koa-redux三者中间件对比/

    落落落洛克
  • Koa与常用中间件的使用

    Node.js 是一个异步的世界,官方 API 支持的都是 callback 形式的异步编程模型,这会带来许多问题,例如callback 的嵌套问题 ,以及异步...

    越陌度阡
  • KOA中间件的基本运作原理

    在中间件系统的实现上,KOA中间件通过async/await来在不同中间件之间交换控制权,工作机制和栈结构非常相似,建议结合《express中间件系统的基本实现...

    大史不说话
  • koa与express的中间件机制揭秘

    TJ大神开发完express和koa后毅然决然的离开了nodejs转向了go,但这两个web开发框架依然是用nodejs做web开发应用最多的。

    挥刀北上
  • Koa 中间件的原理及其应用

    koa 把很多 async 函数组成一个处理链,每个 async 函数都可以做一些自己的事情,然后用 await next() 来调用下一个 async 函数。...

    Leophen
  • 关于koa2,你不知道的事

    koa 是一个基于 node 实现的一个新的 web 框架,它是由 express 框架的原班人马打造。特点是优雅、简洁、表达力强、自由度高。和 express...

    lucifer210
  • Egg 中间件使用详解

    1. 在 middleware 文件夹中定义中间件文件,如 auth.js,并实现自定义的功能。

    越陌度阡
  • Koa入门(二)搭建 Koa 程序

    安装 mkdir koa-demo && cd koa-demo && npm init -y && npm i koa --save && code .

    测不准
  • 知新 | koa框架入门到熟练第一章

    是由Express原班人马打造,致力于成为一个更小的,更加富有表现力的,web框架。

    mySoul
  • express, koa, redux三者中间件简单对比分析

    Link: http://yoursite.com/2018/09/14/express-koa-redux三者中间件对比/

    coder_koala
  • ejs koa

    npm https://www.npmjs.com/package/koa-static

    mySoul
  • 【JS】304- KOA2框架原理解析和实现

    koa是一个基于node实现的一个新的web框架,它是由express框架的原班人马打造的。它的特点是优雅、简洁、表达力强、自由度高。它更express相比,它...

    pingan8787
  • .NetCore中间件实现原理

    中间件的默认实现类在 Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder 中

    蓝夏
  • 手写koa-static源码,深入理解静态服务器原理

    本文会接着讲一个常用的中间件----koa-static,这个中间件是用来搭建静态服务器的。

    蒋鹏飞

扫码关注云+社区

领取腾讯云代金券