Koa:核心探秘与入坑指北

  • 框架目录
  • 初识
  • ctx
  • use与中间件
  • ctx.body
  • 请求体
  • static
  • 关于错误捕获
  • 获取demo代码

pre-notify

给最近的koa2学习作个小结,主要分为使用的注意事项以及源码实现两个部分,感受写得有点啰嗦,之后有空再修正吧~git

koa2和promise、async-await密切相关,但碍于篇幅这里并无对promise部分详细介绍,若是对promise、async-await还不是很清楚的同窗能够参考个人这篇文章github

异步发展简明指北express

(づ ̄ 3 ̄)づnpm

框架目录

koa/
|
| - context.js
| 
| - request.js
|
| - response.js
|
·- application.js
复制代码

初识

介绍

首先咱们经过Koa包导入的是一个类(Express中是一个工厂函数),咱们能够经过new这个类来建立一个appjson

let Koa = require('koa');

let app = new Koa();
复制代码

这个app对象上就两个方法数组

listen 用来启动一个http服务器promise

app.listen(8080);
复制代码

use用来注册一个中间件bash

app.use((ctx,next)=>{
	...
})

// 通常咱们将(ctx,next)=>{}包装成一个异步函数
//async (ctx,next)=>{}
复制代码

能够发现这个use方法接收一个函数做为参数,这个函数又接收两个参数ctxnext服务器

其中ctx是koa本身封装的一个上下文对象,这个对象你能够看作是原生http中req和res的集合。app

而next和Express中的next同样,能够在注册的函数中调用用以执行下一个中间件。

框架搭建

/* application.js */

class Koa extends EventEmitter{
    constructor(){
    	super();
        this.middlewares = [];
        this.context = context;
        this.request = request;
        this.response = response;
    }
    
    //监听&&启动http服务器
    listen(){
    	const server = http.createServer(this.handleRequest());
     	return server.listen(...arguments);
    }
    
    //注册中间件
    use(fn){
    	this.middlewares.push(fn);
    }
    
    //具体的请求处理方法
    handleRequest(){
    	return (req,res)=>{...}
    }
   
   //建立上下文对象
    createContext(req,res){
    	...
    }
    
    //将中间件串联起来的方法
    compose(ctx,middlewares){
    	...
    }
    
}
复制代码

ctx

用法

ctx,即context,大多数人称之为上下文对象。

这个对象下有4个主要的属性,它们分别是

  • ctx.req:原生的req对象
  • ctx.res:原生的res对象
  • ctx.request:koa本身封装的request对象
  • ctx.response:koa本身封装的response对象

其中koa本身封装的和原生的最大的区别在于,koa本身封装的请求和响应对象的内容不只囊括原生的还有一些其独有的东东

...
console.log(ctx.query); //原生中须要通过url.parse(p,true).query才能获得的query对象
console.log(ctx.path); //原生中须要通过url.parse(p).pathname才能获得的路径(url去除query部分)
...
复制代码

除此以外,ctx自己还代理了ctx.request和ctx.response身上的属性,So以上还能简化为

...
console.log(ctx.query);
console.log(ctx.path);
...
复制代码

原理

首先咱们要建立三个模块来表明三个对象

ctx对象/模块

//context.js
let proto = {};
module.exports = proto;
复制代码

请求对象/模块

let request = {};
module.export = request;
复制代码

响应对象/模块

let response = {};
module.exports = response;
复制代码

而后在application.js中引入

let context = require('./context');
let request = require('./request');
let response = require('./response');
复制代码

并在constructor中挂载

this.context = context;
this.request = request;
this.response = response;
复制代码

接下来咱们来理一理流程,ctx.request/response是koa本身封装的,那么何时生成的呢?确定是获得原生的req、res以后才能进行加工吧。

So,咱们在专门处理请求的handleRequest方法中来建立咱们的ctx

handleRequest(){
    return (req,res)=>{
    	let ctx = this.createContext(req,res);
        ...
    }
}
复制代码

