Express是基于Nodejs以及一系列Nodejs第三方package的一款Web開發(fā)框架。
Express經(jīng)歷過2.x,3.x以及最新的4.x版本。Express各個(gè)版本的差異還是比較大的?,F(xiàn)在2.x版本官方已經(jīng)不再維護(hù)(deprecated),3.x版本雖然可以使用,但是官方建議升級(jí)到4.x版本。如果你之前使用的express 3.x版本,官方也有給出遷移到4.x的指南。
本文圍繞express的Routing和Middleware機(jī)制,作一些討論。主要參考express官網(wǎng)的guide。
我們首先從最基本的講起。首先我們得有一個(gè)nodejs項(xiàng)目,
> mkdir test & cd test
> npm init
我一般習(xí)慣使用npm init
來創(chuàng)建一個(gè)nodejs項(xiàng)目。因?yàn)樗梢蕴峁┮粋€(gè)交互式的命令行來生成最基本的package.json
文件。
在創(chuàng)建好package.json
文件之后,接下來我們就需要安裝express了,
> npm install express --save
我們使用--save
選項(xiàng)自動(dòng)更新package.json
的dependencies
字段。在express安裝結(jié)束之后,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"
}
}
根據(jù)你系統(tǒng)的差異或者網(wǎng)絡(luò)因素,你可能需要使用sudo npm install express --save
,或者是sudo cnpm install express --save
。
使用npm安裝包的時(shí)候,除了--save
選項(xiàng),我們還可以使用--save-dev
。兩者在安裝完包之后,都可以自動(dòng)更新package.json
文件,不同的是,前者更新的是dependencies
字段,后者更新的是devDependencies
字段。而dependencies
和devDependencies
的區(qū)別可以簡單的理解成,前者是生產(chǎn)環(huán)境所需要的依賴,而后者是開發(fā)環(huán)境所需要的依賴。
在安裝完畢express之后,我們需要新建一個(gè)啟動(dòng)文件,就是package.json
中main
字段指定的文件。當(dāng)然你說我能不能新建一個(gè)文件叫別的什么名字,那當(dāng)然也是沒問題的。
> touch index.js
index.js
文件的內(nèi)容如下,
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');
});
最后再啟動(dòng)這個(gè)文件,
> node index.js
至此,我們使用express開發(fā)的一個(gè)最最簡單的web程序就完成了。可以我瀏覽器輸入localhost:3000
,看看效果吧。
經(jīng)過上面的簡單示例,看起來使用express做web真的非常簡單。為什么可以做到這么簡單呢?這是跟express的設(shè)計(jì)理念有關(guān)系的。
整個(gè)express應(yīng)用可以簡單的抽象成一系列的路由和中間件。當(dāng)然這些路由和中間件之間存在著調(diào)用和先后關(guān)系。
那么,路由和中間件具體又是指的是什么呢?
簡單來說,express中的路由可以看成一種path watcher,比如上面示例中的代碼,
app.get('/', function (req, res) {
res.send('Hello world!');
});
這段代碼中,express app將監(jiān)控/
路徑,來自客戶端所有對/
路徑的請求,都使用后面的回調(diào)函數(shù)處理。
而這里的處理請求的回調(diào)函數(shù)其實(shí)就是所謂的中間件。
所以,中間件其實(shí)就是處理HTTP請求的回調(diào)函數(shù),一般來說它會(huì)有3個(gè)參數(shù),分別是req
,res
和next
,分別表示請求對象,響應(yīng)對象以及next回調(diào)。next回調(diào)的作用是將控制權(quán)傳遞給下一個(gè)中間件。
如果一個(gè)中間件是專門用于錯(cuò)誤處理的,它將會(huì)有4個(gè)參數(shù),分別是err
,req
,res
,next
。其中第一個(gè)參數(shù)表示錯(cuò)誤對象。
中間件的基本執(zhí)行過程是這樣的,在中間件內(nèi)部對req和res對象進(jìn)行處理加工,執(zhí)行相關(guān)邏輯,然后根據(jù)業(yè)務(wù)決定是否執(zhí)行next()
函數(shù),傳遞到下一個(gè)中間件。
除此之外,一般我們建議一個(gè)中間件只專注做一件事,也就是說中間件的職責(zé)盡量單一,這樣利于維護(hù)和協(xié)調(diào)中間件。
下面我們來詳細(xì)的說一說express的中間件機(jī)制。
其實(shí)所謂的中間件就是一個(gè)函數(shù)回調(diào)。express中采用use
來注冊中間件。比如,
var expresss = require('express');
var app = express();
app.use(function (req, res, next) {
console.log('Time: ', new Date());
next();
});
// more code...
如上例所示,這個(gè)中間件是整個(gè)app的第一個(gè)中間件,當(dāng)有請求過來時(shí),它將會(huì)被第一個(gè)調(diào)用,在console中打印出時(shí)間信息。執(zhí)行完畢之后,通過next()
將執(zhí)行權(quán)傳遞給后面的中間件。
此外,我們可以再中間件內(nèi)容做一些額外的事情,比如對請求路徑的判斷,
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!
。如果是其他路徑,就調(diào)用下一個(gè)中間件。/about
,如果是,將返回給客戶端about page!
。如果不是,調(diào)用下一個(gè)中間件。/
也不是/about
,此時(shí)我們將給出一個(gè)404 error!
的提示,并同時(shí)終止中間件的繼續(xù)調(diào)用。從上面的示例代碼,我們足以管中窺豹,領(lǐng)略一下express中間件機(jī)制,以及它們的調(diào)用策略。簡單來說就是,express按照順序執(zhí)行中間件,每個(gè)中間件做好自己的任務(wù)并決定是否調(diào)用下一個(gè)中間件。
express的官網(wǎng)上對中間件作了個(gè)分類,包含如下幾種,
應(yīng)用級(jí)別的中間件其實(shí)就是指掛在app
上的中間件。通常我們除了可以通過app.use
來掛載中間件之外,express還提供了app.METHOD
這種方式。這里的METHOD
指的是http的方法。比如
app.get
app.post
app.put
除此之外,還有一個(gè)特別的動(dòng)詞我們也可以使用,就是app.all()
,它表示所有的請求都會(huì)通過這個(gè)中間件??聪旅娴氖纠a,
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!");
});
所有的請求都將會(huì)執(zhí)行第一個(gè)中間,用戶設(shè)置http響應(yīng)的頭信息。如果請求的是/
或者/about
,則執(zhí)行相應(yīng)的中間件。這里注意,第二個(gè)和第三個(gè)中間件都沒有調(diào)用next()
,所以他們執(zhí)行完畢之后,并不會(huì)調(diào)用后面的中間件。如果請求的路徑不是/
或者/about
,那么第二個(gè)和第三個(gè)中間件都將不會(huì)被調(diào)用,第四個(gè)中間件將會(huì)被調(diào)用。
所以說,express應(yīng)用中中間件的順序是很重要的,它將影響中間件的執(zhí)行順序。
前面我們知道,可以通過app.use
或者是app.METHOD('/')
來配置路由。
在Express4中,路由成了一個(gè)單獨(dú)的組件,express提供了一個(gè)新的對象express.Router
,可以通過它來更好的配置路由。我們先來看一個(gè)示例,
// 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...');
});
通過這個(gè)簡單的例子,我們可以獨(dú)立封裝一組路由成為一個(gè)路由中間件,在主文件中可以根據(jù)需要掛在到不同的路徑上,如app.use('/birds', birds)
。這樣一來,路由中間件中對路由的配置最終會(huì)被解析成如下,
/birds/
/birds/about/
我們使用express進(jìn)行web開發(fā)的時(shí)候,一般也是推薦建立一個(gè)routes文件夾,將所有頁面的路徑配置都抽象成一個(gè)路由中間件,然后在主文件中掛載。
使用進(jìn)行路由配置的時(shí)候,我們不僅僅可以使用字符常量,還可以使用字符串的模式匹配,如下,
// 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$/');
});
如你所見,路由的模式匹配其實(shí)很簡單。你可以使用正則,讓你的路由回調(diào)更加隨心所欲的匹配不同的路徑。
路由參數(shù)的意思就是你可以在路由配置時(shí)定義好路徑中帶有的參數(shù),比如
app.get('/book/:bookId', function (req, res, next) {
console.log(req.params);
});
這樣你就可以在回調(diào)中,處理req
中的params
對象。
有時(shí)候我們可能會(huì)有這樣一個(gè)需求,我需要在路由回調(diào)中進(jìn)行一些預(yù)處理。比如下面這個(gè)例子
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);
上面的示例代碼,有兩點(diǎn)需要注意,
1.我們可以在路由配置的回調(diào)中,添加多個(gè)回調(diào)函數(shù)。如果有多個(gè)回調(diào),我們可以有兩種形式,如下,
app.get('/', function() {}, function() {});
app.get('/', [callback1, callback2, callback3]);
2.同一個(gè)路由配置的多個(gè)回調(diào)函數(shù)在執(zhí)行時(shí)可以被跳過。
上面代碼中的next('route')
表示跳過當(dāng)前路由中間件中剩下的路由回調(diào),執(zhí)行下一個(gè)中間件。而next()
表示執(zhí)行當(dāng)前中間件的下一個(gè)路由回調(diào)。所以,當(dāng)我請求/user/100
時(shí),返回的是regular。當(dāng)我請求/user/0
時(shí),返回的是special。
app.router()
在express4中我們可以使用app.router
對同一個(gè)路徑做不同的請求方法配置,如下,
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');
});
其實(shí)這種方式在路由中間件中也是可以用的。針對特定的路由定制場景是特別適用的,可以更好的模塊化、節(jié)省一些冗余的代碼。
之前我們的示例代碼中,都是簡單是使用res.send
給客戶端發(fā)送簡單的文本。如果我們想要客戶端渲染一個(gè)自定義的頁面,那該怎么做呢?
這就需要用到模板了。嚴(yán)格來說,express使用的模板屬于服務(wù)端模板,因?yàn)樗窃诜?wù)端解析完畢,發(fā)送給客戶端進(jìn)行渲染的。express支持很多模板引擎,基本上將市面上常見模板引擎一網(wǎng)打盡了。express默認(rèn)的模板引擎是jade,一款非常優(yōu)雅的模板引擎。后面博主會(huì)有文章介紹jade。
言歸正傳,我們在express中如何使用模板引擎來加載模板呢?其實(shí)很簡單,三步走即可,
具體看下面的示例代碼,
// 省略無關(guān)代碼
// 設(shè)置模板文件夾的路徑
app.set('views', path.join(__dirname, 'views'));
// 設(shè)置模板文件的后綴名
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');
});
// 省略無關(guān)代碼
我們在views
目錄下有3個(gè)模板,分別為views/index.jade
,views/about.jade
,views/article.jade
,當(dāng)請求不同的路徑時(shí),我們會(huì)發(fā)送不同的模板給前端渲染。
如果你使用的是別的模板引擎,請參閱模板引擎針對express的相關(guān)說明。
本文由于篇幅原因,不會(huì)關(guān)注express的方方面面,更多的內(nèi)容請參考官網(wǎng)的API Reference
更多建議: