上一篇我們講了如何使用angular搭建起項目的前端框架,前端抽象出一個service層來向后端發送請求,后端則返回相應的json數據。本篇我們來介紹一下,如何在nodejs環境下利用express來搭建起服務端,使之正確的響應前端的請求。本文所講的示例還是基於我們的學習項目QuestionMaker(https://github.com/Double-Lv/QuestionMaker)
運行起基於express的web服務器
express是一個web應用開發框架,它基於nodejs,擴展了很多web開發所需的功能,使得我們能夠很方便的訪問和操作request和response。請注意它和nginx或者tomcat並不是一個概念,它是一個開發框架,而不是服務器。
運行起基於express的web服務器是非常簡單的,因為express都綁你封裝好了。首先需要用npm安裝好express,然后在項目根目錄下新建一個server.js文件,內容如下:
var express = require('express'); var app = express(); app.listen(3000); var _rootDir = __dirname; var protectDir = _rootDir + '/protect/'; app.use(express.static(_rootDir)); //注冊路由 app.get('/', function(req, res){ res.sendFile(_rootDir+'/src/index.html'); }); app.use(function(req, res, next) { res.status(404).sendFile(_rootDir+'/src/404.html'); }); app.use(function(err, req, res, next) { console.error(err.stack); res.status(500).send('500 Error'); });
上述代碼實現了這幾個功能,首先創建了http服務器,監聽在3000端口。
然后app.use(express.static(_rootDir));這一行是使用了靜態文件服務的中間件,這樣我們項目下的js、css以及圖片等靜態文件就都可以訪問到了。
接下來是注冊路由,此處只匹配一個路由規則,那就是"/"(網站的根目錄),當匹配到此路由后把首頁文件index.html直接用res.sendFile方法給發送到瀏覽器端。這樣瀏覽器用http://127.0.0.1:3001就可以訪問到index.html了。網站的其他頁面也可以通過配置類似的路由進行返回。express還支持配置模板引擎,默認支持ejs,你也可以自己配置其他的比如handlebars。
但是在本項目中,我們用的是angular的前端模板,所以后端就不需要模板了,沒有進行配置。我們的路由機制也是完全使用的ng的前端路由,所以在express中只配置一條就夠了。
在最后還有兩塊代碼,分別是404和500錯誤的捕獲。你可能會疑惑為什么是這樣寫呢?從上到下排下來就能分別捕獲404和500了嗎?其實這就是express的中間件機制,在此機制下,對客戶端請求的處理像是一個流水線,把所有中間件串聯起來,只要某個中間件把請求返回了,就結束執行,否則就從上到下一直處理此請求。
上面代碼的流程就是,先按路由規則來匹配路徑,如果路由匹配不到,則認為是發生404。500的錯誤請注意一個細節,在回調函數的參數中,第一個會傳入err,就是錯誤對象,以此來標記是一個500錯誤。
理解中間件
express的核心是中間件機制,通過使用各種中間件,能夠實現靈活的組裝我們所需的功能。中間件是在管道中執行的,所謂管道就是像流水線一樣,每到達一個加工區,相應的中間件就可以處理request和response對象,處理完后再送往下一個加工區。如果某個加工區把請求終結了,比如調用send方法返回給了客戶端,那么處理就終止了。大部分情況下,都有現成的中間件供我們使用,比如用body-parser解析請求實體,用路由(路由也是一種中間件)來正確的派發請求。
比如我們在server.js中添加如下的代碼:
app.use(function(req, res, next){ console.log('中間件1'); next(); }); app.use(function(req, res, next){ console.log('中間件2'); });
我們添加了兩個中間件,請求過來之后會先被第一個捕獲,然后進行處理,輸出“中間件1”。后面接着執行了next()方法,就會進入下一個中間件。一個中間件執行后只有兩種選擇,要么用next指向下一個中間件,要么將請求返回。如果什么都不做,請求將會被掛起,也就是說瀏覽器端將得不到返回,一直處於pendding狀態。例如上面的中間件2,將會造成請求掛起,這是應該杜絕的。
路由設計
運行起了服務器,了解了中間件編程方式,接下來我們就該為前端提供api了。比如前端post一個請求到/api/submitQuestion來提交一份數據,我們該如何接收請求並做出處理呢,這就是路由的設計了。
給app.use的第一個參數傳入路徑可以匹配到對應的請求,例如:
app.use('/api/submitQuestion', function(){})
這樣就可以捕獲到剛剛的提交試題的請求,在第二個參數中可以進行相應的處理,比如把數據插入到數據庫。
但是,要注意了,express路由的正確使用姿勢並不是這樣的。app.use是用來匹配中間件的路徑的,而不是請求的路徑。因為路由也是一種中間件,所以這樣的用法也是能夠完成功能的,但是我們還是應該按照官方標准的寫法來寫。
標准的寫法是什么樣子呢?代碼如下:
var apiRouter = express.Router(); apiRouter.post('/submitQuestion', questionController.save); app.use('/api', apiRouter);
我們利用的是express.Router這個對象,它同樣有use、post、get等方法,用來匹配請求路徑。然后我們再使用app.use把apiRouter作為第二個參數傳進去。
要注意的是apiRouter.post和app.use的第一個參數。app.use匹配的是請求的“根路徑”,這樣可以把請求分為不同的類別,比如所有的異步接口我們都叫api,那么這類請求我們就都應該掛在“/api”下。按照這樣的規則,我們整個項目的路由規則如下:
//注冊路由 app.get('/', function(req, res){ res.sendFile(_rootDir+'/src/index.html'); }); var apiRouter = express.Router(); apiRouter.post('/getQuestion', questionController.getQuestion); apiRouter.post('/getQuestions', questionController.getQuestions); apiRouter.post('/submitQuestion', questionController.save); apiRouter.post('/updateQuestion', questionController.update); apiRouter.post('/removeQuestion', questionController.remove); apiRouter.post('/getPapers', paperController.getPapers); apiRouter.post('/getPaper', paperController.getPaper); apiRouter.post('/getPaperQuestions', paperController.getPaperQuestions); apiRouter.post('/submitPaper', paperController.save); apiRouter.post('/updatePaper', paperController.update); apiRouter.post('/removePaper', paperController.remove); app.use('/api', apiRouter);
在router的第二個參數中,我們傳入了questionController.save這樣的方法,這是什么東西呢?怎么有點MVC的味道呢?沒錯,我們已經能夠匹配到路由了,那服務端的業務邏輯以及數據庫訪問等該如何組織代碼呢?
用“MVC”組織代碼
用MVC的結構組織代碼當然是黃金法則了。express可以用模板引擎來渲染view層,路由機制來組織controller層,但是express並沒有明確規定MVC結構應該怎樣寫,而是把自由選擇交給你,自己來組織MVC結構。當然你也可以組織別的形式,比如像Java中的“n層架構”。
在本項目中,我們就以文件夾的形式來簡單組織一下。因為我們使用了前端模板,所以后端的view層就不存在了,只有controller和model。看一下項目的目錄:

在protect下有兩個文件夾controllers和models分別放C和M。我們路由中使用的questionController對象就定義在questionController.js中,來看一下用於保存試題的save方法是如何定義的:
var Question = require('../models/question'); module.exports = { //添加試題 save: function(req, res){ var data = req.body.question; Question.save(data, function(err, data){ if(err){ res.send({success: false, error: err}); } else{ res.send({success: true, data: data}); } }); } }
questionController作為一個模塊,使用標准的commonjs語法,我們定義了save方法,通過req.body.question,可以拿到前台傳過來的數據。在這個模塊中,我們require了位於model層的Question模型,沒錯,它就是用來操作數據庫的,調用Question.save方法,這份數據就存入了數據庫,然后在回調函數中,我們用res.send將json數據返回給前端。
定義好questionController后,我們就可以在server.js中把它給require進去了,然后就有了之前我們在路由中使用的
apiRouter.post('/submitQuestion', questionController.save);
整個流程就串通起來了。
models文件夾中放的就是模型了,用來管理與數據庫的映射和交互,這里使用了mongoose作為數據庫的操作工具,model層如何來編寫,本篇就不做介紹了,在下一篇中我們再詳細講解。
最后再聲明一下,本篇文章的代碼是基於一個練習項目QuestionMaker,為了更好理解文章中的敘述,請查看項目的源碼:
https://github.com/Double-Lv/QuestionMaker