createContext

为了使咱们的每次请求都拥有一个全新的ctx对象,咱们在createContext方法中采用Object.create来建立一个继承this.context的对象。

这样即便咱们在每一次请求中改变了ctx,例如ctx.x = xxx,那么也只会在本次的ctx中建立一个私有属性而不会影响到下一次请求中的ctx。(response也是同理)

createContext(req,res){
    let ctx = Object.create(this.context); //ctx.__proto__ = this.context
    ctx.response = Object.create(this.response);
}
复制代码

呃,说回咱们最初的目的,咱们要建立一个ctx对象,这个ctx对象下有4个主要的属性:ctx.reqctx.resctx.requestctx.response

其中ctx.request/response囊括ctx.req/res的全部属性,那么咱们要怎么将本来req和res下的属性赋给koa本身建立的请求和响应对象呢?这么多属性,难道要一个一个for过去吗?显然这样操做过重了。

咱们能不能想个办法当咱们访问ctx.request.xx属性的时候其实就是访问ctx.req.xx属性呢?

get/set

of coures,we can!

//application.js

createContext(req,res){
...
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    return ctx;
}

// --- --- ---

//request.js
let request = {
    get method(){
    	return this.req.method
    }
}
复制代码

经过以上代码,咱们在访问ctx.response.method的时候其实访问的就是ctx.req.method,而ctx.req.method其实就是req.method。

其中的get method(){}这样的语法时es5里的特性,当咱们访问该对象下的method属性时就会执行该方法并以这个方法中的返回值做为咱们访问到的值。

咱们还能经过在get中作一些处理来为ctx.request建立一些原生的req对象没有的属性

let request = {
...
  get query(){
    return url.parse(this.req.url,true).query;
  }
};
复制代码

delateGetter

除了经过ctx.request.query拿到query对象,咱们还能经过ctx.query这样简写的方式直接拿到本来在request下的全部属性。这又是怎么实现的呢?

很简单,咱们只须要用ctx来代理ctx.request便可

// context.js
...
function delateGetter(property,name){
    proto.__defineGetter__(name,function(){
    	return this[property][name];
    });
}

delateGetter('request','query');
...
复制代码

经过proto.__defineGetter__(name,function(){})代理(和上一节所展现的get/set是同样的功能)

当咱们访问proto.name的时候其实就是访问的proto.property.name

也就是说ctx.query的值即为ctx.request.query的值。

注意: 这里get/set,delateGetter/Setter都只演示了一两个属性,想要更多,就得添加更多的get()/set(),delateGetter/Setter(),嗯源码就这么干的。

use与中间件

咱们经过use方法注册中间件,这些中间件会根据注册时的前后顺序,被依次注册到一个数组当中,而且当一个请求来临时,这些中间件会按照注册时的顺序依次执行。

但这些中间件并非自动依次执行的,咱们须要在中间件callback中手动调用next方法执行下一个中间件callback(和express中同样),而且最后的显示的结果是有点微妙的。

next与洋葱模型

咱们来看下面这样一个栗子

app.use(async (ctx,next)=>{
  console.log(1);
  await next();
  console.log(2);
});

app.use(async (ctx,next)=>{
  console.log(3);
  await next();
  console.log(4);
});

<<<
1
3
4
2
复制代码

嗯,第一次接触koa的同窗确定很纳闷,what the fk???这是什么鬼?

嗯,咱们先记住这个现象先不急探究,再接着往下看看中间件其它须要注意的事项。

中间件与异步

咱们在注册中间件时,一般会将回调包装成一个async函数,这样,倘若咱们的回调中存在异步代码,就能不写那冗长的回调而经过await关键字像写同步代码同样写异步回调。

app.use(async (ctx,next)=>{
    let result = await read(...); //promisify的fs.read
    console.log(result);
})
复制代码

包装成promise

须要补充的一点时,要让await有效,就须要将异步函数包装成一个promise,一般咱们直接使用promisify方法来promise化一个异步函数。

