4. 中間件、Express
一、Node.js 中間件
1. 中間件概念
在NodeJS中,中間件主要是指封裝所有Http請求細節處理的方法。一次Http請求通常包含很多工作,如記錄日誌、ip過濾、查詢字符串、請求體解析、Cookie處理、權限驗證、參數驗證、異常處理等,但對於Web應用而言,並不希望接觸到這麼多細節性的處理,因此引入中間件來簡化和隔離這些基礎設施與業務邏輯之間的細節,讓開發者能夠關注在業務的開發上,以達到提升開發效率的目的。
中間件的行爲比較類似Java中過濾器的工作原理,就是在進入具體的業務處理之前,先讓過濾器處理。它的工作模型下圖所示。
簡單來說就是:中間件就是在HTTP請求中的過濾層(HTTP請求交給業務邏輯處理前,先交給另一些方法過濾)。
中間件機制核心實現
中間件是從Http請求發起到響應結束過程中的處理方法,通常需要對請求和響應進行處理,因此一個基本的中間件的形式如下:
const middleware = (req, res, next) => {
// ... ToDo ...
// 關鍵在next()
next()
}
(1)基本形式的實現
如下定義三個簡單的中間件:
const middleware1 = (req, res, next) => {
console.log('middleware1 start')
next()
}
const middleware2 = (req, res, next) => {
console.log('middleware2 start')
next()
}
const middleware3 = (req, res, next) => {
console.log('middleware3 start')
next()
}
通過遞歸的形式,將後續中間件的執行方法傳遞給當前中間件,在當前中間件執行結束,通過調用`next()`方法執行後續中間件的調用。
// 中間件數組
const middlewares = [middleware1, middleware2, middleware3]
function run (req, res) {
const next = () => {
// 獲取中間件數組中第一個中間件
const middleware = middlewares.shift()
if (middleware) {
middleware(req, res, next)
}
}
next()
}
run() // 模擬一次請求發起
結果:
(2)複雜形式的實現
有些中間件不止需要在業務處理前執行,還需要在業務處理後執行,比如統計時間的日誌中間件。在方式一情況下,無法在 next() 爲異步操作時再將當前中間件的其他代碼作爲回調執行。因此可以將 next() 方法的後續操作封裝成一個 Promise 對象,中間件內部就可以使用 next().then 形式完成業務處理結束後的回調。改寫 run() 方法如下:
// 中間件數組
const middlewares = [middleware1, middleware2, middleware3]
function run (req, res) {
const next = () => {
const middleware = middlewares.shift()
if (middleware) {
// 將middleware(req, res, next)包裝爲Promise對象
return Promise.resolve(middleware(req, res, next))
}
}
next()
}
run()
中間件的調用方式需改寫爲:
得益於async函數的自動異步流程控制,中間件也可以用如下方式來實現:
const middleware1 = (req, res, next) => {
console.log('middleware1 start')
// 所有的中間件都應返回一個Promise對象
// Promise.resolve()方法接收中間件返回的Promise對象,供下層中間件異步控制
return next().then(() => {
console.log('middleware1 end')
})
}
// async函數自動返回Promise對象
const middleware2 = async (req, res, next) => {
console.log('middleware2 start')
await new Promise(resolve => {
setTimeout(() => resolve(), 1000)
})
await next()
console.log('middleware2 end')
}
const middleware3 = async (req, res, next) => {
console.log('middleware3 start')
await next()
console.log('middleware3 end')
}
結果:
(3)基於 Node.js Connect 中間件框架
前言
“中間件”在軟件領域是一個非常廣的概念,除操作系統的軟件都可以稱爲中間件,比如,消息中間件,ESB中間件,日誌中間件,數據庫中間件等等。
Connect被定義爲Node平臺的中間件框架,從定位上看Connect一定是出衆的,廣泛兼容的,穩定的,基礎的平臺性框架。如果攻克Connect,會有助於我們更瞭解Node的世界。Express就是基於Connect開發的。
- body-parser - previous bodyParser , json, and urlencode。 (請求體解析)
- compression - previously compress(壓縮)
- connect-timeout - previously timeout(定時器)
- cookie-parser - previously cookieParser
- cookie-session - previously cookieSession(Cookie與Session會話)
- csurf - previously csurf
- errorhandler - previously error-handler
- express-session - previously session (express與Session會話)
- method-override - previously method-override
- morgan - previously logger(請求記錄器)
- response-time - previously response-time
- serve-favicon - previously favicon(服務圖標)
- serve-index - previously directory
- serve-static - previously static
- vhost - previously
vhost
(虛擬主機)
簡單實例:
1)npm install connect compression cookie-session body-parser
var connect = require('connect');
var http = require('http');
var cookieSession = require('cookie-session');
// parse urlencoded request bodies into req.body
var bodyParser = require('body-parser');
// gzip/deflate outgoing responses
var compression = require('compression');
var app = connect();
app.use(compression());
app.use(cookieSession({
name: 'session',
keys: ['Errrl', 'Errrl2']
}))
app.use(function (req, res, next) {
// Use it to record browser times
req.session.views = (req.session.views || 0) + 1
next();
})
app.use(bodyParser.urlencoded({ extended: false }));
// respond to browser
app.use(function (req, res) {
res.end('Hello from Connect!\n');
});
//create node.js http server and listen on port
http.createServer(app).listen(8000);
詳細來了解:https://npm.taobao.org/package/connect
(4)總結
上述講述的 connect 中間件框架衍生出來的就是 express 可以這麼說,express 框架就是基於 connect 開發出來的。在 express 框架中,中間件的實現方式爲方式一,並且全局中間件和內置路由中間件中根據請求路徑定義的中間件共同作用,不過無法在業務處理結束後再調用當前中間件中的代碼。koa2 框架中中間件的實現方式爲方式二,將 next() 方法返回值封裝成一個 Promise ,便於後續中間件的異步流程控制,實現了 koa2 框架提出的洋蔥圈模型,即每一層中間件相當於一個球面,當貫穿整個模型時,實際上每一個球面會穿透兩次。
二、Express 框架
1. Express 簡介
Express 是基於 Node.js 平臺,快速、開放、極簡的 Web 開發框架, 提供一系列強大特性幫助你創建各種Web應用。Express 不對 node.js 已有的特性進行二次抽象,我們只是在它之上擴展了Web應用所需的功能。豐富的HTTP工具以及來自Connect框架的中間件隨取隨用,創建強健、友好的API變得快速又簡單 。官網:http://www.expressjs.com.cn/
2. 入門
安裝
就像一個普通的第三方模塊一樣安裝即可;
npm install express
路由
路由是指確定應用程序如何響應客戶端對特定端點的請求,該特定端點是URI(或路徑)和特定的HTTP請求方法(GET,POST等)。
每個路由可以具有一個或多個處理程序函數,這些函數在路由匹配時執行。
路由定義採用以下結構:
app.METHOD(PATH, HANDLER)
說明:
- app:express的實例對象。
- METHOD:請求方法,注意小寫(比如post,get 等)。
- PATH:請求路徑,url。
- HANDLE:是當路由匹配時執行的路徑映射函數。
實例:
app.get('/', function (req, res) {
res.send('Hello World!')
})
靜態文件
爲了提供諸如圖像、CSS 文件和 JavaScript 文件之類的靜態文件,請使用 Express 中的 express.static 內置中間件來託管靜態文件。
此函數特徵如下:
express.static(root, [options])
通過如下代碼就可以將規定目錄下的圖片、CSS 文件、JavaScript 文件對外開放訪問了:
注意:此處的規定目錄名一定是 public。
app.use(express.static('public'))
現在,你就可以訪問 public 目錄中的所有文件了:
http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/bg.png
http://localhost:3000/hello.html
此外還可以爲該 express.static() 功能所服務的文件創建虛擬路徑前綴(文件系統中實際上不存在該路徑)
app.use('/static', express.static('public'))
現在,你就可以通過帶有 /static 前綴地址來訪問 public 目錄中的文件了。
http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css
http://localhost:3000/static/js/app.js
http://localhost:3000/static/images/bg.png
http://localhost:3000/static/hello.html
Hello World
/* app.js */
/* Hello World */
const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(3000, () => console.log('server is create http://127.0.0.1:8000'))
啓動
node app.js
3. 項目重構
接下來就對上一次的項目進行 express 框架重構,從而來了解 express 框架的各部分功能。
(1)啓動服務器
創建 http.js:
var express = require('express');
var app = express();
app.listen('8000',()=>{
console.log('server is create http://127.0.0.1:8000');
});
(2)重寫路由模塊
之前我們寫了一個獨立的模塊(luyou.js)來處理請求,而在 express 中已經幫我們寫好了路由的請求處理規則,不需要我們進行判斷;
路由是指確定應用程序如何響應對特定端點的客戶端請求,該請求是URI(或路徑)和特定HTTP請求方法(GET,POST等)。
每個路由都可以有一個或多個處理函數,這些函數在路由匹配時執行。
修改 http_請求處理.js:
怎麼修改?需要什麼?
1)需要路由模塊
2)需要請求路徑
3)需要路徑的映射方法
/* http_請求處理.js */
var express = require('express');
/* 創建一個路由模塊 */
var router = express.Router();
/* 導入映射方法模塊 */
var chuli = require('./http_處理數據');
/* 根據對應的路徑其映射對應的方法(跳轉),理解:中間件 */
/* 鏈式操作 */
router
.get('/',chuli.getall)
.get('/getone',chuli.getone)
.get('/delone',chuli.delone)
.get('/setone',chuli.setone_get)
.post('/setone',chuli.setone_post)
.get('/login',chuli.login_get)
.post('/login',chuli.login_post)
/* 導出router */
module.exports.router = router;
好了導出的 router 導去哪?
毋庸置疑,當然是導去 http.js 中。
修改 http.js:
var express = require('express');
/* 導入router */
var router = require('./http_請求處理');
var app = express();
/* 導入的router就像是express中的中間件,app.use()就可以使用這個路由中間件了 */
app.use(router);
app.listen('8000',()=>{
console.log('server is create http://127.0.0.1:8000');
});
(4)修改模塊,完成對數據庫增、刪、改、查操作,實現登錄功能
https://blog.csdn.net/Errrl/article/details/104299384
在上篇文章中講到如何實現添加、上傳用戶信息,在這一講中將會講到。
1)獲取上篇文章中的 http_sql.js
修改 http_處理數據.js:
const fs = require('fs');
const url = require('url');
const sql = require('./http_sql');
var getall = (req,res)=>{
sql.select((data)=>{
/**
* 在此響應
* 1. 使用art-template模板解析html 組合數據,再使用res.send() 返回 html。
* 2. 使用express模板引擎解析html組合數據,再使用res.render() 返回 html。
*/
})
}
使用模板引擎響應 html:
模板:
index.html
upuser.html
修改 http.js:
var express = require('express');
/* 導入router */
var router = require('./http_請求處理');
var app = express();
/* 導入的router就像是express中的中間件,app.use()就可以使用這個路由中間件了 */
app.use(router);
/* 使用模板引擎,同時加載靜態資源 */
app.engine('html',require('express-art-template'));
app.use(express.static('public'));
app.listen('8000',()=>{
console.log('server is create http://127.0.0.1:8000');
});
修改 http_處理數據.js:
const fs = require('fs');
const url = require('url');
const sql = require('./http_sql');
var getall = (req,res)=>{
sql.select((data)=>{
/* 利用模板引擎響應數據 */
res.render('index.html',{data:data});
})
}
var getone = (req,res)=>{
var re_url_id = url.parse(req.url,true).query.id;
sql.where('id='+re_url_id).select((data)=>{
res.render('./new.html',{data:data});
})
}
var delone = (req,res)=>{
var re_url_id = url.parse(req.url,true).query.id;
sql.where('id='+re_url_id).del((data)=>{
if(data.affectedRows>=1){
var scr = "<script>alert('刪除成功');window.location.href='/'</script>";
res.setHeader('content-type','text/html;charset=utf-8');
res.end(scr)
}
})
}
var setone_get = (req,res)=>{
var re_url_id = url.parse(req.url,true).query.id;
sql.where('id='+re_url_id).select((data)=>{
res.render('./upuser.html',{data:data});
})
}
實現用戶上傳信息(比如:表單信息,圖片,文件,視頻...):
使用 formidable 第三方插件來進行信息的上傳。
1)npm install formidable
2)接上
var fs = require('fs');
var formidable = require('formidable');
var setone_post = (req,res)=>{
/* 實例化插件 */
var form = new formidable.IncomingForm();
var re_url_id = url.parse(req.url,true).query.id;
/* 封裝響應方法 */
var fn = (datas)=>{
sql.where('id='+id).update(datas,(data)=>{
if(data.affectedRows>=1){
var scr = "<script>alert('修改成功');window.location.href='/'</script>";
res.setHeader('content-type','text/html;charset=utf-8');
res.end(scr);
}
})
}
/* 實例化對象解析請求體中上傳的信息類型 */
form.parse(req,(err,fields,files)=>{
/* fields:表單信息, files:上傳文件的信息, 注意都要上傳。 */
/* 先來判斷是否有上傳文件的存在,有就將其添加到public中,連同表單信息一起修改數據庫,響應;沒有則僅表單信息修改數據庫,響應。 */
if(!files.zhaopian.name == ''){
/* 修改保存路徑 */
// 1. 讀出文件路徑
var readStream = fs.createReadStream(files.zhaopian.path);
// 2. 製作文件名與文件保存路徑
var rel_path = '/img/'+files.zhaopian.name;
var writeStream = fs.createWriteStream('./public'+rel_path);
// 3. 文件轉移
readStream.pipe(writeStream);
/* 組合數據修改數據庫,響應 */
fields.img = rel_path;
/* 改數據庫,響應 */
fn(fields);
}else{
fn(fields);
}
})
}
實現添加用戶:
修改 http_請求處理:
.get('/add', chuli.add_get)
.post('/add', chuli.add_post)
修改 http_處理數據:
1)添加 模板 add.html。
2)sql 語句中需要 id ,以及各個表單的數據。但是怎麼獲取id , 獲取什麼 id。
var add_get = (req,res)=>{
sql.ad((data)=>{
/* data: RowDataPacket { 'max(id)': 6 } */
var r = JSON.parse(JSON.stringify(data[0])); //=> { 'max(id)': 6 }
var d = r['max(id)'] + 1; //=> 7
/* 放在最後面 */
res.render('./add.html',{data:d});
})
}
var add_post = (req,res)=>{
var form = formidable.IncomingForm();
var fn = (datas)=>{
sql.add(datas,(data)=>{
if(data.affectRows>=1){
var scr = "<script>alert('添加成功');window.location.href='/'</script>";
res.setHeader('content-type','text/html;charset=utf-8');
res.end(scr);
}
})
}
/* 上傳必要的數據 */
form.parse(req,(err,fields,files)=>{
if(!files.zhaopian.name == ''){
var readStream = fs.createReadStream(files.zhaopian.path);
var real_path = '/img/'+files.zhaopian.name;
var writeStream = fs.createWriteStream('./public'+real_path);
readStream.pipe(writeStream);
fielids.img = real_path;
fn(fields);
}else{
fn(fields);
}
})
}
/* 導出 add_get add_post */
修改 http_sql:
var ad = (callback) => {
/* 獲取id最大值 */
var sql = 'select max(id) from users';
connection.query(sql, (err, data) => {
callback(data);
})
}
var add = (datas,callback)=>{
/* 如果datas中存在img就完整添加上傳信息,沒有則相反 */
if(datas.img == undefined){
var sql = `INSERT INTO users(id,name,nengli,jituan,img) VALUES('${datas.id}','${datas.name}','${datas.nengli}','${datas.jituan}','')`;
}else{
var sql = `INSERT INTO users(id,name,nengli,jituan,img) VALUES('${datas.id}','${datas.name}','${datas.nengli}','${datas.jituan}','${datas.img}')`;
}
connection.query(sql,(err,data)=>{
callback(data);
})
}
/* 導出 ad add */
實現用戶登錄功能
由於管理系統不是隨隨便便就被訪問到,所以就要對其進行登錄跳轉。
使用 cookie-session 實現用戶登錄功能。
1)創建 cookie-session
var cookieSession = require('cookie-session');
app.use(cookieSession({
name:"sessionID",
keys:['Errrl']
}))
2)利用 cookie-session
模板 login.html:
.post('/login', chuli.login_post)
.get('/login', chuli.login_get)
var login_get = (req, res) => {
res.render('./login.html', {})
}
var login_post = (req, res) => {
res.setHeader('content-type', 'text/html;charset=utf-8');
var form = new formidable.IncomingForm();
form.parse(req, (err, fields) => {
/* 獲取數據庫中儲存的用戶名與密碼 */
sql.user((data_username, data_password) => {
/* 驗證輸入的用戶名密碼是否與數據庫中儲存的用戶名密碼相同,相同則通過,不同則反 */
if (fields.username == data_username && fields.psw == data_password) {
req.session.session_data = fields; // 得到令牌,此處是關鍵。
// 登錄成功,允許訪問首頁
var scr = "<script>alert('登錄成功');window.location.href = '/'</script>";
res.end(scr);
}
})
sql.user((data_username, data_password) => {
if (fields.username != data_username && fields.psw != data_password) {
// 登錄失敗,不允許訪問首頁
var scr = "<script>alert('登錄失敗');window.location.href = '/login'</script>";
res.end(scr);
}
})
})
}
/* 導出 */
同時還要對獲取主頁信息進行攔截:
var getall = (req, res) => {
if (!req.session.session_data) { // 判斷是否得到了令牌,有就通行,無就不通行
var scr = "<script>alert('請登錄後訪問');window.location.href = '/login'</script>"
res.setHeader('content-type', 'text/html;charset=utf-8');
res.send(scr);
return;
}
sql.select((data) => {
res.render('./index.html', { data: data });
})
}
上述的username,password存放於數據庫中,所以:
var user = (callback) => {
var sql = "select * from users3"
connection.query(sql, (err, data) => {
for (i in data) {
callback(data[i].username, data[i].password);
}
})
}
(5)最終效果