学习笔记-模拟实现koa中间件

中间件原理

koa向外暴漏出了两个最重要的方法:

  • listen():用于启动服务
  • use():用于注册中间件,use字面意思就是用到,用户想要koa做的所有事情都是通过use方法传递给koa的,这些方法统称为中间件。

由koa源码不难发现,koa把所有的中间件串起来组成了一个洋葱圈模型。一般情况下,正常的请求会经过同一中间件两次.当然也不能武断的说就是两次,就是洋葱圈模型,当只走完一个中间件就直接响应返回了(例如鉴权失败时),此时就是一次,也就不是洋葱圈了。
image

koa-static

用法

  • 此中间件的作用是响应静态文件的读取请求
  • 两个基本配置:
    • root:静态文件所在的目录
    • opts:一些参数,例如设置缓存、默认名称
  • 一个重要参数:要响应的文件的信息,例如名称,相对root的路径等
  • 具体用法参照koa-static

模拟实现

const fs = require('fs')
const path = require('path')
const util = require('util')
const Event = require('events')
const mime = require("mime");
const stat = util.promisify(fs.stat)

class TinyKoaStaitc extends Event {
  constructor(root, opts) {
    super()
    this.root = root
    this.opts = opts
  }
  serve(filepath) {
    let _this = this
    let opts = Object.assign({}, this.opts)
    opts.root = path.resolve(path.join(this.root, filepath))
    async function server(ctx, next) {
      try {
        let stats = await stat(opts.root)
        opts.headers.map(item => {
          ctx.set(item.key, item.value)
        })
        ctx.set("Content-Type", `${mime.getType(opts.root)};charset=utf8`);
        ctx.set('Content-Length', stats.size)
        ctx.set('last-modified', stats.ctime)
        ctx.stauts = 200
        ctx.body = fs.createReadStream(opts.root)
      } catch (err) {
        ctx.status = 404
        _this.emit('static-server-error', util.inspect(err))
      }

    }
    return server
  }
}

module.exports = TinyKoaStaitc

fs.stat是一个异步方法,用util.promisify包装为一个返回promise的方法,当然stat也有其同步的形式。此处千万要注意事件循环机制,如果promisify的stat没有await而是直接在then回调中写响应的话,返给客户端的永远时404

调用:

const Koa=require('koa')
// const Koa=require('./src/tiny-koa.js')
const debug=require('debug')('app')
const koaStatic=require('./src/middleware/tiny-koa-static.js')
const staticServer=new koaStatic('./src/static',{
  headers:[
    {
      key:'cache-control',
      value:'max-age=10,private'
    }
  ]
})

const app=new Koa()
const port=9001

app.use(staticServer.serve('./template.html'))

app.listen(port,(err)=>{
  // console.log(process.env)
  debug(`start at ${port}`)
})

staticServer.on('static-server-error',(error)=>{
  console.log(error);
})


koa-bodyparser

用法

模拟实现

const qs = require("querystring");
const Event = require('events')

class TinyKoaBodyParser extends Event {
  constructor() {
    super()
  }

  parser() {
    function parser(ctx, next) {
      let body = {}
      let buffers = []
      ctx.req.on('data', chunk => {
        buffers.push(chunk)
      })
      ctx.req.on('end', _ => {
        let bufs = Buffer.concat(buffers).toString()
        let contentType = ctx.get("Content-Type").toLowerCase();
        if (contentType === "application/x-www-form-urlencoded") {
          ctx.request.body = querystring.parse(data);
        } else if (contentType === "applaction/json") {
          // 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
          ctx.request.body = JSON.parse(data);
        }
        next()
      })
    }
  }

}

由于此中间件用于往request.body上挂载请求体的,要让后续的中间件都能从body上取到请求体,next() 必须等待挂载结束后才能执行,挂载过程应该是一个可以await的promise,修改代码如下:

parser() {
  return async function parser(ctx, next) {
    await new Promise((resolve, reject) => {
      let buffers = []
      ctx.req.on('data', chunk => {
        buffers.push(chunk)
      })
      ctx.req.on('end', _ => {
        let bufs = Buffer.concat(buffers).toString()
        let contentType = ctx.get("Content-Type").toLowerCase();
        if (contentType === "application/x-www-form-urlencoded") {
          ctx.request.body = querystring.parse(data);
        } else if (contentType === "applaction/json") {
          // 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
          ctx.request.body = JSON.parse(data);
        }
        resolve()
      })
    })
    await next()
  }
}

koa-session

用法

const CONFIG = {
  key: 'koa:sess', // 设置cookie的id
  maxAge: 86400000, // cookie的有效期
  autoCommit: true, /** (boolean) automatically commit headers (default true) */
  overwrite: true, /** (boolean) can overwrite or not (default true) */
  httpOnly: true, /** (boolean) httpOnly or not (default true) */
  signed: true, // 是否签名
  rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */
  renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/
};

// 注册koa-session中间件
app.use(session(CONFIG, app));


app.use(ctx => {
  // ignore favicon
  if (ctx.path === '/favicon.ico') return;
 
  let n = ctx.session.views || 0; //从session上取值
  ctx.session.views = ++n; //给保存在session上的变量赋值
  ctx.body = n + ' views';
});

原理分析

由于http协议是无状态的,每一次响应请求时服务器是不能知道请求来源的身份,除非请求自报家门,服务器分析相关的信息后识别出请求出自谁。所以在第一次响应请求时,服务器就额外给客户端颁发一把钥匙,服务器自己在某处(db、redis…)存一个只有这个钥匙能打开的宝箱,下次此客户端再发请求时,会自动带上钥匙,如果服务器想要知道是谁再访问或者访问者的相关信息,只需要拿着钥匙去开对应的宝箱即可。因此每个客户端都对应着一把钥匙,也对应着一个宝箱。这个钥匙就是cookie,这个宝箱就是session。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值