next也要使用await

还须要注意的是倘若下一个要执行的中间件回调中也存在异步函数,咱们就须要在调用next时也使用await关键字

app.use(async (ctx,next)=>{
    let result = await read(...); //promisify的fs.read
    console.log(result);
    await next(); //自己async函数也是一个promise对象,故使用await有效
    console.log('1');
})
复制代码

不使用awiat的话,倘若下一个中间件中存在异步就不会等待这个异步执行完就会打印1

原理

接下来咱们来看怎么实现中间件洋葱模型。

若是一个中间件回调中没有异步的话其实很简单

let fns = [fn1,fn2,fn3];
function dispatch(index){
    let middle = fns[index];
    if(fns.length === index)return;
    middle(ctx,()=>dispatch(index+1));
}
复制代码

咱们只须要有一个dispatch方法来遍历存放中间件回调函数的数组。并将这个dispatch方法做为next参数传给本次执行的中间件回调。

这样咱们就能在一个回调中经过调用next来执行下一次遍历(dispatch)。

但一个中间件回调中每每存在异步代码,若是咱们像上面这样写是达不到咱们想要的效果的。

那么,要怎样作呢?咱们须要借助promise的力量,将每一个中间件回调串联起来。

handleRequest(){
    ...
    let composeMiddleWare = this.compose(ctx,this.middlewares)
    ...
}
复制代码
compose(ctx,middlewares){
    function dispatch(index){
    	let middleware = middlewares[index];
        if(middlewares.length === index)return Promise.resolve();
        return Promise.resolve(middleware(ctx,()=>dispatch(index+1)));
    }
    return dispatch(0);
}
复制代码

其中一个middleware便是一个async fn,而每个async fn都是一个promise,

在上面的代码中咱们让这个promise转换为成功态后才会去遍历下一个middleware,而何时promise才会转为成功态呢?

嗯,只有当一个async fn执行完毕后,async fn这个promise才会转为成功态,而每个async fn在内部若存在异步函数的话又可使用await,

SO,咱们就这样将各个middleware串联了起来,即便其内部存在异步代码,也会按照洋葱模型执行。

ctx.body

使用

ctx.body便是koa中对于原生res的封装。

app.use(async (ctx,next)=>{
	ctx.body = 'hello';
});

<<<
hello
复制代码

须要注意的是,ctx.body能够被屡次连续调用,但只有最后被调用的会生效

...
ctx.body = 'hello';
ctx.body = 'world';
...

<<<
world
复制代码

ctx.body支持以流、object做为响应值。

ctx.body = {...}
复制代码
ctx.body = require('fs').createReadStream(...);
复制代码

原理

咱们调用ctx.body实际上调用的是ctx.response.body(参考ctx代理部分),而且咱们只是给这个属性赋值,这仅仅是个属性并不会立马调用res.end等来进行响应

而咱们真正响应的时候是在全部中间件都执行完毕之后

//application.js

handleRequest(){
  let composeMiddleWare = this.compose(ctx,this.middlewares);
    composeMiddleWare.then(function(){
        let body = ctx.body;
        if(body == undefined){
          return res.end('Not Found');
        }
        if(body instanceof Stream){ //若是ctx.body是一个流
          return body.pipe(res);
        }
        if(typeof body === 'object'){ //若是ctx.body是一个对象
          return res.end(JSON.stringify(body));
        }
        res.end(ctx.body); //ctx.body是字符串和buffer
    })
}

复制代码

请求体

上面咱们说过在async fn中咱们能使用await来"同步"异步方法。

其实除了一些异步方法须要await外,请求体的接收也须要await

app.use(async (ctx,next)=>{
    ctx.req.on('data',function(data){ //异步的
      buffers.push(data);
    });
    ctx.req.on('end',function(){
      console.log(Buffer.concat(buffers));
    });
});

app.use(async (ctx,next)=>{
	console.log(1);
})
复制代码

