因爲NodeJs自己的異步非阻塞特性和對http的自然支持,因此使用NodeJs編寫高性能,可伸縮的Web服務器很是簡單。開發完整的Web服務器還須要路由,錯誤處理,請求攔截,請求和響應的解析,模板引擎等功能,因此直接使用NodeJs的http模塊開發起來仍是挺痛苦的。html
目前有不少的Web框架都是基於http模塊封裝而成,最流行的當屬Express框架。前端
學習資源:node
npm install express --save
快速開啓服務器:git
const express = require('express'); const app = express(); app.get('/', function(req, res){ res.send('hello world'); }); app.listen(3000);
express提供了託管靜態文件的功能,好比html,圖片,CSS,JavaScript等文件。github
app.use(express.static('public'));
支持指定掛載路徑:web
app.use('/static', express.static('public'));
通常框架提供的靜態文件託管功能,只是在開發環境使用。在生產環境,推薦交給反向代理服務器來管理,好比Nginx;最佳的作法是花點錢交給CDN提供商來服務。mongodb
路由就是根據用戶請求的url路徑,交由對應的響應方法處理。數據庫
app.get('/', function (req, res) { res.send('GET request to the homepage'); }); // POST method route app.post('/', function (req, res) { res.send('POST request to the homepage'); });
// 匹配根路徑 app.get('/', function (req, res) { res.send('root'); }); // 匹配 /user 路徑的請求 app.get('/user', function (req, res) { res.send('user'); }); // 匹配 /xxx 路徑的請求 app.get('/xxx', function (req, res) { res.send('xxx'); });
支持簡單的字符串匹配:express
// 匹配 acd 和 abcd app.get('/ab?cd', function(req, res) { res.send('ab?cd'); }); // 匹配 abcd、abbcd、abbbcd等 app.get('/ab+cd', function(req, res) { res.send('ab+cd'); }); // 匹配 abcd、abxcd、abRABDOMcd、ab123cd等 app.get('/ab*cd', function(req, res) { res.send('ab*cd'); });
也支持徹底的正則方式,可是不能用字符串了。npm
// 匹配以abc開頭的全部字符 app.get(/abc[0-9a-zA-Z]*/, function(req, res) { res.send('ab?cd'); });
模塊化路由是指咱們能夠將一組具備業務相關性的路由封裝爲一個router對象,而後掛載到指定路徑。
const express = require('express'); const router = express.Router(); // 該路由使用的中間件 router.use(function timeLog(req, res, next) { console.log('Time: ', Date.now()); next(); }); // 定義模塊的根路由 router.get('/', function(req, res) { res.send('auth模塊根路由'); }); // 定義登陸 router.get('/login', function(req, res) { res.send('login'); }); module.exports = router;
將router掛載到指定路徑:
const auth = require('./auth'); ... app.use('/auth', auth);
中間件就是整個請求——>響應流程中的一系列處理函數。路由也是屬於中間件,是掛載了路徑的中間件。
使用中間件有兩點注意:
const app = express(); // 沒有掛載路徑的中間件,應用的每一個請求都會執行該中間件 app.use(function (req, res, next) { console.log('Time:', Date.now()); next(); }); // 掛載至 /user/:id 的中間件,任何指向 /user/:id 的請求都會執行它 app.use('/user/:id', function (req, res, next) { console.log('Request Type:', req.method); next(); });
路由級中間件和應用級中間件同樣,只是它綁定的對象爲 express.Router()
。
const router = express.Router(); // 沒有掛載路徑的中間件,經過該路由的每一個請求都會執行該中間件 router.use(function (req, res, next) { console.log('Time:', Date.now()); next(); }); // 一箇中間件棧,顯示任何指向 /user/:id 的 HTTP 請求的信息 router.use('/user/:id', function(req, res, next) { console.log('Request URL:', req.originalUrl); next(); }, function (req, res, next) { console.log('Request Type:', req.method); next(); }); // 一箇中間件棧,處理指向 /user/:id 的 GET 請求 router.get('/user/:id', function (req, res, next) { // 若是 user id 爲 0, 跳到下一個路由 if (req.params.id == 0) next('route'); // 負責將控制權交給棧中下一個中間件 else next(); // }, function (req, res, next) { // 渲染常規頁面 res.render('regular'); }); module.exports = router;
掛載router
const app = express(); app.use('/', router);
程序總會遇到錯誤,好比:傳參錯誤,咱們不當心調用了undifined
,或者咱們本身也可能拋出異常。當發生這些錯誤的時候咱們須要給用戶一個友好的迴應,這就是錯誤處理。
定義錯誤處理中間件和定義其餘中間件同樣,除了須要 4 個參數,而不是 3 個,其格式以下 (err, req, res, next)
。例如:
app.use(function(err, req, res, next) { console.error(err.stack); res.status(500).send('出錯了!'); });
錯誤中間件返回的響應是隨意的,能夠響應一個 HTML 錯誤頁面、一句簡單的話、一個 JSON 字符串,或者其餘任何您想要的東西。
在其餘 app.use()
和路由調用後,最後定義錯誤處理中間件,好比:
app.use(bodyParser()); app.use(router); app.use(function(err, req, res, next) { // ... });
Html頁面自己是靜態的,不含動態數據的。模板引擎的做用就是將從數據庫讀出來的,處理好的數據填充到html頁面,而後返回給用戶。
express支持不少的模板引擎,每一個模板引擎都有本身的語法。這裏以jade爲例。
須要安裝相應的模板引擎 npm 軟件包。
npm install jade --save
設置模板的目錄,和當前使用的模板引擎
app.set('views', './views') //設置模板存放的目錄 app.set('view engine', 'jade'); //設置使用的引擎
在 views
目錄下生成名爲 index.jade
的 Jade 模板文件,內容以下:
html head title=title body h1=message
而後建立一個路由渲染 index.jade
文件。
app.get('/', function (req, res) { res.render('index', { title: 'Hey', message: 'Hello there!'}); });
index.jade
文件會被引擎渲染爲html文件,而後發送給用戶。jade引擎的更多使用請參考它的官方文檔,這裏只作簡單演示,緣由以下:
現代Web開發的模式是逐漸趨向於先後端分離,前端的人負責寫Html頁面,後臺的人寫API服務,前端數據經過AJAX獲得。這樣的好處是開發效率高,溝通成本下降。加上Vue和React等具備高性能和高效率的前端渲染引擎的出現,後端的模板引擎的幾乎沒有用武之地了。但不排除某些公司的技術人員仍然頑固地選擇後端渲染。
在生產環境咱們不會使用node app.js
的方式來開啓程序,由於咱們有這樣的一些需求:
進程管理器就能提供上面的功能,讓咱們的NodeJs進程永不退出。最流行的進程管理器有3個:
其中PM2除了提供上面的功能外,還能夠監視程序的日誌,錯誤提醒,而且內置了負載均衡器,界面還友好。因此咱們選擇使用PM2。
首先,安裝。
npm install pm2 -g
啓動程序:
pm2 start app.js
其餘經常使用命令:
pm2 list pm2 stop app pm2 reload app pm2 start xxx.js -i 4
第三方中間件:http://www.expressjs.com.cn/resources/middleware.html
API查詢:http://www.expressjs.com.cn/4x/api.html
完成對TODO事項的增刪改查功能,數據庫使用mongodb。
建立項目,新建package.json
文件。
npm i express -S npm i mongoose -S npm i morgan -S // 打印log的類庫 npm i express-async-errors -S // 用來捕獲promise錯誤
因爲express 4x
爲了精簡框架,移除了不少內置的中間件,好比body-parser
。因此爲了可以解析body參數,須要額外安裝這個模塊。
npm i body-parser -S
整個項目結構,仍然是基於MVC架構來分層。
service包:存放全部的業務邏輯類,至關於service層
config.js
:配置文件db.js
:數據庫入口文件app.js
:app入口文件
依次編寫:
app.js
和 config.js
'use strict' // 鏈接數據庫 require('./db') const config = require('./config') const bodyParser = require('body-parser'); const morgan = require('morgan'); const express = require('express') // 引入express異步異常捕獲模塊 require('express-async-errors'); const app = express(); // 註冊log中間件 app.use(morgan('combined')); // 註冊body解析中間件 // parse application/x-www-form-urlencoded app.use(bodyParser.urlencoded({ extended: false })); // parse application/json app.use(bodyParser.json()); // 註冊路由 app.use('/todos', require('./router/todo')); // 註冊錯誤處理中間件 app.use(function (err, req, res, next) { console.log(err); res.send({ code: -1, msg: err.toString() }) }); app.listen(config.PORT);
'use strict' const config = { PORT: 3000, DB: 'TODO', } module.exports = config
router
'use strict' const router = require('express').Router() const todoController = require('../controller/todo') module.exports = router router.get('/', async (req, res)=>{ let todos = await todoController.getAllTodo(); res.send({ code: 0, data: todos }) }); router.post('/', async (req, res)=>{ await todoController.addTodo(req.body) res.send({code: 0}) }); router.put('/:id', async (req, res)=>{ await todoController.updateTodo(req.params.id, req.body) res.send({code: 0}) }); // 刪除某個todo router.delete('/:id', async (req, res)=>{ await todoController.deleteTodo(req.params.id) res.send({code: 0}) });
db.js
和 model
'use strict' const config = require('./config') const mongoose = require('mongoose') mongoose.connect(`mongodb://127.0.0.1/${config.DB}`) const db = mongoose.connection db.on('error', err=>{ console.log(err); }); db.on('open', ()=>{ console.log('db connect successful!'); });
'use strict' // To-do: 模型類, 內容,是否完成,建立日期 const mongoose = require('mongoose') const schema = new mongoose.Schema({ content: String, isDone: { type:Boolean, default: false }, created: { type: Date, default: Date.now() } }); module.exports = mongoose.model('todo', schema);
controller
'use strict' const Todo = require('../model/todo') // 取出數據庫中的全部todo,而後返回 async function getAllTodo() { return await Todo.find({}) } async function updateTodo(id, update) { await isIdExist(id) // { n: 1, nModified: 1, ok: 1 } let res = await Todo.updateOne({_id:id}, update) if(!res || res.n<1){ throw `${update}更新失敗` } } async function addTodo(todo) { // 先查詢名字是否存在 let t = await Todo.findOne({content: todo.content}) if(t){ //說明已經存在,則拋出異常,由異常處理中間件負責處理 throw `${todo.content} 已經存在` } await Todo.create(todo) } async function isIdExist(id) { // 先判斷傳入的id是否存在 let t = await Todo.findOne({_id: id}) if(!t){ throw `${id}不存在` } } async function deleteTodo(id) { await isIdExist(id) // { n: 1, ok: 1 } let res = await Todo.deleteOne({_id:id}) console.log(res); if(!res || res.n < 1){ throw `${id}刪除失敗` } } module.exports = { getAllTodo, updateTodo, addTodo, deleteTodo }
使用postman進行測試每一個接口。