express源碼閱讀

express源碼閱讀

簡介:這篇文章的主要目的是分析express的源碼,可是網絡上express的源碼評析已經數不勝數,因此本文章另闢蹊徑,準備仿製一個express的輪子,固然輪子的主體思路是閱讀express源碼所得。javascript

源碼地址:expross前端

1. 搭建結構

有了想法,下一步就是搭建一個山寨的框架,萬事開頭難,就從創建一個文件夾開始吧!java

首先創建一個文件夾,叫作expross(你沒有看錯,山寨從名稱開始)。node

expross
 |
 |-- application.js

接着建立application.js文件,文件的內容就是官網的例子。git

var http = require('http');

http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World');
}).listen(3000);

一個簡單的http服務就建立完成了,你能夠在命令行中啓動它,而expross框架的搭建就從這個文件出發。github

1.1 第一劃 Application

在實際開發過程當中,web後臺框架的兩個核心點就是路由和模板。路由說白了就是一組URL的管理,根據前端訪問的URL執行對應的處理函數。怎樣管理一組URL和其對應的執行函數呢?首先想到的就是數組(其實我想到的是對象)。web

建立一個名稱叫作router的數組對象。express

var http = require('http');

//路由
var router = [];
router.push({path: '*', fn: function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('404');
}}, {path: '/', fn: function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World');
}});


http.createServer(function(req, res) {
    //自動匹配
    for(var i=1,len=router.length; i<len; i++) {
        if(req.url === router[i].path) {
            return router[i].fn(req, res);
        }
    }
    return router[0].fn(req, res);
}).listen(3000);

router數組用來管理全部的路由,數組的每一個對象有兩個屬性組成,path表示路徑,fn表示路徑對應的執行函數。一切看起來都很不錯,可是這並非一個框架,爲了組成一個框架,而且貼近express,這裏繼續對上面的代碼進一步封裝。數組

首先定義一個類:Application緩存

var Application = function() {}

在這個類上定義二個函數:

Application.prototype.use = function(path, cb) {};

Application.prototype.listen = function(port) {};

把上面的實現,封裝到這個類中。use 函數表示增長一個路由,listen 函數表示監聽http服務器。

var http = require('http');

var Application = function() {
    this.router = [{
        path: '*', 
        fn: function(req, res) {
            res.writeHead(200, {'Content-Type': 'text/plain'});
            res.end('Cannot ' + req.method + ' ' + req.url);
        }
    }];
};

Application.prototype.use = function(path, cb) {
    this.router.push({
        path: path,
        fn: cb
    });
};

Application.prototype.listen = function(port) {
    var self = this;
    http.createServer(function(req, res) {
        for(var i=1,len=self.router.length; i<len; i++) {
            if(req.url === self.router[i].path) {
                return self.router[i].fn(req, res);
            }
        }
        return self.router[0].fn(req, res);
    }).listen(port);
};

能夠像下面這樣啓動它:

var app = new Application();
app.use('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World');
});
app.listen(3000);

看樣子已經和express的外觀很像了,爲了更像,這裏建立一個expross的文件,該文件用來實例化Application。代碼以下:

var Application = require('./application');
exports = module.exports = createApplication;

function createApplication() {
    var app = new Application();
    return app;
}

爲了更專業,調整目錄結構以下:

-----expross
 | |
 | |-- index.js
 | |
 | |-- lib
 |      |
 |      |-- application.js
 |      |-- expross.js
 |
 |---- test.js

運行node test.js,走起……

1.2 第二劃 Layer

爲了進一步優化代碼,這裏抽象出一個概念:Layer。表明層的含義,每一層就是上面代碼中的router數組的一個項。

Layer含有兩個成員變量,分別是path和handle,path表明路由的路徑,handle表明路由的處理函數fn。

------------------------------------------------
|     0     |     1     |     2     |     3     |      
------------------------------------------------
| Layer     | Layer     | Layer     | Layer     |
|  |- path  |  |- path  |  |- path  |  |- path  |
|  |- handle|  |- handle|  |- handle|  |- handle|
------------------------------------------------
                  router 內部

建立一個叫作layer的類,併爲該類添加兩個方法,handle_requestmatchmatch用來匹配請求路徑是否符合該層,handle_request用來執行路徑對應的處理函數。

