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代碼

倉庫:點我

相關文章
相關標籤/搜索