Express是一基於Node的一個框架,用來快速創建Web服務的一個工具,為什么要使用Express呢,因為創建Web服務如果從Node開始有很多繁瑣的工作要做,而Express為你解放了很多工作,從而讓你更加關注於邏輯業務開發。舉個例子:
創建一個很簡單的網站:
1. 使用Node來開發:
var http = require('http'); var url = require("url"); http.createServer(function(req, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); var url_str = url.parse(req.url,true); res.end('Hello World\n' + url_str.query); }).listen(8080, "127.0.0.1"); console.log('Server running at http://127.0.0.1:8080/');
這是一個簡單的 hello world,運行以后訪問http://127.0.0.1會打印相關字符串,這是最普通的頁面,但實際上真正的網站要比這個復雜很多,主要有:
(1) 多個頁面的路由功能
(2) 對請求的邏輯處理
那么使用node原生寫法就要進行以下處理
// 加載所需模塊 var http = require("http"); // 創建Server var app = http.createServer(function(request, response) { if(request.url == '/'){ response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Home Page!\n"); } else if(request.url == '/about'){ response.writeHead(200, { "Content-Type": "text/plain" }); response.end("About Page!\n"); } else{ response.writeHead(404, { "Content-Type": "text/plain" }); response.end("404 Not Found!\n"); } }); // 啟動Server app.listen(1984, "localhost");
代碼里在createServer函數里傳遞一個回調函數用來處理http請求並返回結果,在這個函數里有兩個工作要做:
(1)路由分析,對於不同的路徑需要進行分別處理
(2)邏輯處理和返回,對某個路徑進行特別的邏輯處理
在這里會有什么問題?如果一個大型網站擁有海量的網站(也就是路徑),每個網頁的處理邏輯也是交錯復雜,那這里的寫法會非常混亂,沒法維護,為了解決這個問題,TJ提出了Connect的概念,把Java里面的中間件概念第一次進入到JS的世界,Web請求將一個一個經過中間件,並通過其中一個中間件返回,大大提高了代碼的可維護性和開發效率。
// 引入connect模塊 var connect = require("connect"); var http = require("http"); // 建立app var app = connect(); // 添加中間件 app.use(function(request, response) { response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello world!\n"); });
// 啟動應用 http.createServer(app).listen(1337);
但是TJ認為還應該更好一點,於是Express誕生了,通過Express開發以上的例子:
2. 使用Express來開發:
var express = require('express'); var app = express(); app.get('/', function (req, res) { res.send('Hello World!'); });
app.get('/about', function (req, res) { res.send('About'); });
var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; console.log('Example app listening at http://%s:%s', host, port); });
從Express例子可以看出,使用Express大大減少了代碼函數,而且邏輯更為簡潔,所以使用Express可以提高開發效率並降低工程維護成本。
首先Express有幾個比較重要的概念:路由,中間件和模版引擎
開發人員可以為Web頁面注冊路由,將不同的路徑請求區分到不同的模塊中去,從而避免了上面例子1所說的海量路徑問題,例如
var express = require("express"); var http = require("http"); var app = express(); app.all("*", function(request, response, next) { response.writeHead(404, { "Content-Type": "text/plain" }); next(); }); app.get("/", function(request, response) { response.end("Welcome to the homepage!"); }); app.get("/about", function(request, response) { response.end("Welcome to the about page!"); }); app.get("*", function(request, response) { response.end("404!"); }); http.createServer(app).listen(1337);
開發人員可以為特定的路由開發中間件模塊,中間件模塊可以復用,從而解決了復雜邏輯的交錯引用問題,例如
var express = require('express'); var app = express(); // 沒有掛載路徑的中間件,應用的每個請求都會執行該中間件 app.use(function (req, res, next) { console.log('Time:', Date.now()); next(); }); // 掛載至 /user/:id 的中間件,任何指向 /user/:id 的請求都會執行它 app.use('/user/:id', function (req, res, next) { console.log('Request Type:', req.method); next(); }); // 路由和句柄函數(中間件系統),處理指向 /user/:id 的 GET 請求 app.get('/user/:id', function (req, res, next) { res.send('USER'); }); var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; console.log('Example app listening at http://%s:%s', host, port); });
同時Express對Request和Response對象進行了增強,添加了很多工具函數。
其中路由和中間件還有很多細節問題,可以參考http://www.expressjs.com.cn/來學習
下面我們來看看Express的工作原理
我們首先來看看Express的源碼結構:
簡單介紹下:
Middleware:中間件
init.js 初始化request,response
query.js 格式化url,將url中的rquest參數剝離, 儲存到req.query中
Router:路由相關
index.js: Router類,用於存儲中間件數組
layer.js 中間件實體類
route.js route類,用於處理不同Method
Application.js 對外API
Express.js 入口
Request.js 請求增強
Response.js 返回增強
Utils.js 工具函數
View.js 模版相關
現在看不明白沒關系,可以先看看后面的解釋然后再回頭看就明白了:
我們前面有說道路由和中間件,那么我們就需要有地方來保存這些信息,比如路由信息,比如中間件回調函數等等,express中有一個對象Router對象專門用來存儲中間件對象,他有一個數組叫stack,保存了所有的中間件對象,而中間件對象是Layer對象。
Router對象就是router/index.js文件,他的代碼是:
Router對象的主要作用就是存儲中間件數組,對請求進行處理等等。
Layer對象在router/layer.js文件中,是保存中間件函數信息的對象,主要屬性有:
源碼見:
這里面的細節先不多考慮,只需要了解關鍵的信息path,handler和route
handler是保存中間件回調函數的地方,path是路由的url,route是一個指針,指向undefined或者一個route對象,為何會有兩種情況呢,是因為中間件有兩種類型:
(1)普通中間件:普通中間件就是不管是什么請求,只要路徑匹配就執行回調函數
(2)路由中間件:路由中間件就是區分了HTTP請求的類型,比如get/post/put/head 等等(有幾十種)類型的中間件,就是說還有區分請求類型才執行。
所以有兩種Layer,一種是普通中間件,保存了name,回調函數已經undefined的route變量。
另外一種是路由中間件,除了保存name,回調函數,route還會創建一個route對象:
route對象在router/route.js文件中,
我們看到route對象有path變量,一個methods對象,也有一個stack數組,stack數組其實保存的也是Layer對象,這個Layer對象保存的是對於不同HTTP方法的不同中間件函數(handler變量)。
也許你會問,這個route的數組里面的Layer和上面router的數組里面的Layer有何不同,他們有一些相同之處也有一些不同之處,主要是因為他們的作用不同:
相同之處:他們都是保存中間件的實例對象,當請求匹配到指定的中間件時,該對象實例將會觸發。
不同之處:
Router對象的Layer對象有route變量,如果為undefined表示為普通中間件,如果指向一個route對象表示為路由中間件,沒有method對象。而route對象的Layer實例是沒有route變量的,有method對象,保存了HTTP請求類型。
所以Router對象中的Layer對象是保存普通中間件的實例或者路由中間件的路由,而route對象中的Layer是保存路由中間件的真正實例。
我們來看個例子,加入有段設置路由器的代碼:
app.use("/index.html",function(){ //此處省略一萬行代碼});
app.use("/contract.html",function(){ //此處省略一萬行代碼});
app.get("/index.html",function(){ //此處省略一萬行代碼});
app.post("/index.html",function(){ //此處省略一萬行代碼});
app.get("/home.html",function(){ //此處省略一萬行代碼});
代碼中注冊了2個普通中間件about.html和contract.html,兩個路由中間件,index.html和home.html,對index.html有get和post兩種中間件函數,對home.html只有get中間件函數,在內存中存儲的形式就是:
我們上面看到了幾種注冊中間件的方式,下面就來介紹下路由器的幾個動作邏輯:
route對象:
router.METHOD(path,callback);//METHOD是HTTP請求方法(get/post等),他的實現過程在這里:
methods變量是一個數組包含了幾十個http請求類型,這段代碼給route對象添加了幾十個方法,主要邏輯就是創建一個Layer對象,保存中間件函數對象和Method方法,添加到route的stack數組中去。
我們再來看看Router對象的方法:
proto.use = function use(fn) { var offset = 0; var path = '/'; // default path to '/' // disambiguate router.use([fn]) if (typeof fn !== 'function') { var arg = fn; while (Array.isArray(arg) && arg.length !== 0) { arg = arg[0]; } // first arg is the path if (typeof arg !== 'function') { offset = 1; path = fn; } } var callbacks = flatten(slice.call(arguments, offset)); if (callbacks.length === 0) { throw new TypeError('Router.use() requires middleware functions'); } for (var i = 0; i < callbacks.length; i++) { var fn = callbacks[i]; if (typeof fn !== 'function') { throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn)); } // add the middleware debug('use %s %s', path, fn.name || '<anonymous>'); var layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, end: false }, fn); layer.route = undefined; this.stack.push(layer); } return this; };
這個就是app.use的實現方法,實際上app.use就是調用了router.use,后面詳細介紹,先看看這個方法做了什么,當我們調用app.use(function(){XXX});的時候,這里的函數首先判斷了參數類型,看看有沒有path傳遞進來,沒有path就是"/"有的話保存到path變量,然后對后面的所有中間件函數進行了以下處理:
創建了一個layer對象,保存了路徑,中間件函數並且設置了route變量為undefined,最后把這個變量保存到router的stack數組中去,到此一個普通中間件函數創建完成,為何要設置route變量為undefined,因為app.use創建的中間件肯定是普通中間件,app.METHOD創建的才是路由中間件。
當我面調用app.get("",function(){XXX})的時候調用的其實是router對象的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方法也創建了一個layer對象,但是因為本身是路由中間件,所以還會創建一個route對象,並且保存到layer的route變量中去。
現在我們總結一下:
1. route對象的主要作用是創建一個路由中間件,並且創建多個方法的layer保存到自己的stack數組中去。
2. router對象的主要作用是創建一個普通中間件或者路由中間件的引導着(這個引導着Layer對象鏈接到一個route對象),然后將其保存到自己的stack數組中去。
所以route對象的stack數組保存的是中間件的方法的信息(get,post等等)而router對象的stack數組保存的是路徑的信息(path)
好了,說完了這些基礎組件,下面說一下真正暴露給開發者的對外接口,很顯然剛才說的都是內部實現細節,我們開發者通常不需要了解這些細節,只需要使用application提供的對外接口。
application在application.js文件下,主要保存了一些配置信息和配置方法,然后是一些對外操作接口,也就是我們說的app.use,app.get/post等等,有幾個重要的方法:
app.use = function use(fn) { var offset = 0; var path = '/'; // default path to '/' // disambiguate app.use([fn]) if (typeof fn !== 'function') { var arg = fn; while (Array.isArray(arg) && arg.length !== 0) { arg = arg[0]; } // first arg is the path if (typeof arg !== 'function') { offset = 1; path = fn; } } var fns = flatten(slice.call(arguments, offset)); if (fns.length === 0) { throw new TypeError('app.use() requires middleware functions'); } // setup router this.lazyrouter(); 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) { req.__proto__ = orig.request; res.__proto__ = orig.response; next(err); }); }); // mounted an app fn.emit('mount', this); }, this); return this; };
我們看到app.use在進行了一系列的參數處理后,最終調用的是router的use方法創建一個普通中間件。
methods.forEach(function(method){ app[method] = function(path){ if (method === 'get' && arguments.length === 1) { // app.get(setting) return this.set(path); } this.lazyrouter(); var route = this._router.route(path); route[method].apply(route, slice.call(arguments, 1)); return this; }; });
同route一樣,將所有的http請求的方法創建成函數添加到application對象中去,從而可以使用app.get/post/等等,最終的效果是調用router的route方法創建一個路由中間件。
所有的方法再通過express入口文件暴露在對外接口中去。而middleware中的兩個文件是對application做的一些初始化操作,request.js和response.js是對請求的兩個對象的一些增強。
到此我們基本了解了express的創建路由和中間件函數的基本原理,下一篇我們來了解如何調用這些路由和中間件函數。