參考KOA,5步手寫一款粗糙的web框架

我常常在網上看到相似於KOA VS express的文章,你們都在討論哪個好,哪個更好。做爲小白,我真心看不出他兩who更勝一籌。我只知道,我只會跟着官方文檔的start作一個DEMO,而後我就會宣稱我會用KOA或者express框架了。可是幾個禮拜後,我就全忘了。web框架就至關於一個工具,要使用起來,那是分分鐘的事。畢竟人家寫這個框架就是爲了方便你們上手使用。可是這種生硬的照搬模式,不適合我這種理解能力極差的使用者。所以我決定扒一扒源碼,經過官方API,本身寫一個web框架,其實就至關於「抄」一遍源碼,加上本身的理解,從而加深影響。不只須要知其然,還要須要知其因此然。html

我這裏選擇KOA做爲參考範本,只有一個緣由!他很是的精簡!核心只有4個js文件!基本上就是對createServer的一個封裝。node

在開始解刨KOA以前,createServer的用法仍是須要回顧下的:git

const http = require('http');
let app=http.createServer((req, res) => {
    //此處省略其餘操做
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.body="我是createServer";
    res.end('okay');
});
app.listen(3000)
複製代碼

回顧了createServer,接下來就是解刨KOA的那4個文件了:github

  • application.js
    • 這個js主要就是對createServer的封裝,其中一個最主要的目的就是將他的callback分離出來,讓咱們能夠經過app.use(callback);來調用,其中callback大概就是令你們聞風喪膽的中間件(middleWare)了。
  • request.js
    • 封裝createServer中返回的req,主要用於讀寫屬性。
  • response.js
    • 封裝createServer中返回的res,主要用於讀寫屬性。
  • context.js
    • 這個文件就很重要了,它主要是封裝了request和response,用於框架和中間件的溝通。因此他叫上下文,也是有道理的。

好了~開始寫框架咯~web

僅分析大概思路,分析KOA的原理,因此並非100%重現KOA。express

本文github地址:點我api

step1 封裝http.createServer

先寫一個初始版的application,讓程序先跑起來。這裏咱們僅僅實現:數組

  • 封裝http.createServer到myhttp的類
  • 將回調獨立出來
  • listen方法能夠直接用

step1/application.jspromise

let http=require("http")
class myhttp{
    handleRequest(req,res){
        console.log(req,res)
    }
    listen(...args){
        // 起一個服務
        let server = http.createServer(this.handleRequest.bind(this));
        server.listen(...args)
    }
}
複製代碼

這邊的listen徹底和server.listen的用法一摸同樣,就是傳遞了下參數bash

友情連接

server.listen的API

ES6解構賦值...

step1/testhttp.js

let myhttp=require("./application")
let app= new myhttp()
app.listen(3000)
複製代碼

運行testhttp.js,結果打印出了reqres就成功了~

step2 封裝原生req和res

這裏咱們須要作的封裝,所需只有兩步:

  • 讀取(get)req和res的內容
  • 修改(set)res的內容

step2/request.js

let request={
    get url(){
        return this.req.url
    }
}
module.exports=request
複製代碼

step2/response.js

let response={
    get body(){
        return this.res.body
    },
    set body(value){
        this.res.body=value
    }
}
module.exports=response
複製代碼

若是po上代碼,就是這麼簡單,須要的屬性能夠本身加上去。那麼問題來這個this指向哪裏??代碼是很簡單,可是這個指向,並不簡單。

回到咱們的application.js,讓這個this指向咱們的myhttp的實例。

step2/application.js

class myhttp{
    constructor(){
        this.request=Object.create(request)
        this.response=Object.create(response)
    }
    handleRequest(req,res){
        let request=Object.create(this.request)
        let response=Object.create(this.response)
        request.req=req
        request.request=request
        response.req=req
        response.response=response
        console.log(request.headers.host,request.req.headers.host,req.headers.host)
    }
    ...
}
複製代碼

此處,咱們用Object.create拷貝了一個副本,而後把request和response分別掛上,咱們能夠經過最後的一個測試看到,咱們能夠直接經過request.headers.host訪問咱們須要的信息,而能夠不用經過request.req.headers.host這麼長的一個指令。這爲咱們下一步,將requestresponse掛到context打了基礎。

step3 context閃亮登場

context的功能,我對他沒有其餘要求,就能夠直接context.headers.host,而不用context.request.headers.host,可是我不可能每次新增須要的屬性,都去寫一個get/set吧?因而Object.defineProperty這個神操做來了。

step3/content.js

let context = {
}
//可讀可寫
function access(target,property){
   Object.defineProperty(context,property,{
        get(){
            return this[target][property]
        },
        set(value){
            this[target][property]=value
        }
   })
}
//只可讀
function getter(target,property){
   Object.defineProperty(context,property,{
        get(){
            return this[target][property]
        }
   })
}
getter('request','headers')
access('response','body')
...
複製代碼

這樣咱們就能夠方便地進行定義數據了,不過須要注意地是,Object.defineProperty地對象只能定義一次,不能屢次定義,會報錯滴。

step3/application.js 接下來就是鏈接contextrequestresponse了,新建一個createContext,將responserequest顛來倒去地掛到context就可了。

