參考KOA,5步手寫一款粗糙的web框架
我經常在網上看到類似於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)了。
- 這個js主要就是對createServer的封裝,其中一個最主要的目的就是將他的callback分離出來,讓我們可以通過
- 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
,結果打印出了req
和res
就成功了~
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
這麽長的一個指令。這為我們下一步,將request
和response
掛到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
接下來就是連接context
和request
和response
了,新建一個createContext
,將response
和request
顛來倒去地掛到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框架