Express route 原始碼解析

tanka發表於2019-03-06

express route 原始碼解析

使用express,對route部分比較好奇,花了兩天時間看了下原始碼,今天分享出來,希望可以給對express route實現同樣好奇的人帶來一些幫助.

先寫幾行程式碼

app.use((req,res,next)=>{
    console.log('use 中介軟體');
    next();
})
app.get('/' , (req,res,next)=>{
    res.end('main');
})
app.get('/book/*' , ()=>{
    res.end('book')
})
複製程式碼

這裡先配置幾條路由,當有url匹配到路由就會執行相對應的回撥函式。如果你在回撥函式裡面呼叫next(),它還會繼續向下找符合的項,並執行相應的回撥函式。這裡看到express用起來真的很方便,但它內部是如何實現的呢?

最初的猜測:

1.一定是先把這些配置項儲存起來,把每一個路由的regexp和相應的回撥函式當作一個物件儲存起來,最後儲存到一個陣列中。

2.當有請求過來,遍歷陣列,拿出url和每一個路由的regexp匹配,匹配成功了執行相應的回撥函式。

3.next:當第2步完成後,如果呼叫next,執行了相應的回撥函式後不停,繼續向下遍歷陣列,執行相應的邏輯。如果沒有呼叫next, 就直接停了。

第一步,express是怎麼儲存各個路由配置項,又是以什麼結構儲存的

console.log(app),找到了我想要的東西,app._router裡面儲存的是各個配置項的資訊。

Express route 原始碼解析
這裡看到express 構建了一個router物件,並且把相關的路由資訊儲存在stack裡面。每一個layer裡面裝著regexp和相應的回撥函式。(和我猜的差不多,哈哈哈)

之後做的事,就是看原始碼,看如何存的了。既然我是通過app.get()方法配置的,直接找app.get()相應的原始碼就好了。

一步一步來

先是開啟入口檔案,這個很好找,是express.js.(原始碼1)

Express route 原始碼解析

這裡,通過mixin(app, proto, false)看出來,app的方法都是寫在proto裡面,也就是都寫在application.js裡面。直接在application.js裡面搜尋app.get,然而並沒有搜到。。。之後我發現,原來express是這麼做的。(原始碼2)

Express route 原始碼解析
methods是各個方法名字組成的陣列,get,post啥的。。找到了app.get()方法後,就看看它是如何生成上面的router物件的.很容易看到,生成router物件應該就是在this.lazyrouter()這兒了。(原始碼3)

Express route 原始碼解析
接下來很清晰,直接看Router函式就可以了。router的建構函式在router資料夾下的index.js裡面。(原始碼4)

Express route 原始碼解析

果然在這裡看到了, router.stack裡面應該裝的就是各個layer了?.(後面會說到,layer就是裝著每一個路由項,包括regexp,回撥函式等)。不過現在還沒有裝呢,沒執行this.stack.push()操作呢。還得往回看,回到(原始碼2),剛剛只是執行了this.lazyrouter();。之後執行了var route = this._router.route(path);這步開始裝layer了。接下來挺重要的了。this._router.route(path),也就是./router/index.js下(原始碼5)

Express route 原始碼解析
這裡可以看到,又出現了一個Route建構函式。route這個物件函式很重要,後面會詳細說。(這裡先說答案,這個route裡面會有一個stack陣列,裝著layer,裡面的layer會放真正路由的回撥函式)

再貼一次layer物件的資料結構

Express route 原始碼解析
同時看Layer的建構函式,./router/layer.js(原始碼6)

Express route 原始碼解析
這裡看到layer裡面regexp,path啥的屬性都存起來了,但是回撥函式還沒有裝起來.this.handle=fn. 按道理說hanldle裡面應該就裝著回撥函式,但這裡handle裝的是route.dispatch.bind(route).這個函式很重要,之後在有請求過來的時候,會先走到這個函式,通過這個函式再去呼叫我們配置路由的回撥函式,之後會細說.

再看看剛剛那個route物件,建構函式在./router/route.js裡面(原始碼7)

Express route 原始碼解析
可以看到這裡面也有一個stack,這個stack裡面同樣裝的是layer物件,不同的是這次的layer裡面的handle是真實的路由回撥函式了.

在哪裡裝的??? 讓我們回到(原始碼2)

route[method].apply(route, slice.call(arguments, 1));

這行程式碼,真正的執行程式碼在./router/route.js裡面(原始碼8)

Express route 原始碼解析
這裡看到handles 就真的是路由回撥函式,至於為什麼是一個陣列,因為可以為一個路由配置多箇中介軟體,這個很好理解。之後的215行,會在new一個layer,這個layer的handle裡面裝的是真正的路由回撥函式了。上面的過程就是通過app.get()等方法建立出layer的過程,我給他起名路由layer.

先整理一下: app._router,也就是最大的那個物件,裡面會有一個stack陣列,裝著通過app.get('/','callback')等建立出的layer,這種layer(有路由的,就是通過app.get()等方法建立出來的),會有一個handle,這個handle==route.dispatch.bind(route).同時會有一個route,裡面同樣會有一個stack,這裡面的layer的handle撞著真實的路由回撥函式。如下圖

