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