koa簡易框架(一)

源碼

koa使用分析

const Koa = require('koa');
let app = new Koa();//Koa是一個類,通過new生成一個實例

//koa的原型上有use方法,來註冊中間件
app.use((ctx,next)=>{
    //koa擁有ctx屬性,上面掛載了很多屬性
    console.log(ctx.req.path);
    console.log(ctx.request.req.path);
    console.log(ctx.request.path);
    console.log(ctx.path);
    next();//洋蔥模型,中間件組合
})

app.listen(3000);//Koa的原型上擁有監聽listen

洋蔥模型和中間件組合

洋蔥模型

在這裏插入圖片描述
在這裏插入圖片描述

const Koa = require('koa');
const app = new Koa();
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)
})
app.use(async (ctx, next) => {
    console.log(5)
    awit next();
    console.log(6)
})
//打印結果:1 3 5 6 4 2 

中間件組合

koa洋蔥模型的實現,其實就是通過use將函數存放在一個middlewares隊列中,然後通過函數dispatch派發中間件。

dispatch組合中間件:

let app = {
    middlewares:[];     //緩存隊列
    use(fn){    //註冊中間件
        this.middlewares.push(fn);
    }
} 
app.use(next=>{
    console.log(1)
    next();
    console.log(2)
});
app.use(next => {
    console.log(3)
    next();
    console.log(4)
})
app.use(next => {
    console.log(5)
    next();
    console.log(6)
})
dispatch(0)
function dispatch(index){   //派發執行中間件
    if(index===app.middlewares.length) retrun ;
    let middleware = app.middlewares[index];
    middleware(()=>{
        dispatch(index+1);
    })
}

koa組成

在這裏插入圖片描述
koa主要是由四部分組成:

  • application:koa的主要邏輯,包含了中間件處理過程
  • context:koa關於ctx的封裝
  • request:koa請求對象的封裝
  • response:koa響應對象封裝

koa實現

  1. Koa是一個類,擁有middleware、ctx、request、response
  2. Koa.prototype擁有use註冊中間件
  3. Koa.prototype擁有listen監聽網絡請求,其內部是對http模塊的封裝
  4. Koa中handleRquest處理上下文ctx和中間件middleware

application.js

在koa目錄下新建一個lib文件夾,新建application.js文件,代碼如下:

let http = require('http');
let EventEmitter = require('events');
let context = require('./context');
let request = require('./request');
let response = require('./response');
let Stream = require('stream');
const fs = require('fs')

class koa extends EventEmitter {
    constructor(){
        super();
        this.middlewares = [];
        this.context = context;
        this.request = request;
        this.response = response;
        this.ctx = null;
    }
    
    use(fn){
        this.middlewares.push(fn);
    }
    
    createContext(req, res){
        const ctx = Object.create(this.context);
        // ctx.request爲request.js裏面this作了指向,this此時指向ctx
        const request = ctx.request = Object.create(this.request);
        const response = ctx.response = Object.create(this.response);

        // 請仔細閱讀以下眼花繚亂的操作,後面是有用的
        /*
        * console.log(ctx.req.url)
          console.log(ctx.request.req.url)
          console.log(ctx.response.req.url)
          console.log(ctx.request.url)
          console.log(ctx.request.path)
          console.log(ctx.url)
          console.log(ctx.path)

          訪問localhost:3000/abc

          /abc
          /abc
          /abc
          /undefined
          /undefined
          /undefined
          /undefined

          姿勢多,不一定爽,要想爽,我們希望能實現以下兩點:

          從自定義的request上取值、拓展除了原生屬性外的更多屬性,例如query path等。
          能夠直接通過ctx.url的方式取值,上面都不夠方便。
        * */
        ctx.req = request.req = response.req = req;
        ctx.res = request.res = response.res = res
        request.ctx = response.ctx = ctx;
        request.response = response;
        response.request = request;
        return ctx;
    }
    
    compose(middlewares, ctx){
        function dispatch(index) {
            if(index === middlewares.length){
                return Promise.resolve()
            }
            let middleware = middlewares[index];
            return Promise.resolve(middleware(ctx, () => {
                    dispatch(index+1)
            }))
        }
       return Promise.resolve(dispatch(0))
    }

    handleRequest(req, res){
        res.statusCode = 404;
        let ctx = this.ctx = this.createContext(req, res);
        let fn = this.compose(this.middlewares, ctx);
        ctx.setHeaders = (key,val)=>{
            res.setHeader(key, val)
        }
        setTimeout(() => {
            fn.then(() => {
                let body = ctx.body;
                if (Buffer.isBuffer(body) || typeof body === 'string'){
                    if(body.indexOf('<!DOCTYPE html>') != -1){
                        res.setHeader('Content-Type','text/html;charset=utf8')
                    }
                    else{
                        res.setHeader('Content-Type','text/plain;charset=utf8')
                    }
                    res.end(body);
                }
                else if(typeof body == 'number'){
                    res.setHeader('Content-Type','text/plain;charset=utf8')
                    res.end(body.toString());
                }
                else if (body instanceof Stream){
                    res.setHeader('Content-Type',''+ctx.contentType+';charset=utf8')
                    body.pipe(res);
                }
                else if(typeof body == 'object'){
                    res.setHeader('Content-Type','application/json;charset=utf8')
                    res.end(JSON.stringify(body));
                }
                else{
                    res.setHeader('Content-Type','text/plain;charset=utf8')
                    res.end('Not Found');
                }

            }).catch(err => {
                this.emit('error', err);
                res.statusCode = 500
                res.end('server error')
            })
        },20)
    }
    
    listen(...args){
        let server = http.createServer(this.handleRequest.bind(this));
        server.listen(...args);
    }
}
module.exports = koa;

源碼解讀:

  1. 這裏我們有context,response,request,context使用了代理模式,代理之後,通過ctx.body,ctx.method等方式,可以直接設置返回數據或者方式。
  2. 這裏的listen方法,主要是利用了http起用一個服務,handleRequest方法通過bind方式,可以直接將http綁定上去,使其handleRequest的this指向http。
  3. handleRequest這裏主要是處理服務啓動後的回調函數,參數包括req,res,ctx.setHeaders是我們這裏封裝的一個方式,通過這個方法,在其他頁面可以設置返回頭部,接下來我們將app.use裏面函數一個個執行
let fn = this.compose(this.middlewares, ctx);
  1. this.middlewares是我們通過app.use方法,將use裏面函數放到一個數組裏面,稍後會進行講解,ctx是對應的是context,我們把req,res當做參數傳入,就可以在context.js文件裏面具體設置req,res,在fn.then裏面我們需要判斷,因爲在返回的數據裏面,分爲number,string,html以及文件類型,針對於每一種類型,我們需要設置不同頭部。
  2. ctx.contentType,我們這裏有一個這個主要是因爲,文檔類型不同,我們需要設置不同的Content-Type。
  3. compose方法,主要是針對app.use方式,在app.use裏面我們寫了不同函數,我們需要執行這些函數,除此之外,如果有next,這個時候調用dispatch(index+1),同時我們需要把它封裝成Promise方式。
  4. createContext方法,在handleRequest調用,req,res對應的是http的回調req,res,在這裏我們使用了Object.create方式,相當於繼承,避免改變直接改變原對象,ctx.request爲request.js裏面this作了指向,this此時指向ctx,其他也是同理
  5. use方法是將app.use調用的函數放到一個數組裏面

request.js

/*
*   非常簡單,使用對象get訪問器返回一個處理過的數據就可以將數據綁定到request上了,
*   這裏的問題是如何拿到數據,由於前面ctx.request這一步,所以this就是ctx,
*   那this.req就是原生的req,再利用一些第三方模塊對req進行處理就可以了,
*   源碼上拓展了非常多,這裏只舉例幾個,看懂原理即可。
    訪問localhost:3000/abc?id=1

    /abc?id=1
    /abc?id=1
    /abc?id=1
    /abc?id=1
    /abc
    undefined
    undefined
*
* */
const url = require('url')
// noinspection JSAnnotator
let request = {
    get url() { // 這樣就可以用ctx.request.url上取值了,不用通過原生的req
        return this.req.url
    },
    get path() {
        return url.parse(this.req.url).pathname
    },
    get query() {
        return url.parse(this.req.url).query
    },
    get method(){
        return this.req.method.toLowerCase()
    },
    get header(){
        return this.req.headers
    },
    get requestBody(){
        return this._body
    },
    set requestBody(val){
        this._body = val
    },
    // 。。。。。。
}
module.exports = request