function Layer(path, fn) {
    this.handle = fn;
    this.name = fn.name || '<anonymous>';
    this.path = path;
}
//簡單處理
Layer.prototype.handle_request = function (req, res) {
  var fn = this.handle;

  if(fn) {
      fn(req, res);
  }
}
//簡單匹配
Layer.prototype.match = function (path) {
    if(path === this.path) {
        return true;
    }
    
    return false;
}

由於router數組中存放的將是Layer對象,因此修改Application.prototype.use代碼以下:

Application.prototype.use = function(path, cb) {
    this.router.push(new Layer(path, cb));
};

固然也不要忘記Application構造函數的修改。

var Application = function() {
    this.router = [new Layer('*', function(req, res) {
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('Cannot ' + req.method + ' ' + req.url);
    })];
};

接着改變listen函數,將其主要的處理邏輯抽取成handle函數,用來匹配處理請求信息。這樣可讓函數自己的語意更明確,而且遵照單一原則。

Application.prototype.handle = function(req, res) {
    var self = this;

    for(var i=0,len=self.router.length; i<len; i++) {
        if(self.router[i].match(req.url)) {
            return self.router[i].handle_request(req, res);
        }
    }

    return self.router[0].handle_request(req, res);
};

listen函數變得簡單明瞭。

Application.prototype.listen = function(port) {
    var self = this;

    http.createServer(function(req, res) {
        self.handle(req, res);
    }).listen(port);
};

運行node test.js,走起……

1.3 第三劃 router

Application類中,成員變量router負責存儲應用程序的全部路由和其處理函數,既然存在這樣一個對象,爲什麼不將其封裝成一個Router類,這個類負責管理全部的路由,這樣職責更加清晰,語意更利於理解。

so,這裏抽象出另外一個概念:Router,表明一個路由組件,包含若干層的信息。

創建Router類,並將原來Application內的代碼移動到Router類中。

var Router = function() {
    this.stack = [new Layer('*', function(req, res) {
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('Cannot ' + req.method + ' ' + req.url);
    })];
};

Router.prototype.handle = function(req, res) {
    var self = this;

    for(var i=0,len=self.stack.length; i<len; i++) {
        if(self.stack[i].match(req.url)) {
            return self.stack[i].handle_request(req, res);
        }
    }

    return self.stack[0].handle_request(req, res);
};

Router.prototype.use = function(path, fn) {
    this.stack.push(new Layer(path, fn));
};

爲了利於管理,現將路由相關的文件放到一個目錄中,命名爲router。將Router類文件命名爲index.js保存到router文件夾內,並將原來的layer.js移動到該文件夾。現目錄結構以下:

-----expross
 | |
 | |-- index.js
 | |
 | |-- lib
 |      |
 |      |-- router
 |      |     |
 |      |     |-- index.js
 |      |     |-- layer.js
 |      |     
 |      |
 |      |-- application.js
 |      |-- expross.js
 |
 |---- test.js

修改原有application.js文件,將代碼原有router的數組移除,新增長_router對象,該對象是Router類的一個實例。

var Application = function() {
    this._router = new Router();
};

Application.prototype.use = function(path, fn) {
    var router = this._router;
    return router.use(path, fn);
};

Application.prototype.handle = function(req, res) {
    var router = this._router;
    router.handle(req, res);
};

到如今爲止,總體的框架思路已經很是的明確,一個應用對象包括一個路由組件,一個路由組件包括n個層,每一個層包含路徑和處理函數。每次請求就遍歷應用程序指向的路由組件,經過層的成員函數match來進行匹配識別URL訪問的路徑,若是成功則調用層的成員函數handle_request進行處理。

運行node test.js,走起……

1.4 第四劃 route

若是研究過路由相關的知識就會發現,路由實際上是由三個參數構成的:請求的URI、HTTP請求方法和路由處理函數。以前的代碼只處理了其中兩種,對於HTTP請求方法這個參數卻刻意忽略,如今是時候把它加進來了。

按照上面的結構,若是加入請求方法參數,確定會加入到Layer裏面。可是再加入以前,須要仔細分析一下路由的常見方式:

GET        /pages
GET     /pages/1
POST    /page
PUT     /pages/1
DELETE     /pages/1

