我經常在網上看到類似於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
的用法一摸一樣,就是傳遞了下引數
友情連結
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步終於把準備工作做好了,接下來進入正題。? 友情連結:
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手敲指南請收下