Express 是基於 Nodejs 以及一系列Nodejs第三方package的一款Web開發框架。
Express經歷過2.x,3.x以及最新的4.x版本。Express各個版本的差異還是比較大的。現在2.x版本官方已經不再維護(deprecated),3.x版本雖然可以使用,但是官方建議升級到4.x版本。如果你之前使用的express 3.x版本,官方也有給出遷移到4.x的 指南 。
本文圍繞express的Routing和Middleware機制,作一些討論。主要參考express官網的guide。
安裝和基本示例
我們首先從最基本的講起。首先我們得有一個nodejs項目,
> mkdir test & cd test
> npm init
我一般習慣使用 npm init 來創建一個nodejs項目。因爲它可以提供一個交互式的命令行來生成最基本的 package.json 文件。
在創建好 package.json 文件之後,接下來我們就需要安裝express了,
> npm install express --save
我們使用 –save 選項自動更新 package.json 的 dependencies 字段。在express安裝結束之後, package.json 就變成了下面這樣了,
{
"name": "test",
"version": "1.0.0",
"description": "kick off express4",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"nodejs"
],
"author": "gejiawen",
"license": "MIT",
"dependencies": {
"express": "^4.13.3"
}
}
根據你係統的差異或者網絡因素,你可能需要使用 sudo npm install express –save ,或者是 sudo cnpm install express –save 。
使用npm安裝包的時候,除了 –save 選項,我們還可以使用 –save-dev 。兩者在安裝完包之後,都可以自動更新 package.json 文件,不同的是,前者更新的是 dependencies 字段,後者更新的是 devDependencies 字段。而 dependencies 和 devDependencies 的區別可以簡單的理解成,前者是生產環境所需要的依賴,而後者是開發環境所需要的依賴。
在安裝完畢express之後,我們需要新建一個啓動文件,就是 package.json 中 main 字段指定的文件。當然你說我能不能新建一個文件叫別的什麼名字,那當然也是沒問題的。
> touch index.js
index.js 文件的內容如下,
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.listen(3000, function () {
console.log('app is listening at localhost:3000');
});
最後再啓動這個文件,
> node index.js
至此,我們使用express開發的一個最最簡單的web程序就完成了。可以我瀏覽器輸入 localhost:3000 ,看看效果吧。
工作原理
經過上面的簡單示例,看起來使用express做web真的非常簡單。爲什麼可以做到這麼簡單呢?這是跟express的設計理念有關係的。
整個express應用可以簡單的抽象成一系列的路由和中間件。當然這些路由和中間件之間存在着調用和先後關係。
那麼,路由和中間件具體又是指的是什麼呢?
簡單來說,express中的路由可以看成一種 path watcher ,比如上面示例中的代碼,
app.get('/', function (req, res) {
res.send('Hello world!');
});
這段代碼中,express app將監控 / 路徑,來自客戶端所有對 / 路徑的請求,都使用後面的回調函數處理。
而這裏的處理請求的 回調函數 其實就是所謂的中間件。
所以,中間件其實就是 處理HTTP請求 的回調函數,一般來說它會有3個參數,分別是 req , res 和 next ,分別表示請求對象,響應對象以及next回調。next回調的作用是將控制權傳遞給下一個中間件。
如果一箇中間件是專門用於錯誤處理的,它將會有4個參數,分別是 err , req , res , next 。其中第一個參數表示錯誤對象。
中間件的基本執行過程是這樣的, 在中間件內部對req和res對象進行處理加工,執行相關邏輯,然後根據業務決定是否執行 next() 函數,傳遞到下一個中間件 。
除此之外,一般我們建議一箇中間件只專注做一件事,也就是說中間件的職責儘量單一,這樣利於維護和協調中間件。
中間件機制
下面我們來詳細的說一說express的中間件機制。
其實所謂的中間件就是一個函數回調。express中採用 use 來註冊中間件。比如,
var expresss = require('express');
var app = express();
app.use(function (req, res, next) {
console.log('Time: ', new Date());
next();
});
// more code...
如上例所示,這個中間件是整個app的第一個中間件,當有請求過來時,它將會被第一個調用,在console中打印出時間信息。執行完畢之後,通過 next() 將執行權傳遞給後面的中間件。
此外,我們可以再中間件內容做一些額外的事情,比如對請求路徑的判斷,
app.use(function(req, res, next) {
if (request.url === "/") {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the homepage!\n");
} else {
next();
}
});
app.use(function(req, res, next) {
if (req.url === "/about") {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("about page!\n");
} else {
next();
}
});
app.use(function(req, res, next) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 error!\n");
})
上面的代碼表示,
如果請求的是 / 路徑,返回給客戶端 Welcome to the homepage! 。如果是其他路徑,就調用下一個中間件。
第二個中間件中,判斷路徑是否是 /about ,如果是,將返回給客戶端 about page! 。如果不是,調用下一個中間件。
當執行權傳遞到第三個中間件時,那麼客戶端請求的路徑不是 / 也不是 /about ,此時我們將給出一個 404 error! 的提示,並同時終止中間件的繼續調用。
從上面的示例代碼,我們足以管中窺豹,領略一下express中間件機制,以及它們的調用策略。簡單來說就是, express按照順序執行中間件,每個中間件做好自己的任務並決定是否調用下一個中間件 。
中間件的分類
express的官網上對中間件作了個分類,包含如下幾種,
Application-level middleware (應用級別中間件)
Router-level middleware (路由級別中間件)
Error-handling middleware (錯誤處理中間件)
Built-in middleware (內置中間件)
Third-party middleware (第三方中間件)
應用級別的中間件其實就是指掛在 app 上的中間件。通常我們除了可以通過 app.use 來掛載中間件之外,express還提供了 app.METHOD 這種方式。這裏的 METHOD 指的是http的方法。比如
app.get
app.post
app.put
除此之外,還有一個特別的動詞我們也可以使用,就是 app.all() ,它表示所有的請求都會通過這個中間件。看下面的示例代碼,
app.all("*", function(request, response, next) {
response.writeHead(200, { "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響應的頭信息。如果請求的是 / 或者 /about ,則執行相應的中間件。這裏注意,第二個和第三個中間件都沒有調用 next() ,所以他們執行完畢之後,並不會調用後面的中間件。如果請求的路徑不是 / 或者 /about ,那麼第二個和第三個中間件都將不會被調用,第四個中間件將會被調用。
所以說,express應用中中間件的順序是很重要的,它將影響中間件的執行順序。
Router機制
前面我們知道,可以通過 app.use 或者是 app.METHOD(‘/’) 來配置路由。
在Express4中,路由成了一個單獨的組件,express提供了一個新的對象 express.Router ,可以通過它來更好的配置路由。我們先來看一個示例,
// birds.js
var express = require('express');
var router = express.Router();
router.use(function (req, res, next) {
console.log('Time: ', new Date());
next();
});
router.get('/', function (req, res) {
res.send('Birds home page');
});
router.get('/about', function (req, res) {
res.send('Birds about page');
});
module.exports = router;
// index.js
var express = require('express');
var birds = require('./birds');
var app = express();
app.get('/', function (req, res) {
res.send('app home page');
});
app.use('/birds', birds);
app.listen(3000, function () {
console.log('server starting...');
});
通過這個簡單的例子,我們可以獨立封裝一組路由成爲一個路由中間件,在主文件中可以根據需要掛在到不同的路徑上,如 app.use(‘/birds’, birds) 。這樣一來,路由中間件中對路由的配置最終會被解析成如下,
/birds/
/birds/about/
我們使用express進行web開發的時候,一般也是推薦建立一個routes文件夾,將所有頁面的路徑配置都抽象成一個路由中間件,然後在主文件中掛載。
模式匹配和參數
使用進行路由配置的時候,我們不僅僅可以使用字符常量,還可以使用字符串的模式匹配,如下,
// will match acd and abcd
app.get('/ab?cd', function(req, res) {
res.send('ab?cd');
});
// will match abcd, abbcd, abbbcd, and so on
app.get('/ab+cd', function(req, res) {
res.send('ab+cd');
});
// will match abcd, abxcd, abRABDOMcd, ab123cd, and so on
app.get('/ab*cd', function(req, res) {
res.send('ab*cd');
});
// will match /abe and /abcde
app.get('/ab(cd)?e', function(req, res) {
res.send('ab(cd)?e');
});
// will match anything with an a in the route name:
app.get(/a/, function(req, res) {
res.send('/a/');
});
// will match butterfly, dragonfly; but not butterflyman, dragonfly man, and so on
app.get(/.*fly$/, function(req, res) {
res.send('/.*fly$/');
});
如你所見,路由的模式匹配其實很簡單。你可以使用正則,讓你的路由回調更加隨心所欲的匹配不同的路徑。
路由參數的意思就是你可以在路由配置時定義好路徑中帶有的參數,比如
app.get('/book/:bookId', function (req, res, next) {
console.log(req.params);
});
這樣你就可以在回調中,處理 req 中的 params 對象。
路由回調
有時候我們可能會有這樣一個需求,我需要在路由回調中進行一些預處理。比如下面這個例子
var express = require('express');
var app = express();
app.get('/user/:id', function (req, res, next) {
if (req.params.id === 0) {
next('route');
} else {
next();
}
}, function (req, res, next) {
res.send('regular');
});
app.get('/user/:id', function (req, res, next) {
res.send('special');
});
app.listen(3000);
上面的示例代碼,有兩點需要注意,
1.我們可以在路由配置的回調中,添加多個回調函數。如果有多個回調,我們可以有兩種形式,如下,
app.get('/', function() {}, function() {});
app.get('/', [callback1, callback2, callback3]);
2.同一個路由配置的多個回調函數在執行時可以被跳過。
上面代碼中的 next(‘route’) 表示 跳過 當前路由中間件中剩下的路由回調,執行下一個中間件。而 next() 表示執行當前中間件的下一個路由回調。所以,當我請求 /user/100 時,返回的是regular。當我請求 /user/0 時,返回的是special。
app.router()
在express4中我們可以使用 app.router 對同一個路徑做不同的請求方法配置,如下,
app.route('/book')
.get(function(req, res) {
res.send('Get a random book');
})
.post(function(req, res) {
res.send('Add a book');
})
.put(function(req, res) {
res.send('Update the book');
});
其實這種方式在路由中間件中也是可以用的。針對特定的路由定製場景是特別適用的,可以更好的模塊化、節省一些冗餘的代碼。
模板引擎
之前我們的示例代碼中,都是簡單是使用 res.send 給客戶端發送簡單的文本。如果我們想要客戶端渲染一個自定義的頁面,那該怎麼做呢?
這就需要用到模板了。嚴格來說,express使用的模板屬於服務端模板,因爲它是在服務端解析完畢,發送給客戶端進行渲染的。express支持很多 模板引擎 ,基本上將市面上常見模板引擎一網打盡了。express默認的模板引擎是 jade ,一款非常優雅的模板引擎。後面博主會有文章介紹jade。
言歸正傳,我們在express中如何使用模板引擎來加載模板呢?其實很簡單,三步走即可,
設置好模板的靜態路徑
設置好渲染的模板引擎
在需要的地方渲染你的模板
具體看下面的示例代碼,
// 省略無關代碼
// 設置模板文件夾的路徑
app.set('views', path.join(__dirname, 'views'));
// 設置模板文件的後綴名
app.set('view engine', 'jade');
app.get('/', function (req, res){
res.render('index');
});
app.get('/about', function(req, res) {
res.render('about');
});
app.get('/article', function(req, res) {
res.render('article');
});
// 省略無關代碼
我們在 views 目錄下有3個模板,分別爲 views/index.jade , views/about.jade , views/article.jade ,當請求不同的路徑時,我們會發送不同的模板給前端渲染。
如果你使用的是別的模板引擎,請參閱模板引擎針對express的相關說明。