1. 程式人生 > >參考KOA,5步手寫一款粗糙的web框架

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

return type ejs wait 報錯 export bbb clas 普通

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

我這裏選擇KOA作為參考範本,只有一個原因!他非常的精簡!核心只有4個js文件!基本上就是對createServer的一個封裝。

在開始解刨KOA之前,createServer的用法還是需要回顧下的:

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個文件了:

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

好了~開始寫框架咯~

僅分析大概思路,分析KOA的原理,所以並不是100%重現KOA。

本文github地址:點我

step1 封裝http.createServer

先寫一個初始版的application

,讓程序先跑起來。這裏我們僅僅實現:

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

step1/application.js

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的用法一摸一樣,就是傳遞了下參數

友情鏈接

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步終於把準備工作做好了,接下來進入正題。??
友情鏈接:

  • Object.defineProperty

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和ejs,等這一塊加入我的web框架之後,就很完美了~

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