HTTP的請求方法有不少,上面的路由列表是一組常見的路由樣式,遵循REST原則。分析一下會發現大部分的請求路徑實際上是類似或者是一致的,若是將每一個路由都創建一個Layer添加到Router裏面,從效率或者語意上都稍微有些不符,由於他們是一組URL,負責管理page相關信息的URL,可否把這樣相似訪問路徑相同而請求方法不一樣的路由劃分到一個組裏面呢?

答案是能夠行的,這就須要再次引入一個概念:route,專門來管理具體的路由信息。

------------------------------------------------
|     0     |     1     |     2     |     3     |      
------------------------------------------------
| item      | item      | item      | item      |
|  |- method|  |- method|  |- method|  |- method|
|  |- handle|  |- handle|  |- handle|  |- handle|
------------------------------------------------
                  route 內部

在寫代碼以前,先梳理一下上面全部的概念之間的關係:application、expross、router、route和layer。

--------------
| Application  |                                 ---------------------------------------------------------
|     |           |        ----- -----------        |     0     |     1     |     2     |     3     |  ...  |
|      |-router | ----> |     | Layer     |       ---------------------------------------------------------
 --------------        |  0  |   |-path  |       | item      | item      | item      | item      |       |
  application           |     |   |-route | ----> |  |- method|  |- method|  |- method|  |- method|  ...  |
                       |-----|-----------|       |  |- handle|  |- handle|  |- handle|  |- handle|       |
                       |     | Layer     |       ---------------------------------------------------------
                       |  1  |   |-path  |                                  route
                       |     |   |-route |       
                       |-----|-----------|       
                       |     | Layer     |
                       |  2  |   |-path  |
                       |     |   |-route |
                       |-----|-----------|
                       | ... |   ...     |
                        ----- ----------- 
                             router

application表明一個應用程序。expross是一個工廠類負責建立application對象。router是一個路由組件,負責整個應用程序的路由系統。route是路由組件內部的一部分,負責存儲真正的路由信息,內部的每一項都表明一個路由處理函數。router內部的每一項都是一個layer對象,layer內部保存一個route和其表明的URI。

若是一個請求來臨,會現從頭到尾的掃描router內部的每一層,而處理每層的時候會先對比URI,匹配掃描route的每一項,匹配成功則返回具體的信息,沒有任何匹配則返回未找到。

建立Route類,定義三個成員變量和三個方法。path表明該route所對應的URI,stack表明上圖中route內部item所在的數組,methods用來快速判斷該route中是是否存在某種HTTP請求方法。

var Route = function(path) {
    this.path = path;
    this.stack = [];

    this.methods = {};
};

Route.prototype._handles_method = function(method) {
    var name = method.toLowerCase();
    return Boolean(this.methods[name]);
};

Route.prototype.get = function(fn) {
    var layer = new Layer('/', fn);
    layer.method = 'get';

    this.methods['get'] = true;
    this.stack.push(layer);

    return this;
};

Route.prototype.dispatch = function(req, res) {
    var self = this,
        method = req.method.toLowerCase();

    for(var i=0,len=self.stack.length; i<len; i++) {
        if(method === self.stack[i].method) {
            return self.stack[i].handle_request(req, res);
        }
    }
};

在上面的代碼中,並無定義前面結構圖中的item對象,而是使用了Layer對象進行替代,主要是爲了方便快捷,從另外一種角度看,其實兩者是存在不少共同點的。另外,爲了利於理解,代碼中只實現了GET方法,其餘方法的代碼實現是相似的。

既然有了Route類,接下來就改修改原有的Router類,將route集成其中。

Router.prototype.handle = function(req, res) {
    var self = this,
        method = req.method;

    for(var i=0,len=self.stack.length; i<len; i++) {
        if(self.stack[i].match(req.url) && 
            self.stack[i].route && self.stack[i].route._handles_method(method)) {
            return self.stack[i].handle_request(req, res);
        }
    }

    return self.stack[0].handle_request(req, res);
};

Router.prototype.get = function(path, fn) {
    var route = this.route(path);
    route.get(fn);
    return this;
};

Router.prototype.route = function route(path) {
    var route = new Route(path);

    var layer = new Layer(path, function(req, res) {
        route.dispatch(req, res)
    });

    layer.route = route;

    this.stack.push(layer);
    return route;
};

