聊一聊三步法解析Express源碼
在抖音上有幸看到一個(gè)程序員講述如何閱讀源代碼,主要分為三步:領(lǐng)悟思想、把握設(shè)計(jì)、體會(huì)細(xì)節(jié)。
- 領(lǐng)悟思想:只需體會(huì)作者設(shè)計(jì)框架的初衷和目的
- 把握設(shè)計(jì):只需體會(huì)代碼的接口和抽象類以及宏觀的設(shè)計(jì)
- 體會(huì)細(xì)節(jié):是基于頂層的抽象接口設(shè)計(jì),逐漸展開(kāi)代碼的畫卷
基于上述三步法,迫不及待的拿Express開(kāi)刀了。本次源碼解析有什么不到位的地方各位讀者可以在下面留言,我們一起交流。
一、領(lǐng)悟思想
在Express中文網(wǎng)上,介紹Express是基于Node.js平臺(tái),快速、開(kāi)放、極簡(jiǎn)的Web開(kāi)發(fā)框架。在這句話里面可以得到解讀出以下幾點(diǎn)含義:
Express是基于Node.js平臺(tái),并且具備快速、極簡(jiǎn)的特點(diǎn),說(shuō)明其初衷就是為了通過(guò)擴(kuò)展Node的功能來(lái)提高開(kāi)發(fā)效率。
開(kāi)放的特點(diǎn)說(shuō)明該框架不會(huì)對(duì)開(kāi)發(fā)者過(guò)多的限制,可以自由的發(fā)揮想象進(jìn)行功能的擴(kuò)展。
Express是Web開(kāi)發(fā)框架,說(shuō)明作者的定位就是為了更加方便的幫助我們處理HTTP的請(qǐng)求和響應(yīng)。
二、把握設(shè)計(jì)
理解了作者設(shè)計(jì)的思想,下面從源碼目錄、核心設(shè)計(jì)原理及抽象接口三個(gè)層面來(lái)對(duì)Express進(jìn)行整體的把握。
2.1 源碼目錄
如下所示是Express的源碼目錄,相比較來(lái)說(shuō)還是比較簡(jiǎn)單的。
- ├─application.js---創(chuàng)建Express應(yīng)用后可直接調(diào)用的api均在此處(核心)
- ├─express.js---入口文件,創(chuàng)建一個(gè)Express應(yīng)用
- ├─request.js---豐富了http中request實(shí)例上的功能
- ├─response.js---豐富了http中response實(shí)例上的功能
- ├─utils.js---工具函數(shù)
- ├─view.js---與模板渲染相關(guān)的內(nèi)容
- ├─router---與路由相關(guān)的內(nèi)容(核心)
- | ├─index.js
- | ├─layer.js
- | └route.js
- ├─middleware---與中間件相關(guān)的內(nèi)容
- | ├─init.js---會(huì)將新增加在request和response新增加的功能掛載到原始請(qǐng)求的request和response的原型上
- | └query.js---將請(qǐng)求url中的query部分添加到request的query屬性上
2.2 抽象接口
對(duì)源碼的目錄結(jié)構(gòu)有了一定了解,下面利用UML類圖對(duì)該系統(tǒng)各個(gè)模塊的依賴關(guān)系進(jìn)一步了解,為后續(xù)源碼分析打好基礎(chǔ)。
2.3 設(shè)計(jì)原理
這一部分是整個(gè)Express框架的核心,下圖是整個(gè)框架的運(yùn)行流程,一看是不是很懵逼,為了搞清楚這一部分,需要明確四個(gè)概念:Application、Router、Layer、Route。
為了明確上述四個(gè)概念,先引入一段代碼
- const express = require('./express');
- const res = require('./response');
- const app = express();
- app.get('/test1', (req, res, next) => {
- console.log('one');
- next();
- }, (req, res) => {
- console.log('two');
- res.end('two');
- })
- app.get('/test2', (req, res, next) => {
- console.log('three');
- next();
- }, (req, res) => {
- console.log('four');
- res.end('four');
- })
- app.listen(3000);
1.Application
表示一個(gè)Express應(yīng)用,通過(guò)express()即可進(jìn)行創(chuàng)建。
2.Router
路由系統(tǒng),用于調(diào)度整個(gè)系統(tǒng)的運(yùn)行,在上述代碼中該路由系統(tǒng)包含app.get('/test1',……)和app.get('/test2',……)兩大部分
3.Layer
代表一層,對(duì)于上述代碼中app.get('/test1',……)和app.get('/test2',……)都可以成為一個(gè)Layer
4.Route
一個(gè)Layer中會(huì)有多個(gè)處理函數(shù)的情況,這多個(gè)處理函數(shù)構(gòu)成了Route,而Route中的每一個(gè)函數(shù)又成為Route中的Layer。對(duì)于上述代碼中,app.get('/test1',……)中的兩個(gè)函數(shù)構(gòu)成一個(gè)Route,每個(gè)函數(shù)又是Route中的Layer。
了解完上述概念后,結(jié)合該幅圖,就大概能對(duì)整個(gè)流程有了直觀感受。首先啟動(dòng)服務(wù),然后客戶端發(fā)起了http://localhost:3000/test2的請(qǐng)求,該過(guò)程應(yīng)該如何運(yùn)行呢?
啟動(dòng)服務(wù)時(shí)會(huì)依次執(zhí)行程序,將該路由系統(tǒng)中的路徑、請(qǐng)求方法、處理函數(shù)進(jìn)行存儲(chǔ)(這些信息根據(jù)一定結(jié)構(gòu)存儲(chǔ)在Router、Layer和Route中)
對(duì)相應(yīng)的地址進(jìn)行監(jiān)聽(tīng),等待請(qǐng)求到達(dá)。
請(qǐng)求到達(dá),首先根據(jù)請(qǐng)求的path去從上到下進(jìn)行匹配,路徑匹配正確則進(jìn)入該Layer,否則跳出該Layer。
若匹配到該Layer,則進(jìn)行請(qǐng)求方式的匹配,若匹配方式匹配正確,則執(zhí)行該對(duì)應(yīng)Route中的函數(shù)。
上述解釋的比較簡(jiǎn)單,后續(xù)會(huì)在細(xì)節(jié)部分進(jìn)一步闡述。
三、體會(huì)細(xì)節(jié)
通過(guò)上述對(duì)Express設(shè)計(jì)原理的分析,下面將從兩個(gè)方面做進(jìn)一步的源碼解讀,下面流程圖是一個(gè)常見(jiàn)的Express項(xiàng)目的過(guò)程,首先會(huì)進(jìn)行app實(shí)例初始化、然后調(diào)用一系列中間件,最后建立監(jiān)聽(tīng)。對(duì)于整個(gè)工程的運(yùn)行來(lái)說(shuō),主要分為兩個(gè)階段:初始化階段、請(qǐng)求處理階段,下面將以app.get()為例來(lái)闡述一下該核心細(xì)節(jié)。
3.1 初始化階段
下面利用app.get()這個(gè)路由來(lái)了解一下工程的初始化階段。
1.首先來(lái)看一下app.get()的內(nèi)容(源代碼中app.get()是通過(guò)遍歷methods的方式產(chǎn)生)
- app.get = function(path){
- // ……
- this.lazyrouter();
- var route = this._router.route(path);
- route.get.apply(route, slice.call(arguments, 1));
- return this;
- };
2.在app.lazyrouter()會(huì)完成router的實(shí)例化過(guò)程
- app.lazyrouter = function lazyrouter() {
- if (!this._router) {
- this._router = new Router({
- caseSensitive: this.enabled('case sensitive routing'),
- strict: this.enabled('strict routing')
- });
- // 此處會(huì)使用一些中間件
- this._router.use(query(this.get('query parser fn')));
- this._router.use(middleware.init(this));
- }
- };
注意:該過(guò)程中其實(shí)是利用了單例模式,保證整個(gè)過(guò)程中獲取router實(shí)例的唯一性。
3.調(diào)用router.route()方法完成layer的實(shí)例化、處理及保存,并返回實(shí)例化后的route。(注意源碼中是proto.route)
- router.prototype.route = function route(path) {
- var route = new Route(path);
- var layer = new Layer(path, {
- sensitive: this.caseSensitive,
- strict: this.strict,
- end: true
- }, route.dispatch.bind(route));
- layer.route = route;// 把route放到layer上
- this.stack.push(layer); // 把layer放到數(shù)組中
- return route;
- };
4.將該app.get()中的函數(shù)存儲(chǔ)到route的stack中。(注意源碼中也是通過(guò)遍歷method的方式將get掛載到route的prototype上)
- Route.prototype.get = function(){
- var handles = flatten(slice.call(arguments));
- for (var i = 0; i < handles.length; i++) {
- var handle = handles[i];
- // ……
- // 給route添加layer,這個(gè)層中需要存放方法名和handler
- var layer = Layer('/', {}, handle);
- layer.method = method;
- this.methods[method] = true;
- this.stack.push(layer);
- }
- return this;
- };
注意:上述代碼均刪除了源碼中一些異常判斷邏輯,方便讀者看清整體框架。
通過(guò)上述的分析,可以看出初始化階段主要做了兩件事情:
將路由處理方式(app.get()、app.post()……)、app.use()等劃分為路由系統(tǒng)中的一個(gè)Layer。
對(duì)于每一個(gè)層中的處理函數(shù)全部存儲(chǔ)至Route對(duì)象中,一個(gè)Route對(duì)象與一個(gè)Layer相互映射。
3.2 請(qǐng)求處理階段
當(dāng)服務(wù)啟動(dòng)后即進(jìn)入監(jiān)聽(tīng)狀態(tài),等待請(qǐng)求到達(dá)后進(jìn)行處理。
1.app.listen()使服務(wù)進(jìn)入監(jiān)聽(tīng)狀態(tài)(實(shí)質(zhì)上是調(diào)用了http模塊)
- app.listen = function listen() {
- var server = http.createServer(this);
- return server.listen.apply(server, arguments);
- };
2.當(dāng)連接建立會(huì)調(diào)用app實(shí)例,app實(shí)例中會(huì)立即執(zhí)行app.handle()函數(shù),app.handle()函數(shù)會(huì)立即調(diào)用路由系統(tǒng)的處理函數(shù)router.handle()
- app.handle = function handle(req, res, callback) {
- var router = this._router;
- // 如果路由系統(tǒng)中處理不了這個(gè)請(qǐng)求,就調(diào)用done方法
- var done = callback || finalhandler(req, res, {
- env: this.get('env'),
- onerror: logerror.bind(this)
- });
- //……
- router.handle(req, res, done);
- };
3.router.handle()主要是根據(jù)路徑獲取是否有匹配的layer,當(dāng)匹配到之后則調(diào)用layer.prototype.handle_request()去執(zhí)行route中內(nèi)容的處理
- router.prototype.handle = function handle(req, res, out) {
- // 這個(gè)地方參數(shù)out就是done,當(dāng)所有都匹配不到,就從路由系統(tǒng)中出來(lái),名字很形象
- var self = this;
- // ……
- var stack = self.stack;
- // ……
- next();
- function next(err) {
- // ……
- // get pathname of request
- var path = getPathname(req);
- // find next matching layer
- var layer;
- var match;
- var route;
- while (match !== true && idx < stack.length) {
- layer = stack[idx++];
- match = matchLayer(layer, path);
- route = layer.route;
- // ……
- }
- // no match
- if (match !== true) {
- return done(layerError);
- }
- // ……
- // Capture one-time layer values
- req.params = self.mergeParams
- ? mergeParams(layer.params, parentParams)
- : layer.params;
- var layerPath = layer.path;
- // this should be done for the layer
- self.process_params(layer, paramcalled, req, res, function (err) {
- if (err) {
- return next(layerError || err);
- }
- if (route) {
- return layer.handle_request(req, res, next);
- }
- trim_prefix(layer, layerError, layerPath, path);
- });
- }
- function trim_prefix(layer, layerError, layerPath, path) {
- // ……
- if (layerError) {
- layer.handle_error(layerError, req, res, next);
- } else {
- layer.handle_request(req, res, next);
- }
- }
- };
4.layer.handle_request()會(huì)調(diào)用route.dispatch()觸發(fā)route中內(nèi)容的執(zhí)行
- Layer.prototype.handle_request = function handle(req, res, next) {
- var fn = this.handle;
- if (fn.length > 3) {
- // not a standard request handler
- return next();
- }
- try {
- fn(req, res, next);
- } catch (err) {
- next(err);
- }
- };
5.route中的通過(guò)判斷請(qǐng)求的方法和route中l(wèi)ayer的方法是否匹配,匹配的話則執(zhí)行相應(yīng)函數(shù),若所有route中的layer都不匹配,則調(diào)到外層的layer中繼續(xù)執(zhí)行。
- Route.prototype.dispatch = function dispatch(req, res, done) {
- var idx = 0;
- var stack = this.stack;
- if (stack.length === 0) {
- return done();
- }
- var method = req.method.toLowerCase();
- // ……
- next();
- // 此next方法是用戶調(diào)用的next,如果調(diào)用next會(huì)執(zhí)行內(nèi)層的next方法,如果沒(méi)有匹配到會(huì)調(diào)用外層的next方法
- function next(err) {
- // ……
- var layer = stack[idx++];
- if (!layer) {
- return done(err);
- }
- if (layer.method && layer.method !== method) {
- return next(err);
- }
- // 如果當(dāng)前route中的layer的方法匹配到了,執(zhí)行此layer上的handler
- if (err) {
- layer.handle_error(err, req, res, next);
- } else {
- layer.handle_request(req, res, next);
- }
- }
- };
通過(guò)上述的分析,可以看出初始化階段主要做了兩件事情:
- 首先判斷l(xiāng)ayer中的path和請(qǐng)求的path是否一致,一致則會(huì)進(jìn)入route進(jìn)行處理,否則調(diào)到下一層layer
- 在route中會(huì)判斷route中的layer與請(qǐng)求方法是否一致,一致的話則函數(shù)執(zhí)行,否則不執(zhí)行,所有route中的layer執(zhí)行完后跳到下層的layer進(jìn)行執(zhí)行。