本文出自[Century's World]
不知從什么時候開始,node就開始風靡起來,我們甚至都沒有謹慎的研究過他和其他服務器的區別,便開始躍躍欲試。相信很多人會和筆者一樣,在接觸node服務器的第一時刻,便接觸到了express,我有個朋友也說過,學一個東西還是要結合框架來學比較好,當時我便聽了話,開始了express+node的學習,當然,express非常順手,給了我很好的體驗。而今天,我們撇開浮躁,靜下心來,仔細研究Express框架。
假如沒有Express
說到底Express只是一個框架而已,那么,他是一個什么框架呢,我們撇開Express不談,我們想要完成一個Node的服務,只要如下的代碼就可以完成:
var http = require("http");
var server = http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"});
response.write("Hello World!");
response.end();
});
server.listen(80);
很簡單的幾行代碼就實現了一個服務器,假如我們的需求只是簡單的渲染一個頁面的話,我們大可以用這幾行代碼完成我們想做的事情,其實http這系列的庫做了很多事情,非常建議大家回頭去看看這一系列Express底層的Api。當然了最后我們還是會用框架的,自己整理的也好,用現在流行的Expres或者Koa也好,目的是為了應對復雜的使用場景,減少重復繁瑣的代碼。
小小的實現一下
說起Express的特點,大概就是中間件吧,所有的東西都是通過中間件來完成的,那么中間件實現是怎么樣的呢,假如我們自己實現一個Express,我們就應該先解決中間件的問題,那讓我們來嘗試一下實現一個簡單的Express。
首先我們抽離createServer
的參數
const app = function(req, res) {
//TODO someting
}
var server = http.createServer(app);
server.listen(port)
這樣我們邏輯處理就放到了app里面,有的時候一次運行可能產生多個app的實例,為了分隔環境,我們可以用一個工廠方法或者構造行數生成app
const express = function() {
return function(req, res) {
//TODO someting
}
}
const app = express();
var server = http.createServer(app)
server.listen(port)
接下來我們先實現app.use
function Middleware = function(path, fn) {
if (!(this instanceof Middleware)) {
return new Middleware(...arguments);
}
this.path = path;
this.fn = fn;
}
app.use = function(path, ...fns) {
if (arguments.length == 1) {
fns = path;
path = '/';
}
for(var index in fns) {
var fn = fns[index]
const middleware = Middleware(path, fn);
this.middlewares.push(middleware);
}
}
我們定義了一個Middleware的對象,其中有個很多框架中常用的hack,也就是在一個class里面判斷是否是使用new來創建對象的,因為使用new來創建對象的時候this一定是當前類的實例,所以我們可以根據this的類型來重定向一個new Function
。Middleware用于封裝一個中間件層,用于綁定中間件和對應的path。在app.use
里面我們將中間件都保存在app
的middlewares
屬性中,那么接下來,我們就要實現這個中間件的處理過程。
首先我們要增加一個handler的function
function mathMiddleware(url, middleware) {
// TODO match middleware
}
app.handler = function(req, res) {
var url = req.url;
var middlewares = app.middlewares;
var idx = 0;
var match = false;
var middleware;
function done() {
//完成這次請求, 比如有error的情況
}
function next (err) {
if (err) {
done(err)
}
while(match === false && idx < middlewares.length) {
middleware = middlewares[idx];
match = mathMiddleware(url, middleware)
}
middleware.handle(req, res, next);
}
}
在Middleware中加上一個handle
Middleware.prototype.handle = function(req, res, next) {
try {
this.fn(req, res, next);
} catch(e) {
next(e)
}
}
這只是簡單的實現了一下Express的中間件的邏輯,這也大致是Express的實現邏輯,我們知道了這種中間件的實現方式,那么今后在我們的應用中,對某一塊邏輯要使用策略模式、裝飾著模式或者工廠模式的時候,我們也可以用一個這樣的中間件的策略去切割代碼,讓邏輯的處理變的非常簡單而清晰。
別YY了,看一下官方實現
官方的代碼其實寫的非常易懂,總的來說,給我的感覺,express就是一個微型的框架。
結構
├── application.js
├── express.js
├── middleware
│ ├── init.js
│ └── query.js
├── request.js
├── response.js
├── router
│ ├── index.js
│ ├── layer.js
│ └── route.js
├── utils.js
└── view.js
express.js
這是整個應用的入口,主要的工作的整合了application.js
中的關于app的屬性,構造了一個函數來輸出app,將一些輔助函數輸出,比如Router
、Request
等等
router
這個類我們應該不陌生,我們在使用express的時候經常使用
var express = require('express');
var router = express.Router();
這個router其實是整個app的核心,包括app.use的中間件實現也是通過Router實現的,以下是源碼加上注釋
proto.use = function use(fn) {
var offset = 0; //用于計算參數的個數來獲取所有的中間件
var path = '/'; //設置默認的path為 /
if (typeof fn !== 'function') { // use('/', fn)的情況
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
//通過offset獲取所有的中間件
var callbacks = flatten(slice.call(arguments, offset));
//當中間件的個數為零的時候,拋出異常
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}
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))
}
debug('use %o %s', path, fn.name || '<anonymous>')
// 創建一個Layer,類似于之前自己實現的Middleware
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer); //將這個layer加入到棧中
}
return this; //返回自己用于鏈式調用
};
之后會講到這個Layer類,這是一個中間件的承載物,所有的中間件的處理方法和路徑的綁定信息都被封裝在這個類的實例中,而Router的use方法即使將這些中間件打包成為一個Layer,然后存儲到自己的stack中用于之后使用。
然后是一個Router中的request處理類,用于使用中間件處理請求。
proto.handle = function handle(req, res, out) {
var self = this; //用self指向老的this
var idx = 0; // 迭代stack的迭代器
var protohost = getProtohost(req.url) || '' //獲取協議名
var removed = ''; // 定義刪除字段
var slashAdded = false;
var paramcalled = {};
var options = [];
var stack = self.stack; // layer 的集合
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params');
req.next = next;
if (req.method === 'OPTIONS') { // 處理Options的請求,跨域問題
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
next();
// 中間件的迭代方法
function next(err) {
var layerError = err === 'route' ? null : err;
//計算req.url, req.baseUrl
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
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;
}
var path = getPathname(req); //獲取當前path
if (path == null) {
return done(layerError);
}
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
// TODO: 循環獲取匹配的Layer
}
// 沒有匹配的
if (match !== true) {
return done(layerError);
}
//TODO: 根據layer注入req.params
//TODO: 根據需要調用layer.handle_request或者layer.handle_error
}
};
當然了我省略了一部分代碼,給大家大致的介紹了一下router是如何處理中間件的,處理中間件的關鍵就這么兩個函數,一個是use
,一個是handle
,handle
用于以中間件的形式迭代處理請求,use
用于注冊中間件
application.js
這是一個app的類,定義了app的屬性和函數,那么application又和router有什么聯系呢,我們從api中可以發現,幾乎router有的函數,在app中都可以使用,比如router.use和app.use,router.get和app.get,那么官方的實現中,是怎么實現的呢,是否是簡單的繼承呢。我們帶著疑問去觀察這個類,我們會發現application中有個router的屬性,在中間件的表現上,application只是一個傀儡,大部分的實現都還是依靠router的,application的中間件的操作都是交由其router來處理的,也就是說app.use()是約等于app.router.use()的。比如:
app.param = function param(name, fn) {
this.lazyrouter();
if (Array.isArray(name)) {
for (var i = 0; i < name.length; i++) {
this.param(name[i], fn);
}
return this;
}
this._router.param(name, fn);
return this;
};
話雖這么說,但是app的router是懶加載的,當調用use之類的函數的時候會判斷當前是否已經創建了router,否則會創建一個router。除此之外,還會對參數做一些校驗和轉換,因此還是推薦不直接使用app.router的方式的。
middleware
(有點無聊)這個middleware目錄下只是express內置的兩個中間件,一個query是用于在req.query注入url中的query參數的,init是一個初始化的中間件,它把req、res相互引用了一些,并mixin了一些req, res的屬性,還有x-powered-by額?默認給express打個廣告?
Layer
(有那么點意思)這是一個藏在Router下的對象,用于包裝中間件和對應的path。
request&response
(比較無聊)這兩個類里面主要是一些api這個和express的核心部分的關系沒有那么大,他的主要工作主要集中在封裝了一些工具方法,一個方便開發者使用的req, res的屬性集合的對象。
看源碼有什么用?
總的來說,這次的Express源碼之旅是很有幫助的,這是我開始這個源碼計劃的第一個項目,選擇Express的原因是這個框架的代碼確實看起來比較簡單,不需要編譯,其次Express還是我現在用的最多的node服務框架,當然之后會考慮使用koa,所以之后很有可能會帶來koa的源碼解讀。作為一個node服務的框架,這次源碼閱讀讓我更加了解Node的http這個模塊的東西,有很多的基礎的模塊在jshttp中,閱讀它們會讓我更加理解一些關于http的問題,查看了整個中間件的實現,讓我對這種模式豁然開朗,之后希望能夠在項目中靈活的運用。對request和response的閱讀讓我知道了很多之前看文檔沒有仔細觀察到的api。