代碼中,暫時去除use方法,建立get方法用來添加請求處理函數,route方法是爲了返回一個新的Route對象,並將改層加入到router內部。

最後修改Application類中的函數,去除use方法,加入get方法進行測試。

Application.prototype.get = function(path, fn) {
    var router = this._router;
    return router.get(path, fn);
};

Application.prototype.route = function (path) {
  return this._router.route(path);
};

測試代碼以下:

var expross = require('./expross');
var app = expross();

app.get('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World');
});

app.route('/book')
.get(function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Get a random book');
});

app.listen(3000);

運行node test.js,走起……

1.5 第五劃 next

next 主要負責流程控制。在實際的代碼中,有不少種狀況都須要進行權限控制,例如:

var expross = require('./expross');
var app = expross();

app.get('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('first');
});

app.get('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('second');
});

app.listen(3000);

上面的代碼若是執行會發現永遠都返回first,可是有的時候會根據前臺傳來的參數動態判斷是否執行接下來的路由,怎樣才能跳過first進入secondexpress引入了next的概念。

跳轉到任意layer,成本是比較高的,大多數的狀況下並不須要。在express中,next跳轉函數,有兩種類型:

  • 跳轉到下一個處理函數。執行 next()

  • 跳轉到下一組route。執行 next('route')

要想使用next的功能,須要在代碼書寫的時候加入該參數:

var expross = require('./expross');
var app = expross();

app.get('/', function(req, res, next) {
    console.log('first');
    next();
});

app.get('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('second');
});

app.listen(3000);

而該功能的實現也很是簡單,主要是在調用處理函數的時候,除了須要傳入req、res以外,再傳一個流程控制函數next。

Router.prototype.handle = function(req, res) {
    var self = this,
        method = req.method,
        i = 1, len = self.stack.length,
        stack;

    function next() {
        if(i >= len) {        
            return self.stack[0].handle_request(req, res);
        }

        stack = self.stack[i++];

        if(stack.match(req.url) && stack.route 
            && stack.route._handles_method(method)) {
            return stack.handle_request(req, res, next);
        } else {
            next();
        }
    }

    next();
};

修改原有Router的handle函數。由於要控制流程,因此for循環並非很合適,能夠更換爲while循環,或者乾脆使用相似遞歸的手法。

代碼中定義一個next函數,而後執行next函數進行自啓動。next內部和以前的操做相似,主要是執行handle_request函數進行處理,不一樣之處是調用該函數的時候,將next自己當作參數傳入,這樣能夠在內部執行該函數進行下一個處理,相似給handle_request賦予for循環中++的能力。

按照相同的方式,修改Route的dispatch函數。

Route.prototype.dispatch = function(req, res, done) {
    var self = this,
        method = req.method.toLowerCase(),
        i = 0, len = self.stack.length, stack;

    function next(gt) {
        if(gt === 'route') {
            return done();
        }

        if(i >= len) {
            return done();
        }

        stack = self.stack[i++];

        if(method === stack.method) {
            return stack.handle_request(req, res, next);
        } else {
            next();
        }        
    }

    next();
};

代碼思路基本和上面的相同,惟一的差異就是增長route判斷,提供跳過當前整組處理函數的能力。

Layer.prototype.handle_request = function (req, res, next) {
  var fn = this.handle;

  if(fn) {
      fn(req, res, next);
  }
}

Router.prototype.route = function route(path) {
    var route = new Route(path);

    var layer = new Layer(path, function(req, res, next) {
        route.dispatch(req, res, next)
    });

    layer.route = route;

    this.stack.push(layer);
    return route;
};

最後不要忘記修改Layer的handle_request函數和Router的route函數。

1.6 後記

該小結基本結束,固然若是要繼續還能夠寫不少內容,包括錯誤處理、函數重載、高階函數(生成各類HTTP函數),以及各類神奇的用法,如繼承、緩存、複用等等。

可是我以爲搭建結構這一結已經將express的基本結構捋清了,若是重頭到尾的走下來,再去讀框架的源碼應該是沒有問題的。

接下來繼續山寨express 的其餘部分。

相關文章
相關標籤/搜索