koa中间件

1,144 阅读6分钟

koa-static

koa-static文件在100行之内,非常简洁。koa-static就是对koa-send的浅封装,koa-static只支持get head;默认添加了index.html

// 来源于koa-static源码
  if (opts.index !== false) opts.index = opts.index || 'index.html'// 默认添加了index.html
  if (ctx.method === 'HEAD' || ctx.method === 'GET') { // 支持的方法
        try {
          done = await send(ctx, ctx.path, opts)
        } catch (err) {
          if (err.status !== 404) {
            throw err
          }
        }
      }

用法:

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')
const app = new Koa()
const staticPath = './static'
app.use(static(
 path.join( __dirname,  staticPath)
))
app.use( async ( ctx ) => {
 ctx.body = 'hello world'
})
app.listen(3000, () => {
 console.log('static-use-middleware is starting at port 3000')
})

koa-send:

最重要的一行:😂

ctx.body = fs.createReadStream(path)

path是静态资源的路径;通过fs的流处理方式读取,然后把静态资源返回给浏览器,

其余的是一些完善处理:

隐藏文件。。。;path解码 ;File type;Last-Modified和Cache-Control处理;max-age=处理;path路径是不是文件夹等处理;setHeaders;gzip等等

koa-send的部分源码:

// stream
 ctx.set('Content-Length', stats.size)
 if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString())
 if (!ctx.response.get('Cache-Control')) {
   const directives = ['max-age=' + (maxage / 1000 | 0)]
   if (immutable) {
     directives.push('immutable')
     // Cache-Control immutable表示静态资源不可变,可以不发送请求直接使用缓存,
     // 有这个请求是为了更好的用户体验,同时减小服务端压力;(可见基础多嘛重要)
     
   }
   ctx.set('Cache-Control', directives.join(','))
 }
 if (!ctx.type) ctx.type = type(path, encodingExt)
 ctx.body = fs.createReadStream(path)
 return path

非常简单,koa-send也就100来行,主要是做一些其他边界条件处理。

koa-views

koa-views 是模版渲染的中间件,是和koa2搭配使用的;koa-views 底层使用的是 consolidate这个npm包,支持很多种模版引擎,在使用koa-views的时候,还要装上相应模版引擎的npm包,koa-views里边也用了koa-send这个包;当

app.use(views(__dirname))

没map的时候直接返回静态文件,是不会渲染数据的。所以map选项是必不可少的。

   if (isHtml(suffix) && !map) {
     return send(ctx, paths.rel, {
       root: path
     })
   } 
   /*suffix一般都是html,因为
   extension = 'html'
   getPaths(path, relPath, extension).then(paths => {
   const suffix = paths.ext
   ......
   }
   除非对extension赋值;
   */

consolidate这个npm包对应的渲染方式;

const consolidate = require('consolidate')
engineSource = consolidate
const engineName = map && map[suffix] ? map[suffix] : suffix
const render = engineSource[engineName]
return render(resolve(path, paths.rel), state).then(html => {
   // since pug has deprecated `pretty` option
   // we will use the `pretty` package in the meanwhile
   if (locals.pretty) {
     debug('using `pretty` package to beautify HTML')
     html = pretty(html)
   }

   if (autoRender) {
     ctx.body = html
   } else {
     return Promise.resolve(html)
   }
 })

用法:

//template.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <%arr.forEach(a=>{%>
    <li><%=a%></li>
  <%})%>
</body>
</html>
const Koa = require('koa');
const path = require('path');
const Router = require('koa-router')
const views = require('koa-views');
const app = new Koa();
const router = new Router();
app.use(router.routes())
app.use(views(path.resolve(__dirname), {
  map: { html: 'ejs' }
}));
router.get('/',async (ctx,next)=> {
 await ctx.render('template.html',{arr:[1,2,3]})
})
app.listen(3000);

ejs中render函数简化版本

123

koa-bodyparser

首先要说一下post提交数据的方式,这样才能更进一步了解解析body的包。

  • application/x-www-form-urlencoded;浏览器的原生form表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据,这种数据方式最为常见;
  • multipart/form-data;我们使用表单上传文件时,必须让form表单的enctype 等于multipart/form-data,这种方式一般用来上传文件;
  • application/json;这种方式的数据格式就是最为常见的json格式;

koa-bodyparser基于co-body。支持 json,默认的form,text,xml类型的post请求。不支持multipart format data,其实就是没有处理文件上传的功能;残废包😂;koa-bodyparser基于co-body,co-body基于raw-body,raw-body用的是buffer的流处理方式,先存起来,再吐出来,用的on('data',onData),on('end',onEnd)做监听处理;

另外:对Koa1也支持;

koa-body

和koa-bodyparser相比支持文件上传; json处理方式:

bodyPromise = require('co-body').json(ctx, {
   encoding: opts.encoding,
   limit: opts.jsonLimit,
   strict: opts.jsonStrict,
   returnRawBody: opts.includeUnparsed
 });

普通form处理方式

bodyPromise = require('co-body').form(ctx, {
   encoding: opts.encoding,
   limit: opts.formLimit,
   queryString: opts.queryString,
   returnRawBody: opts.includeUnparsed
 });

上传文件/图片处理方式

