前端点滴(Node.js)(五)---- 构建 Web 应用(四)中间件、Express 基础与实例

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开发的。

简单实例:

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)最终效果

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章