源碼解讀

  1. 這裏我們主要是封裝了一些屬性,共後續調用,get url(),這樣就可以用ctx.request.url上取值了,不用通過原生的req,其他同理
  2. 至於這裏的get,set方法,這是es6裏面寫法,參考鏈接

response.js

//response.js

let response = {
    get body(){
        return this._body;
    },
    set body(val){
        this.res.statusCode = 200 // 只要設置了body,就應該把狀態碼設置爲200
        this._body = val // set時先保存下來
    },
    get contentType(){
        return this._type;
    },
    set contentType(val){
        this._type = val // set時先保存下來
    },
    get render(){
        return this._fn;
    },
    set render(val){
        this._fn = val // set時先保存下來
    },
}
module.exports = response;

源碼解讀

同理上面request.js

context.js

//context.js

let proto = {
};
function defineGetter(property,key){
    proto.__defineGetter__(key,function(){
        return this[property][key];
    })
}
function defineSetter(property,key){
    proto.__defineSetter__(key,function(val){
        this[property][key] = val;
    })
}

/*
* __defineGetter__方法可以將一個函數綁定在當前對象的指定屬性上,當那個屬性的值被讀取時,
* 你所綁定的函數就會被調用,第一個參數是屬性,第二個是函數,由於ctx繼承了proto,
* 所以當ctx.url時,觸發了__defineGetter__方法,所以這裏的this就是ctx。這樣,當調用defineGetter方法,就可以將參數一的參數二屬性代理到ctx上了。
* */
defineGetter('request','url');  //ctx代理了ctx.request.url的get
defineGetter('request','path'); //ctx代理了ctx.request.path的get
defineGetter('request','query'); //ctx代理了ctx.request.query的get
defineGetter('request','method'); //ctx代理了ctx.request.method的get
defineGetter('request','header'); //ctx代理了ctx.request.header的get
defineGetter('response','body'); //ctx代理了ctx.response.body的get
defineSetter('response','body'); //ctx代理了ctx.response.body的set
defineGetter('response','contentType'); //ctx代理了ctx.response.contentType的get
defineSetter('response','contentType'); //ctx代理了ctx.response.contentType的set
defineGetter('response','render'); //ctx代理了ctx.response.render的get
defineSetter('response','render'); //ctx代理了ctx.response.render的set
defineGetter('request','requestBody'); //ctx代理了ctx.response.render的get
defineSetter('request','requestBody'); //ctx代理了ctx.response.render的set
module.exports = proto;

源碼解讀

ctx屬性代理了一些ctx.request、ctx.response上的屬性,使得ctx.xx能夠訪問ctx.request.xx或ctx.response.xx

調用方法

const Koa = require('./lib/application');

app.use(async (ctx,next)=>{
    ctx.res.setHeader('Access-Control-Allow-Origin', '*');
    //用於判斷request來自ajax還是傳統請求
    ctx.res.setHeader("Access-Control-Allow-Headers", "Content-Type,XFILENAME,XFILECATEGORY,XFILESIZE,content-type");
    //允許訪問的方式
    ctx.res.setHeader('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS')
    //修改程序信息與版本
    ctx.res.setHeader('X-Powered-By', ' 3.2.1')
    //內容類型:如果是post請求必須指定這個屬性
    ctx.res.setHeader('Content-Type', 'application/json;charset=utf-8')
    if(ctx.req.method == 'OPTIONS'){
        ctx.res.statusCode = 200;/*讓options請求快速返回*/
    }
    next()
})
app.use(async (ctx,next)=>{
    ctx.body = 'hello koa'
    next()
})

app.listen(3000);




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章