#2 中间件, 路由,router,https

本章主要详细的学习Express最大的2个特性:中间件和路由

中间件

中间件在很多框架中都存在,Express中间件将大的请求处理函数(request handler)拆分为多个小的部分进行处理。中间件可以对request,response对象进行逻辑控制,从而返回自己需要的结果。

1.中间件栈 Middleware Stack

Express使用 app.use() 方法将中间件添加到中间件栈中,形成函数数组,采取 FIFO(First In First Out) 的方式,对数据依次的处理。

使用NodeJS框架的server,客户端发起请求到接收响应的流程大致如下:

客户端 request --> Node HTTP server(接收请求) --> Express App (向request,response添加额外的功能)
--> Middleware stack处理 --> Node HTTP server(接收处理后的结果) --> 返回给客户端

2.中间件栈结束处理

中间件栈结束处理,要么反生错误退出,要么调用 res.end()方法,或者是 res.send(), res.sendFile()方法(这2个方法内部自动调用res.end()方法)。

3.编写中间件

最常见的中间件形式为:

/*
 * req: 请求对象
 * res: 响应对象
 * next: 函数,表示执行进入下一个中间件函数
 */
function(req, res, next) { /*...*/ }

最后一个中间件'next'可以省略,比如未匹配到路由,返回404:

app.use(function(req, res) {
    res.status(404).render('404);
});

下面实例用来编写2个中间件,一个用来记录日志,一个用来发送文件:

var express = require('express');
var path = require('path');
var fs = require('fs');

var app = express();

// 记录日志
app.use(function(req, res, next) {
    console.log('请求者IP: ' + req.ip);
    console.loge('请求时间: ' + new Date() );
    next();
});

// 发送文件,使用NODE 提供的fs模块
app.use(function(req, res, next) {
    var filePath = path.join(__dirname, 'static', req.url);
    fs.stat(filePath, function(err, fileInfo) {
        if (err) {
            next();
            return;
        }
        if (fileInfo.isFile()) {
            res.sendFile(filePath);
        } else {
            next()
        }
    });
});

app.use(function(req, res) {
    res.status(404).render('404');
});

app.listen(3000);

当然可以使用第3方提供的 morgan 中间件来替代上面的记录日志中间件和Express自带的express.static() 来替代文件的发送,上面的例子可以改写为:

var logger = require('morgan);
// ...

app.use(logger('short'));
app.use(express.static(path.join(__dirname, 'static')));

// ...

错误处理中间件

其签名为:

function(err, req, res, next) { /*...*/ }

如果进入错误模式,则其它的正常的middleware 都不处理,所以一般错误处理放在最后,就像promise 中的 'catch' 一样。

next()添加一个参数,一般是一个错误对象,则可进入错误模式:

next(new Error('Something bad happened!'));

示例:

app.use(function(err, req, res, next) {
    console.error(err); // 记录所有的错误
    next(err); // 传递给下一个 error-handling 中间件
});

// 对错误进行处理的中间件
// 记住这个中间件只有进入错误模式才会调用
app.use(function(err, req, res, next) {
    res.status(500).send('Internal server error.');
});

路由

上一章对路由做了简单的介绍,路由简单点说就是: URL + HTTP 请求动作('GET','POST','PUT','DELETE'...) ---> 对应响应处理函数

现在将对路由将做更为详细的介绍,比如 静态文件路由问题,router的使用 。

1.路径匹配

上一章中谈到了路径匹配的3中方式:字符串,字符串模版,正则表达式。下面看几个示例:

app.get('/users/:userid', function(req, res) {
    var userid = parseInt(req.params.userid, 10);
    // ...
})

// 可以匹配 /100-300这种形式
app.get(/^\/(\d{3})-\/(\d{3})$/, function(req, res) {
    var startId = parseInt(req.params[0], 10);
    var endId = parseInt(req.params[1], 10);
    // ...
})

随带讲一下 req.params 这个属性:

req.params

当使用命名路由参数时,这个属性是一个对象,例如 '/user/:name', 'name' 则为'req.params'对象的一个属性

// GET /user/james

req.params.name
// james

当使用正则表达式来定义路由路径时,捕获到的组则可通过 'req.params[n]' 来获取:

app.get(/^\/(\d{3})-\/(\d{3})$/, function(req, res) {
    var startId = parseInt(req.params[0], 10);
    var endId = parseInt(req.params[1], 10);
    // ...
})

// GET /100-300
req.params[0] // 100
req.params[1] // 300

2.查询参数

在搜索引擎中查询,常会碰到这种形式的路由 '/search?q=javascript20%tutorials',可以使用下面路由进行处理:

app.get('/search', function(req, res) {
    req.query.q == 'javascript tutorials';
    //...
})

使用 req.query 可以获取查询字符串:

// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"

req.query.shoe.color
// => "blue"

req.query.shoe.type
// => "converse"

Routers

一个router就是中间件(middlewares)和路由(routes)的一个单独的实例。可以将一个大的应用拆分为很多小的应用。每个express app都有一个内置的app router。

使用 express.Router() 实例化一个router

1.示例

主app:

// app.js
var express = require('express');
var path = require('path');
var apiRouter = require('./routes/api_router'); // 引入该router

var app = express(); // 实例化一个express app

// ...

// 可以看出router就像中间件一样,可以使用 'app.use()' 添加到中间件栈中
// 任何 以'/api' 开头的路由都会使用 apiRouter中的逻辑进行处理
app.use('/api', apiRouter);

app.listen(3000);

router:

var express = require('express');

// 实例化一个router
var api = express.Router();

// 这个router可以拥有自己的路由和中间件

// 中间件
// router内部也可以使用 'router.use()' 来写中间件
api.use(function(req, res, next) {
    // ...
})

// 路由
api.get('/users', ...) //  路由 '/api/users'
api.post('/user', ...)
api.get('/message', ...)  // 路由 '/api/message'
api.post('/message', ...)

// 导出模块
module.exports = api;

2.Router apis

router有几个方法,下面介绍一下:

express.Router([options])

可选参数用于定义router的行为,有3个可选参数:

  • caseSensitive: 是否区分大小写。默认值为false,比如 '/foo' 和 '/FOO'是一样的
  • mergeParams: 保留来自父路由器的req.params值。如果父项和子项具有冲突的参数名称,则该子项的值优先。默认值为false
  • strict: 打开严格路由。默认为disabled, 比如 '/foo' 和 '/foo/'是一样的

router.all(path, [callback, ...] callback)

这个方法和 router.METHOD() 很像,只是这个方法匹配所有的 HTTP 请求动作('GET', 'POST'...)

对指定路径下的全局逻辑十分有用,比如:

// 全局路径下,该router所有请求都需要权限
router.all('*', requireAuth, loadUser); // requireAuth, loadUser都是自己写的函数

// 指定路径
router.all('/api/*', requireAuth);

router.route()

返回单一路由实例,可以链式调用,避免重复

var router = express.Router();

// router.param() 对url中指定参数进行逻辑处理
router.param('user_id', function(req, res, next, id) {
    // 模拟用户,可以从数据库中取回数据
    req.user = {
        id: id,
        name: 'TJ'
    };
    next();
});

// 对于这个路径 '/users/:user_id'
router.route('/users/:user_id')
    .all(function(req, res, next) {
        // ...
        next();
    })
    .get(function(req, res, next) {
        res.json(req.user);
    })
    .put(function(req, res, next) {
        res.user.name = req.params.name;
        // 保存user
        res.json(req.user);
    })
    .post(function(req, res, next) {
        next(new Error('something wrong'))
    })
    .delete(function(req, res, next) {
        next(new Error('还未实现该功能'));
    });

3.静态文件路径

以下几种情形:

1.比如说我们访问 'www.example.com/dog.jpg', 现在想通过 'www.example.com/gallery/dog.jpg',可以通过下面方式

var photoPath = path.resolve(__dirname, 'public');
app.use('/gallery', express.static(photoPath));

2.多个静态文件路径,有时候文件可能在不同的文件夹,我们可以多次调用 'express.static()' 方法来添加静态文件的位置

var publicPath = path.resolve(__dirname, 'public');
var userUploadsPath = path.resolve(__dirname, 'user_uploads');

app.use(express.static(publicPath));
app.use(express.static(userUploadsPath));

这种情形可以存在下面几种方式:

  1. 用户请求的资源既不在publicPath和userUploadsPath中,则2个中间件函数将继续到下一个路由和中间件函数
  2. 用户请求的资源在publicPath中,第一个中间件函数发送文件,其余的路由和中间件不被调用
  3. 用户请求的资源在userUploadsPath中,第一个中间件函数调用next(), 第2个中间件函数发送文件,其余的路由和中间件不被调用
  4. 用户请求的资源即在publicPath,又在userUploadsPath中,这种情形,用户将不能获取想要的资源

3.上面的第4中情形,可以通过下列方式解决

app.use('/public', express.static(publicPath));
app.use('/uploads', express.static(userUploadsPath));

现在用户可以同时获取'/public'和'/uploads'的资源,比如 '/public/cat.png','/uploads/dog.png'

HTTPS

HTTPS添加一层安全协议层(TSL(相对ssl更好) 或 SSL)。

通俗解释:每个设备都有一个公共密钥(google称之为证书比如CAs)和一个私有密钥,发送信息,通过私有密钥加密,对方接收信息,通过公共密钥识别,然后通过自身的私有密钥解密。

为了使用HTTPS需要以下步骤:

  1. 使用openssl产生公共密钥和私有密钥
openssl genrsa -out privatekey.pem 1024 // 产生私有密钥到privatekey.pem
openssl req -new -key privatekey.pem -out request.pem // 产生证书签名请求
  1. 需要从CA申请证书,申请CA证书之后,可以使用NODE 内置的 HTTPS模块和express,和http类似,但是你必须提供证书和私有密钥
var express = require('express');
var https = require('https');
var fs = require('fs');

var app = express();

// ...

var httpsOptions = {
    key: fs.readFileSync('path/to/private/key.pem'),
    cert: fs.readFileSync('path/to/cerificate.pem')
};
https.createServer(httpsOptions, app).listen(3000);

  1. 如果想要既可以使用http协议,又可以使用https协议,可以使用下面方法
var express = require('express');
var http = require('http');
var https = require('https');
var fs = require('fs');

var app = express();

// ...

var httpsOptions = {
    key: fs.readFileSync('path/to/private/key.pem'),
    cert: fs.readFileSync('path/to/cerificate.pem')
};

http.createServer(app).listen(80);
https.createServer(httpsOptions, app).listen(443);

总结

本章主要详细的将中间件的运行原理,中间件的定义形式;使用路由时的一些方法,如何使用 router 将app分割成小的app,及router常用的一些方法apis,最后粗略的讲了一下如何使用https模块,对于https协议怎么进行处理。

2017年3月20日 19:40:57

推荐阅读更多精彩内容