上一篇我們講了如何使用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檔案,內容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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中新增如下的程式碼:
1 2 3 4 5 6 7 8 |
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的第一個引數傳入路徑可以匹配到對應的請求,例如:
1 |
app.use('/api/submitQuestion', function(){}) |
這樣就可以捕獲到剛剛的提交試題的請求,在第二個引數中可以進行相應的處理,比如把資料插入到資料庫。
但是,要注意了,express路由的正確使用姿勢並不是這樣的。app.use是用來匹配中介軟體的路徑的,而不是請求的路徑。因為路由也是一種中介軟體,所以這樣的用法也是能夠完成功能的,但是我們還是應該按照官方標準的寫法來寫。
標準的寫法是什麼樣子呢?程式碼如下:
1 2 3 |
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”下。按照這樣的規則,我們整個專案的路由規則如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//註冊路由 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方法是如何定義的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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進去了,然後就有了之前我們在路由中使用的
1 |
apiRouter.post('/submitQuestion', questionController.save); |
整個流程就串通起來了。
models資料夾中放的就是模型了,用來管理與資料庫的對映和互動,這裡使用了mongoose作為資料庫的操作工具,model層如何來編寫,本篇就不做介紹了,在下一篇中我們再詳細講解。
最後再宣告一下,本篇文章的程式碼是基於一個練習專案QuestionMaker,為了更好理解文章中的敘述,請檢視專案的原始碼:https://github.com/Double-Lv/QuestionMaker