本章主要详细的学习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));
这种情形可以存在下面几种方式:
- 用户请求的资源既不在publicPath和userUploadsPath中,则2个中间件函数将继续到下一个路由和中间件函数
- 用户请求的资源在publicPath中,第一个中间件函数发送文件,其余的路由和中间件不被调用
- 用户请求的资源在userUploadsPath中,第一个中间件函数调用next(), 第2个中间件函数发送文件,其余的路由和中间件不被调用
- 用户请求的资源即在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需要以下步骤:
- 使用openssl产生公共密钥和私有密钥
openssl genrsa -out privatekey.pem 1024 // 产生私有密钥到privatekey.pem
openssl req -new -key privatekey.pem -out request.pem // 产生证书签名请求
- 需要从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);
- 如果想要既可以使用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