Express route 原始碼解析

接下來再看通過app.use()建立的中介軟體.和上面差不過,直接看程式碼,再./router/index.js(原始碼9)

Express route 原始碼解析
可以看出來,通過app.use()建立出來的layer,handle屬性就是中介軟體函式,而不像上面那種路由的layer,handle裝的是route.dispatch.bind(route),再在router屬性的layer裝真正的路由回撥函式。同時這種layer,layer.router=undefined;

整理第一步app._router的資料結構

app._router.stack = [layer,layer...]; 裡面裝著2種型別的layer。

第一種是通過app.get()等方式建立的layer.這種layer,handle==route.dispatch.bind(route)。layer.route = [layer..].同時route裡面裝著layer(layer.handle==真正的路由回撥函式)。

第二種是通過app.use()建立的layer,這種layer,layer.handle就是真正的回撥函式。同時layer.route=undefined;

第一步的原始碼過程看著會比較枯燥

第二步:如何通過路由找到相應回撥函式,並執行以及next的實現

接下來看程式碼的過程就是從有請求來的時候,那部分程式碼看就可以了

一步一步來吧

還是看express.js裡面的程式碼 , 原始碼(2-1)

Express route 原始碼解析
當有請求來了會走到app.handle(req,res,next);

app.handle實現程式碼在application.js裡面, 原始碼(2-2)

Express route 原始碼解析

然後在通過router.handle(req,res,done);也就是在router資料夾下的index.js 核心程式碼了, 原始碼(2-3)

Express route 原始碼解析
proto.handle程式碼太多,這裡只貼出了核心程式碼。

可以看到stack也就是app._router.stack裡面裝的是各種layer,有通過app.get()等方式建立的帶route的layer,也有通過app.use()建立的layer.route=undefined的layer.

之後在裡面寫了一個next方法,(據猜測就是回撥函式裡面的第三個引數next), 並執行了next();可以看到裡面做的事情就是開始遍歷layer陣列,直到先找到的第一個match到的layer,之後會走到layer.handle_request(req, res, next)。這裡面的next引數就是上面proto.handle裡面定義的next函式

layer.handle_request()的原始碼在./router/layer.js裡面.(原始碼2-4)

Express route 原始碼解析

這裡要小心一點了 ,fn = this.handle. 上面說過,layer有兩種型別,一種是有路由的layer,layer.handle == route.dispatch.bind(route).另一種是app.use()那種,layer.handle就是回撥函式。

先說第二種,如果先匹配到的layer是app.use()那種,此時執行fn(req,res,next),也就直接執行了回撥函式。如果在回撥函式裡面呼叫了next()方法,也就是在走到上面proto.handle裡面的next(),繼續遍歷stack陣列的下一項,周而復始,直到遍歷完。。(一起都看著那麼的順暢)

再來看第一種:執行fn(req,res,next);這時候走到的是route.dispatch.bind(route)這個函式裡。(講真的,這個函式真心重要

上程式碼,route.dispatch的原始碼在./router/route.js (原始碼2-5)

Express route 原始碼解析

這裡可以看到,這個物件也就是帶路由得那種layer,裡面的lauer.route物件,如下圖

Express route 原始碼解析

這裡可以看到,stack裡面裝的layer就是,layer.handle==真正的回撥函式那種了。之後執行layer.handle_request(req, res, next),走的過程也就是第二種路由走的過程了。

但是會很奇怪,為什麼在這裡又定義了一個next()函式??

答案:這裡面(也就是帶路由的layer,layer.route.stack),stack同樣是一個陣列,這個陣列裡的layer,layer裡面的handle== 真正路由回撥函式,所以需要先把這個陣列的layer遍歷完,執行相應的layer.handle,然後再回到app._router.stack,繼續遍歷app._router.stack。(當然前提是都執行next,不執行next直接就停了啊)。

再貼段程式碼吧,layer.route.stack裡面的兩個layer分別裝這兩個回撥函式。

app.get('/',(req,res,next)=>{
    console.log(1);
    next()
},(req,res,next)=>{
    console.log(2);
    next();
})
複製程式碼

所以這個next是為了遍歷路由layer裡面的stack,等都執行完,再去執行app._router.stack。

具體咋做的呢,繼續看程式碼:dispatch函式的第三個函式done實際上就是proto.handle裡面的next.在112行,隨著在路由layer裡面執行next後 , idx++; 等到陣列滿了,也就會走到done函式了,也就會繼續遍歷app._route.stack裡面的layer了。

得個結論先

也就是說有兩個next , 一個是最上面那個, proto.handle裡面的next方法 ,控制著app._router.stack裡面的layer, 一個是route.dispatch裡面定義的next方法 ,控制著路由layer裡面的route.stack裡面的layer。也就是說通過app.get方法的回撥函式裡面的第三個引數next的定義是route.dispatch裡面定義的next方法,app.use的回撥函式的第三個引數next是proto.handle裡面定義的next方法。

總結

語文不太好 ,寫的有點‘冗餘’,希望感興趣的同學認真看 ,能得到幫助

相關文章