- 框架目錄
- 初識
- ctx
- use與中間件
- ctx.body
- 請求體
- static
- 關于錯誤捕獲
- 獲取demo代碼
[TOC]
## 框架目錄
```
koa/
|
| - context.js
|
| - request.js
|
| - response.js
|
·- application.js
```
## 初識
### 介紹
首先我們通過`Koa`包導入的是一個類(Express中是一個工廠函數),我們可以通過`new`這個類來創建一個app
```
let Koa = require('koa');
let app = new Koa();
```
這個`app`對象上就兩個方法
`listen` 用來啟動一個http服務器
```
app.listen(8080);
```
`use`用來注冊一個中間件
```
app.use((ctx,next)=>{
...
})
// 一般我們將(ctx,next)=>{}包裝成一個異步函數
//async (ctx,next)=>{}
```
可以發現這個use方法接收一個函數作為參數,這個函數又接收兩個參數`ctx`、`next`,
其中ctx是koa自己封裝的一個上下文對象,這個對象你可以看做是原生http中req和res的集合。
而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.req`、`ctx.res`、`ctx.request`、`ctx.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中一樣),并且最后的顯示的結果是有點微妙的。
>**注意:** 這一節內容需要對異步的發展有清楚的認知,
> 對于異步發展還不大清楚的同學可以參考我的這盤文章
> [異步發展簡明指北](https://juejin.im/post/5a6212386fb9a01ca5608de3)
### 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`,我們就可以通過表單上傳文件了,此時請求體的樣子是長這樣的

嗯。。。其實接下來要干的的事情即是對這個請求體進行拆分拼接。。一頓字符串操作,這里就不再展開啦
有興趣的朋友可以到我的倉庫中查看完整代碼示例[點我~](https://github.com/fancierpj0/iKoa/blob/master/better-body.js)
## 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代碼
> 倉庫:[點我](https://github.com/fancierpj0/iKoa)