Express原始碼解析
NodeJS官方提供的最簡單的伺服器例子如下:
const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World!\n'); }); 複製程式碼
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); }); }); 複製程式碼