class myhttp{
    constructor(){
        this.context=Object.create(context)
        ...
    }
    createContext(req,res){
        let ctx=Object.create(this.context)
        let request=Object.create(this.request)
        let response=Object.create(this.response)
        ctx.request=request
        ctx.response=response
        ctx.request.req=ctx.req=req
        ctx.response.res=ctx.res=res
        return ctx
    }
    handleRequest(req,res){
        let ctx=this.createContext(req,res)
        console.log(ctx.headers)
        ctx.body="text"
        console.log(ctx.body,res.body)
        res.end(ctx.body);
    }
    ...
}
複製代碼

以上3步終於把準備工做作好了,接下來進入正題。😭 友情連接:

step4 實現use

這裏我須要完成兩個功能點:

  • use能夠屢次調用,中間件middleWare按順序執行。
  • use中傳入ctx上下文,供中間件middleWare調用

想要多箇中間件執行,那麼就建一個數組,將全部地方法都保存在裏頭,而後等到執行的地時候forEach一下,逐個執行。傳入的ctx就在執行的時候傳入便可。

step4/application.js

class myhttp{
    constructor(){
        this.middleWares=[]
        ...
    }
    use(callback){
        this.middleWares.push(callback)
        return this;
    }
    ...
    handleRequest(req,res){
        ...
        this.middleWares.forEach(m=>{
            m(ctx)
        })
        ...
    }
    ...
}
複製代碼

此處在use中加了一個小功能,就是讓use能夠實現鏈式調用,直接返回this便可,由於this就指代了myhttp的實例app

step4/testhttp.js

...
app.use(ctx=>{
    console.log(1)
}).use(ctx=>{
    console.log(2)
})
app.use(ctx=>{
    console.log(3)
})
...
複製代碼

step5 實現中間件的異步執行

任何程序只要加上了異步以後,感受難度就蹭蹭蹭往上漲。

這裏要分兩點來處理:

  • use中中間件的異步執行
  • 中間件的異步完成後compose的異步執行。

首先是use中的異步 若是我須要中間件是異步的,那麼咱們能夠利用async/await這麼寫,返回一個promise

app.use(async (ctx,next)=>{
    await next()//等待下方完成後再繼續執行
    ctx.body="aaa"
})
複製代碼

若是是promise,那麼我就不能按照普通的程序foreach執行了,咱們須要一個完成以後在執行另外一個,那麼這邊咱們就須要將這些函數組合放入另外一個方法compose中進行處理,而後返回一個promise,最後來一個then,告訴程序我執行完了。

handleRequest(req,res){
    ....
    this.compose(ctx,this.middleWares).then(()=>{
        res.end(ctx.body)
    }).catch(err=>{
        console.log(err)
    })
    
}
複製代碼

那麼compose怎麼寫呢?

首先這個middlewares須要一個執行完以後再進行下一個的執行,也就是回調。其次compose須要返回一個promise,爲了告訴最後我執行完畢了。

初版本compose,簡易的回調,像這樣。不過這個和foreach並沒有差異。這裏的fn就是咱們的中間件,()=>dispatch(index+1)就是next

compose(ctx,middlewares){
    function dispatch(index){
        console.log(index)
        if(index===middlewares.length) return;
        let fn=middlewares[index]
        fn(ctx,()=>dispatch(index+1));
    }
    dispatch(0)
}
複製代碼

第二版本compose,咱們加上async/await,並返回promise,像這樣。不過這個和foreach並沒有差異。dispatch必定要返回一個promise。

compose(ctx,middlewares){
    async function dispatch(index){
        console.log(index)
        if(index===middlewares.length) return;
        let fn=middlewares[index]
        return await fn(ctx,()=>dispatch(index+1));
    }
    return dispatch(0)
}
複製代碼

return await fn(ctx,()=>dispatch(index+1));注意此處,這就是爲何咱們須要在next前面加上await才能生效?做爲promise的fn已經執行完畢了,若是不等待後方的promise,那麼就直接then了,後方的next就自生自滅了。因此若是是異步的,咱們就須要在中間件上加上async/await以保證next執行完以後再返回上一個promise。沒法理解?😷了?咱們看幾個例子。

具體操做以下:

function makeAPromise(ctx){
    return new Promise((rs,rj)=>{
        setTimeout(()=>{
            ctx.body="bbb"
            rs()
        },1000)
    })
}
//若是下方有須要執行的異步操做
app.use(async (ctx,next)=>{
    await next()//等待下方完成後再繼續執行
    ctx.body="aaa"
})
app.use(async (ctx,next)=>{
    await makeAPromise(ctx).then(()=>{next()})
})
複製代碼

上述代碼先執行ctx.body="bbb"再執行ctx.body="aaa",所以打印出來是aaa。若是咱們反一反:

app.use(async (ctx,next)=>{
    ctx.body="aaa"
    await next()//等待下方代碼完成
})
複製代碼

那麼上述代碼就先執行ctx.body="aaa"再執行ctx.body="bb",所以打印出來是bbb。 這個時候咱們會想,既然我這個中間件不是異步的,那麼是否是就能夠不用加上async/await了呢?實踐出真理:

app.use((ctx,next)=>{
    ctx.body="aaa"
    next()//不等了
})
複製代碼

那麼程序就不會等後面的異步結束就先結束了。所以若是有異步的需求,尤爲是須要靠異步執行再進行下一步的的操做,就算本中間件沒有異步需求,也要加上async/await。

有關於router的操做,請移步這份Koa的簡易Router手敲指南請收下

相關文章
相關標籤/搜索