像上面这样的例子1是会被先打印的,这意味着若是咱们想要在一个中间件中获取完请求体并在下一个中间件中使用它,是作不到。

那么要怎样才能达到咱们预期的效果呢?在await一节中咱们讲过,咱们能够将代码封装成一个promise而后再去await就能达到同步的效果。

咱们能够经过npm下载到这样的一个库——koa-bodyparser

let bodyparser = require('koa-bodyparser');
app.use(bodyparser());
复制代码

这样,咱们就能在任何中间件回调中经过ctx.request.body获取到请求体

app.use(async (ctx,next)=>{
	console.log(ctx.request.body);
})
复制代码

但须要注意的是,koa-bodyparser并不支持文件上传,若是要支持文件上传,可使用better-body-parser这个包。

body-parser 实现

function bodyParser(options={}){
  let {uploadDir} = options;
  return async (ctx,next)=>{
    await new Promise((resolve,reject)=>{
      let buffers = [];
      ctx.req.on('data',function(data){
        buffers.push(data);
      });
      ctx.req.on('end',function(){
        let type = ctx.get('content-type');
        // console.log(type);//multipart/form-data; boundary=----WebKitFormBoundary8xKcmy8E9DWgqZT3
        let buff = Buffer.concat(buffers);
        let fields = {};

        if(type.includes('multipart/form-data')){
          //有文件上传的状况
        }else if(type === 'application/x-www-form-urlencoded'){
          // a=b&&c=d
          fields = require('querystring').parse(buff.toString());
        }else if(type === 'application/json'){
          fields = JSON.parse(buff.toString());
        }else{
          // 是个文本
          fields = buff.toString();
        }
        ctx.request.fields = fields;
        resolve();
      });
    });
    await next();
  };
}
复制代码

能够发现 bodyParser自己便是一个async fn,它将on data on end接收请求体部分代码封装成了一个promise,而且await这个promise,这意味着只有当这个promise转换为成功态时,才会走next(遍历下一个中间件)。

而咱们何时将这个promise转换为成功态的呢?是在将请求体解析完毕封装成一个fields对象并挂载到ctx.request.fields以后,咱们才resolve了这个promise。

以上就是bodyParser实现的大致思路,还有一点咱们没有详细解释的部分既是有文件上传的状况。

当咱们将enctype设置为multipart/form-data,咱们就能够经过表单上传文件了,此时请求体的样子是长这样的

嗯。。。其实接下来要干的的事情便是对这个请求体进行拆分拼接。。一顿字符串操做,这里就再也不展开啦

有兴趣的朋友能够到个人仓库中查看完整代码示例点我~

static

Koa中为咱们提供了静态服务器的功能,不过须要额外引一个包

let static = require('koa-static');
let path = require('path');
app.use(static(path.join(__dirname,'public')));
app.listen(8000);
复制代码

只需三行代码,咳咳,静态服务器你值得拥有。

原理

原理也很简单啦,static首先它也是一个async fn

function static(p){

  return async(ctx,next)=>{
    try{
      p = path.join(p,'.'+ctx.path);
      let statObj = await stat(p);
      if(statObj.isDirectory()){
		...
      }else{
        ctx.body = fs.createReadStream(p); //在body上挂载可读流,会在全部中间件执行完毕后以pipe形式输出到客户端
      }
    }catch(e) {
      await next();
    }
  }
}
复制代码

关于错误捕获

最后,koa还容许咱们在一个async fn中抛出一个异常,此时它会返回个客户端一串字符串Internal Server Error,而且它还会触发一个error事件

app.use(async (ctx,next)=>{
  throw Error('something wrong');
});

app.on('error',function(err){
  console.log('e',err);
});
复制代码

原理

// application.js
handleRequest(){
	...
    composeMiddleWare.then(function(){
    	...
    }).catch(e=>{
    	this.emit('error',e);
        res.end('Internal Server Error');
    })
    ...
}
复制代码

获取demo代码

仓库:点我

相关文章
相关标签/搜索