概述
NodeJS官方提供的最簡單的伺服器例子如下:
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader(`Content-Type`, `text/plain`);
res.end(`Hello World!
`);
});
複製程式碼
Express框架沒有那麼神奇,只是代理了http.createServer(requestHandler)
中的requestHandler。並使用已經註冊了的中介軟體和路由匹配響應傳來的使用者請求。
整體思路
通過閱讀原始碼,我覺得可以把Express邏輯分成兩段:啟動服務和響應請求。
啟動服務階段指的是http.createServer(requestHandler)
和server.listener()
兩個API被呼叫前執行的一系列初始化工作。
響應請求階段指的是伺服器接收來自客戶端請求時觸發的request事件的handler。
啟動服務階段
啟動服務最重要的部分就是註冊中介軟體和路由了。
中介軟體和路由可以說是幾乎所有伺服器都會提供的功能。在Express框架裡,中介軟體和路由都會抽象成layer物件,在這篇文章裡,儲存中介軟體layer物件的容器叫做中介軟體router物件,儲存路由layer物件的容器叫做路由router物件。
在Express框架裡,中介軟體就是匹配路徑就會執行的回撥,而路由不僅要匹配路徑還要匹配http method(如get、post之類)。所以對於中介軟體router物件,匹配路徑之後會直接執行回撥,但是路由router物件的匹配路徑之後執行的回撥統一為router.handle(req, res, next)
,裡面的邏輯會繼續匹配http method。
1. app.use
方法
不論是註冊中介軟體router物件還是路由router物件,我們都會使用app.use
。
app.use
方法實質上是呼叫它自身的router物件的use方法:
var router = this._router;
fns.forEach(function (fn) {
// non-express app
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
debug(`.use app under %s`, path);
fn.mountpath = path;
fn.parent = this;
// restore .app property on req and res
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
// mounted an app
fn.emit(`mount`, this);
}, this);
複製程式碼
2. 中介軟體router物件
當我們呼叫類似app.use(`/`, fn)
這樣的語句,其實就是註冊中介軟體。
這裡必須說明一下,每一個express app初始化的時候會使用app.lazyrouter()
來例項化一個router物件,在這篇文章裡,我們姑且叫它中介軟體router物件,因為它主要是負責儲存中介軟體layer物件的,但是它還可以註冊router物件,例如開發中我們會呼叫形如app.use(`/test`, testRouter)
的語句。
中介軟體router物件維護這一個stack陣列,用來裝載Layer物件。
當router物件的use方法被呼叫的時候,就會把路徑和回撥封裝成一個Layer物件,並放入stack陣列中。
請注意:中介軟體router物件的layer物件的route是undefined,跟路由router物件的layer物件的route是不一樣的。
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
複製程式碼
3. 路由router物件
當我們呼叫形如app.use(`/test`, testRouter)
的語句,可以表述為註冊了一個路由中介軟體,而這個中介軟體就是下面的router
函式:
function router(req, res, next) {
router.handle(req, res, next);
}
複製程式碼
為了區別與中介軟體router物件,在這篇文章裡,把註冊在中介軟體router物件上的路由中介軟體定義為路由router物件。
到這裡,我最想告訴大家的是,在express裡,router物件是可以通過這種方式巢狀的。
就和前面提到的一樣,路由也會被抽象成layer物件,並把router
函式作為Layer建構函式的第三個引數傳入。
4. HTTP Method方法和Route例項
HTTP Method指的是get、post、put、delete、header之類的http請求方法。
路由router物件不僅需要匹配路徑還需要匹配HTTP Method。而負責匹配HTTP Method的功能是由Route例項來完成。
當我們在呼叫app[method]
或者router[method]
時,就是在呼叫router.route
方法(就是下面的this.route(path)
),如下:
// create Router#VERB functions
methods.concat(`all`).forEach(function(method){
proto[method] = function(path){
var route = this.route(path)
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
複製程式碼
router.route
方法裡面會生成一個新的layer物件,並把回撥設定為route.dispatch.bind(route)
,這一點與前面提到的中介軟體router物件不同,而且layer的route不再是undefined,最後返回新的Route例項。程式碼如下:
proto.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;
this.stack.push(layer);
return route;
};
複製程式碼
那麼返回的Route例項的作用是什麼呢?先看看它的建構函式:
function Route(path) {
this.path = path;
this.stack = [];
debug(`new %o`, path)
// route handlers for various http methods
this.methods = {};
}
複製程式碼
Route例項維護著一個stack陣列,作用是收集Layer物件;還維護這一個methods物件,作用是指示該route物件可以匹配的http methods。
route收集的Layer物件維護著路由真正的回撥,就是下面的handle:
var layer = Layer(`/`, {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
複製程式碼
5. Layer物件
一個Layer物件維護這一個路徑和回撥,它會把路徑正規表示式化,用以在響應請求階段匹配路徑,先看看它的建構函式:
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug(`new %o`, path)
var opts = options || {};
this.handle = fn;
this.name = fn.name || `<anonymous>`;
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);
// set fast path flags
this.regexp.fast_star = path === `*`
this.regexp.fast_slash = path === `/` && opts.end === false
}
複製程式碼
有三種layer物件:
Layer類別 | route | method |
---|---|---|
中介軟體Layer | undefined | undefined |
路由Layer | 非undefined | undefined |
route Layer | undefined | 非undefined |
中介軟體Layer例項的回撥是fn,也就是註冊的中介軟體函式;路由Layer例項的回撥都是function router(req, res, next)
;route Layer例項的回撥都是route.dispatch.bind(route)
。
響應請求階段
通過啟動服務階段,我們已經把伺服器的準備工作完成 —— 註冊了中介軟體和路由。
當應用執行到server.listener()
時,就可以開始接受並處理客戶端的請求,最後返回伺服器響應。
1. 增強req物件和res物件
當一個請求到來的時候,NodeJS會把請求抽象成req(http.IncomingMessage的例項),把響應抽象成res(http.ServerResponse的例項),傳入server的request事件的handler,但是在Express框架裡,req物件和res物件被增強了。
增強內容可以參考express.js同目錄下的request.js和response.js。
那麼是怎麼增強的呢?
在app.lazyrouter
方法裡,已經新增了一箇中介軟體,就是下面的middleware.init(this)
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled(`case sensitive routing`),
strict: this.enabled(`strict routing`)
});
this._router.use(query(this.get(`query parser fn`)));
this._router.use(middleware.init(this));
}
};
複製程式碼
而在middleware.init(this)
裡,可以看到重新設定了req和res的原型:
exports.init = function(app){
return function expressInit(req, res, next){
if (app.enabled(`x-powered-by`)) res.setHeader(`X-Powered-By`, `Express`);
req.res = res;
res.req = req;
req.next = next;
setPrototypeOf(req, app.request)
setPrototypeOf(res, app.response)
res.locals = res.locals || Object.create(null);
next();
};
};
複製程式碼
2. 正規表示式匹配中介軟體和路由
由於在啟動服務階段,我們已經註冊好了中介軟體和路由,並把它們都抽象成layer物件,所以在處理請求階段的時候,就清晰明瞭了。
基本邏輯是:
遍歷router維護的stack容器;
對於中介軟體layer(就是layer.route為undefined的),路徑匹配成功後就可以執行中介軟體函式了;
對於路由layer(就是layer.route不是undefined的),路徑匹配成功後還需要匹配http method才能執行路由函式。
這一過程,有如下的重要方法:
app.handle,express app處理請求的入口,實質上是呼叫了自身router的handle
router.handle,遍歷router維護的stack陣列,找到匹配路徑的layer物件
Route.prototype._handles_method,對於路由layer物件,還需要這個方法驗證是否可以匹配http method
Route.prototype.dispatch,遍歷route維護的stack陣列,找到匹配路徑和http method的layer物件
Layer.prototype.match,路徑匹配的關鍵
Layer.prototype.handle_request,匹配成功後執行回撥
3. 模板引擎
模板引擎並不是express作者原創的,而是引入了別的第三方庫,然後使用第三方庫提供的API渲染出響應頁面,並返回給客戶端。
目前支援較多的是ejs
和pug
這兩個模板引擎。
Express鑲嵌
一個Express app是可以掛載到另一個Express app上的,因為本質上一個Express app就是為了維護起自身的router物件,所以掛載的方式其實就是在parent express app的上註冊一箇中介軟體,該中介軟體負責把req和res傳遞給child express app,並讓它們建立起父子關係,原始碼如下:
// restore .app property on req and res
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
複製程式碼