express中介軟體,瞭解一下
本篇文章從express原始碼上來理解中介軟體,同時可以對express有更深層的理解
前言
中介軟體函式可以執行哪些任務?
- 執行任何程式碼。
- 對請求和響應物件進行更改。
- 結束請求/響應迴圈。
- 呼叫堆疊中的下一個中介軟體函式。
我們從一個app.use
開始,逐步分析到下一個中介軟體函式的執行。
初始化伺服器
首先從github上下載express
原始碼。
建立一個檔案test.js
檔案,引入根目錄的index.js
檔案,例項化express
,啟動伺服器。
let express = require('../index.js');
let app = express()
function middlewareA(req, res, next) {
console.log('A1');
next();
console.log('A2');
}
function middlewareB(req, res, next) {
console.log('B1');
next();
console.log('B2');
}
function middlewareC(req, res, next) {
console.log('C1');
next();
console.log('C2');
}
app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);
app.listen(8888 , () => {
console.log("伺服器已經啟動訪問http://127.0.0.1:8888");
})
複製程式碼
啟動伺服器,通過訪問http://127.0.0.1:8888
服務,開啟終端,看看終端日誌執行順序。
從日誌我們可以看出,每次next()
之後,都會按照順序依次呼叫下中介軟體函式,然後按照執行順序依次列印A1,B1,C1
,此時中介軟體已經呼叫完成,再依次列印C2,B2,A2
。
目錄結構
--lib
|__ middleware
|__ init.js
|__ query.js
|__ router
|__ index.js
|__ layer.js
|__ route.js
|__ application.js
|__ express.js
|__ request.js
|__ response.js
|__ utils.js
|__ view.js
複製程式碼
通過例項化的express,我們可以看到,index.js
檔案實際上是暴露出lib/express
的檔案。
例項化express
express
,通過mixin
繼承appLication,同時初始化application。
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
// expose the prototype that will get set on requests
app.request = Object.create(req, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
// expose the prototype that will get set on responses
app.response = Object.create(res, {
app: { configurable: true, enumerable: true, writable: true, value: app }
})
app.init();
return app;
}
複製程式碼
而mixin是merge-descriptors
npm模組。Merge objects using descriptors.。
開啟application.js
檔案,發現express
的例項化源自var app = exports = module.exports = {}
。
進一步搜尋app.use
,找到app.use
,而app.use
又只是嚮應用程式路由器新增中介軟體的Proxy
。
/**
* Proxy `Router#use()` to add middleware to the app router.
* See Router#use() documentation for details.
*
* If the _fn_ parameter is an express app, then it will be
* mounted at the _route_ specified.
*
* @public
*/
app.use = function use(fn) {
var offset = 0;
var path = '/';
// 預設path 為 '/'
// app.use([fn])
//判斷app.use傳進來的是否是函式
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// 第一個引數是路徑
//取出第一個引數,將第一個引數賦值給path。
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
//slice.call(arguments,offset),通過slice轉為資料,slice可以改變具有length的類陣列。
//arguments是一個類陣列物件。
//處理多種中介軟體使用方式。
// app.use(r1, r2);
// app.use('/', [r1, r2]);
// app.use(mw1, [mw2, r1, r2], subApp);
var fns = flatten(slice.call(arguments, offset));//[funtion]
//丟擲錯誤
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}
//設定router
this.lazyrouter();
var router = this._router;
fns.forEach(function (fn) {
// 處理不是express的APP應用的情況,直接呼叫route.use。
if (!fn || !fn.handle || !fn.set) {
//path default to '/'
return router.use(path, fn);
}
debug('.use app under %s', path);
fn.mountpath = path;
fn.parent = this;
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);
});
});
// app mounted 觸發emit
fn.emit('mount', this);
}, this);
return this;
};
複製程式碼
定義預設引數offer
和path
。然後處理fn
形參不同型別的情況。將不同型別的中介軟體使用方式的形參轉為扁平化陣列,賦值給fns
。
forEach
遍歷fns,判斷如果fn
、fn.handle
、fn.set
引數不存在,return出去router.use(path, fn)
。
否則繼續執行router.use
。
呼叫handle
函式,執行中介軟體。
程式碼如下:
/**
*將一個req, res對分派到應用程式中。中介軟體執行開始。
*如果沒有提供回撥,則預設錯誤處理程式將作出響應
*在堆疊中冒泡出現錯誤時。
*/
app.handle = function handle(req, res, callback) {
var router = this._router;
// 最後報錯處理error。
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
// no routes
if (!router) {
debug('no routes defined on app');
done();
return;
}
router.handle(req, res, done);
};
複製程式碼
惰性新增Router。
從上述程式碼中可以知道,app.use
的作用實際上是將各種應用函式傳遞給router
的一箇中間層代理。
而且,在app.use中有呼叫this.lazyrouter()
函式,惰性的新增預設router
。
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')));
//初始化router
this._router.use(middleware.init(this));
}
};
複製程式碼
這裡對Router
進行了例項化,同時設定基本的option
,caseSensitive
是否區分大小寫,strict
是否設定嚴格模式。
Router
初始化如下:
/**
* 用給定的“選項”初始化一個新的“路由器”。
*
* @param {Object} [options] [{ caseSensitive: false, strict: false }]
* @return {Router} which is an callable function
* @public
*/
var proto = module.exports = function(options) {
var opts = options || {};
function router(req, res, next) {
router.handle(req, res, next);
}
// 混合路由器類函式
setPrototypeOf(router, proto)
router.params = {};
router._params = [];
router.caseSensitive = opts.caseSensitive;
router.mergeParams = opts.mergeParams;
router.strict = opts.strict;
router.stack = [];
return router;
};
複製程式碼
呼叫app.use
時,引數都會傳遞給router.use
,因此,開啟router/index.js
檔案,查詢router.use
。
/**
*使用給定的中介軟體函式,具有可選路徑,預設為“/”。
* Use(如' .all ')將用於任何http方法,但不會新增
*這些方法的處理程式,所以選項請求不會考慮“。use”
*函式,即使它們可以響應。
*另一個區別是_route_ path被剝離,不可見
*到處理程式函式。這個特性的主要作用是安裝
*無論“字首”是什麼,處理程式都可以在不更改任何程式碼的情況下操作
*路徑名。
*
* @public
*/
proto.use = function use(fn) {
var offset = 0;
var path = '/';
// 預設路徑 '/'
// 消除歧義 router.use([fn])
// 判斷是否是函式
if (typeof fn !== 'function') {
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
// 第一個引數是函式
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
//將arguments轉為陣列,然後扁平化多維陣列
var callbacks = flatten(slice.call(arguments, offset));
//如果callbacks內沒有傳遞函式,拋錯
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}
//迴圈callbacks陣列
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
}
//解析下query和expressInit的含義
// 新增中介軟體
//匿名 anonymous 函式
debug('use %o %s', path, fn.name || '<anonymous>')
var layer = new Layer(path, {
sensitive: this.caseSensitive, //敏感區分大小寫 //預設為false
strict: false, //嚴格
end: false //結束
}, fn);
layer.route = undefined;
this.stack.push(layer);
}
return this;
}
複製程式碼
router.use
的主要作用就是將從app.use
中傳遞過來的函式,通過Layer
例項化的處理,新增一些處理錯誤、處理請求的方法,以便後續呼叫處理。同時將傳遞過來的path
,通過path-to-regexp
模組把路徑轉為正則表示式(this.regexp
),呼叫this.regexp.exec(path)
,將引數提取出來。
Layer
程式碼較多,這裡不貼程式碼了,可以參考express/lib/router/layer.js。
處理中介軟體。
處理中介軟體就是將放入this,stack
的new Layout([options],fn)
,拿出來依次執行。
proto.handle = function handle(req, res, out) {
var self = this;
debug('dispatching %s %s', req.method, req.url);
var idx = 0;
//獲取協議與URL地址
var protohost = getProtohost(req.url) || ''
var removed = '';
//是否新增斜槓
var slashAdded = false;
var paramcalled = {};
//儲存選項請求的選項
//僅在選項請求時使用
var options = [];
// 中介軟體和路由
var stack = self.stack;
// 管理inter-router變數
//req.params 請求引數
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params');
// 設定下一層
req.next = next;
// 對於選項請求,如果沒有其他響應,則使用預設響應
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
// 設定基本的req值
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
next();
function next(err) {
var layerError = err === 'route'
? null
: err;
//是否新增斜線 預設false
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
// 恢復改變req.url
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// 出口路由器訊號
if (layerError === 'router') {
setImmediate(done, null)
return
}
// 不再匹配圖層
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
}
// 獲取路徑pathname
var path = getPathname(req);
if (path == null) {
return done(layerError);
}
// 找到下一個匹配層
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
layer = stack[idx++];
//try layer.match(path) catch err
//搜尋 path matchLayer有兩種狀態一種是boolean,一種是string。
match = matchLayer(layer, path);
route = layer.route;
if (typeof match !== 'boolean') {
layerError = layerError || match;
}
if (match !== true) {
continue;
}
if (!route) {
//正常處理非路由處理程式
continue;
}
if (layerError) {
// routes do not match with a pending error
match = false;
continue;
}
var method = req.method;
var has_method = route._handles_method(method);
// build up automatic options response
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
}
// don't even bother matching route
if (!has_method && method !== 'HEAD') {
match = false;
continue;
}
}
// no match
if (match !== true) {
return done(layerError);
}
//重新賦值router。
if (route) {
req.route = route;
}
// 合併引數
req.params = self.mergeParams
? mergeParams(layer.params, parentParams)
: layer.params;
var layerPath = layer.path;
// 處理引數
self.process_params(layer, paramcalled, req, res, function (err) {
if (err) {
return next(layerError || err);
}
if (route) {
return layer.handle_request(req, res, next);
}
// 處理req.url和layerPath,同時對layer中的請求error和handle_error加tryCatch處理。
trim_prefix(layer, layerError, layerPath, path);
});
}
複製程式碼
執行proto.handle
中介軟體也就的while
迴圈中的一些核心程式碼,每次呼叫app.use
中的回撥函式中的next()
都會讓idx
加一,將stack[idx++];
賦值給layer
,呼叫一開始說到的layer.handle_request
,然後呼叫trim_prefix(layer, layerError, layerPath, path)
,新增一些報錯處理。
trim_prefix
函式如下:
function trim_prefix(layer, layerError, layerPath, path) {
if (layerPath.length !== 0) {
// Validate path breaks on a path separator
var c = path[layerPath.length]
if (c && c !== '/' && c !== '.') return next(layerError)
// //刪除url中與路由匹配的部分
// middleware (.use stuff) needs to have the path stripped
debug('trim prefix (%s) from url %s', layerPath, req.url);
removed = layerPath;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// Ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
// 設定 base URL (no trailing slash)
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
? removed.substring(0, removed.length - 1)
: removed);
}
debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
if (layerError) {
layer.handle_error(layerError, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
};
複製程式碼
總結
以上就是通過app.use
呼叫之後,一步步執行中介軟體函式router.handle
。
next
核心程式碼很簡單,但是需要考慮的場景卻是很多,通過這次原始碼閱讀,更能進一步的理解express的核心功能。
雖說平常做專案用到express
框架很少,或者可以說基本不用,一般都是用Koa
或者Egg
,可以說基本上些規模的場景的專案用的都是Egg
。
但是不可否認得是,express
框架還是一款非常經典的框架。
以上程式碼純屬個人理解,如有不合適的地方,望在評論區留言。