我常常在網上看到相似於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
app.use(callback);
來調用,其中callback
大概就是令你們聞風喪膽的中間件(middleWare)了。好了~開始寫框架咯~web
僅分析大概思路,分析KOA的原理,因此並非100%重現KOA。express
本文github地址:點我api
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
友情連接
step1/testhttp.js
let myhttp=require("./application")
let app= new myhttp()
app.listen(3000)
複製代碼
運行testhttp.js
,結果打印出了req
和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
打了基礎。
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步終於把準備工做作好了,接下來進入正題。😭 友情連接:
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)
})
...
複製代碼
任何程序只要加上了異步以後,感受難度就蹭蹭蹭往上漲。
這裏要分兩點來處理:
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手敲指南請收下