const forms = require('formidable');
function formy(ctx, opts) {
 return new Promise(function (resolve, reject) {
   var fields = {};
   var files = {};
   var form = new forms.IncomingForm(opts);
   form.on('end', function () {
     return resolve({
       fields: fields,
       files: files
     });
   }).on('error', function (err) {
     return reject(err);
   }).on('field', function (field, value) {
     if (fields[field]) {
       if (Array.isArray(fields[field])) {
         fields[field].push(value);
       } else {
         fields[field] = [fields[field], value];
       }
     } else {
       fields[field] = value;
     }
   }).on('file', function (field, file) {
     if (files[field]) {
       if (Array.isArray(files[field])) {
         files[field].push(file);
       } else {
         files[field] = [files[field], file];
       }
     } else {
       files[field] = file;
     }
   });
   if (opts.onFileBegin) {
     form.on('fileBegin', opts.onFileBegin);
   }
   form.parse(ctx.req);
 });
}
 bodyPromise = formy(ctx, opts.formidable);

前两种方式和koa-bodyparser相似都是引用的co-body,第三种处理方式是引用了formidable,formidable封装方法还是用的es5,很多方法挂载到原型链上😂;

koa-better-body

相比于前两个,这个比较全面,json,buffer,text,文件上传都支持,用的generator的方式,还支持koa1;

koa-router

koa-router源码中方法还是挂载在原型上,没有采用es6的class写法,失望😢; 包括的方法有7种:'HEAD', 'OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE' 通过for循环的方式将7种方法挂载到原型上,然后拿到register方法去注册;

for (var i = 0; i < methods.length; i++) {
  function setMethodVerb(method) {
    Router.prototype[method] = function(name, path, middleware) {
      var middleware;
      if (typeof path === "string" || path instanceof RegExp) {
        middleware = Array.prototype.slice.call(arguments, 2);
      } else {
        middleware = Array.prototype.slice.call(arguments, 1);
        path = name;
        name = null;
      }
      this.register(path, [method], middleware, {
        name: name
      });
      return this;
    };
  }
  setMethodVerb(methods[i]);
}

register方法里边的:

var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true,
    name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false,
    strict: opts.strict || this.opts.strict || false,
    prefix: opts.prefix || this.opts.prefix || "",
    ignoreCaptures: opts.ignoreCaptures // 忽视大小写
  });
  stack.push(route);
  return route;

主要核心在new Layer里边;

设置前缀的逻辑

if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix);
// 其实就是下边的逻辑
Layer.prototype.setPrefix = function (prefix) {
  if (this.path) {
    if (this.path !== '/' || this.opts.strict === true) {
      this.path = prefix + this.path;
    } else {
      this.path = prefix;
    }
    this.paramNames = [];
    this.regexp = pathToRegexp(this.path, this.paramNames, this.opts);
  }
  return this;
};

del是delete的一个映射

// Alias for `router.delete()` because delete is a reserved word
Router.prototype.del = Router.prototype['delete'];

提到allowedMethods的作用,首先先说一下http options的作用

  • 检测服务器所支持的请求方法。 可以使用 OPTIONS 方法对服务器发起请求,以检测服务器支持哪些 HTTP 方法
  • CORS 中的预检请求。 在 CORS 中,可以使用 OPTIONS 方法发起一个预检请求,以检测实际请求是否可以被服务器所接受

allowedMethods的作用

  • 没有加allowedMethods的情况下,给接口发送options请求,结果返回404,加上allowedMethods,就返回200,并且通过Allow字段告诉他所支持的请求方法
  • 相应的返回405(不允许)和501(没实现);比如url只是实现了get方法,但是没实现post方法,用post方法访问该接口就返回405;但是用LINK方法就会返回501,因为koa这个框架不支持LINK这个 生僻方法;
// 对应源代码
return function allowedMethods(ctx, next) {
    return next().then(function() {
      var allowed = {};
      if (!ctx.status || ctx.status === 404) {
        for (var i = 0; i < ctx.matched.length; i++) {
          var route = ctx.matched[i];
          for (var j = 0; j < route.methods.length; j++) {
            var method = route.methods[j];
              allowed[method] = method
          }
        }

        var allowedArr = Object.keys(allowed);
        if (!~implemented.indexOf(ctx.method)) {
          if (options.throw) {
            var notImplementedThrowable;
            if (typeof options.notImplemented === 'function') {
              notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function
            } else {
              notImplementedThrowable = new HttpError.NotImplemented();
            }
            throw notImplementedThrowable;
          } else {
            ctx.status = 501;
            ctx.set('Allow', allowedArr.join(', '));
          }
        } else if (allowedArr.length) {
          if (ctx.method === 'OPTIONS') {
            ctx.status = 200;
            ctx.body = '';
            ctx.set('Allow', allowedArr.join(', '));
          } else if (!allowed[ctx.method]) {
            if (options.throw) {
              var notAllowedThrowable;
              if (typeof options.methodNotAllowed === 'function') {
                notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function
              } else {
                notAllowedThrowable = new HttpError.MethodNotAllowed();
              }
              throw notAllowedThrowable;
            } else {
              ctx.status = 405;
              ctx.set('Allow', allowedArr.join(', '));
            }
          }
        }
      }
    });
  };

koa-router里边还引用了一个koa-compose, compose 是一个工具函数,通过这个工具函数组合后,按 app.use() 的顺序同步执行,也就是形成了 洋葱圈 式的调用。 这个函数的源代码不长,不到50行, 利用递归实现了 Promise 的链式执行,不管中间件中是同步还是异步都通过 Promise 转成异步链式执行。

koa-parameter

koa-session

koa-jwt

koa-json-error

koa-mysql