书写可重用的中间件
这里以一个例子来说明重用的方法。
一个用于当请求事件过长而进行提醒的中间件在很多情景下都非常有用。
要求:在测试过程中,所有响应都在100毫秒(ms)内完成,确保能将响应事件大于100ms的请求记录下来。
即:为一个名为request-time.js的独立模块中创建一个中间件。
这个模块暴露一个函数,这个函数本身又返回一个函数。这对于可配置的中间件来说是很常见的写法。
中间件本身创建一个计时器,并在指定的事件内触发。
var timer = setTimeout(function() {
console.log('\033[90m %s %s \033[39m \033[31m is taking too long\033[39m',req.method,req.url);
},time);
这里要确保响应事件在100ms以内要清楚计时器。
另外一个在中间件中常见的模式叫做重写方法(也叫猴子补丁),能够在其他中间件调用它的时,执行指定的行为。
即:
res.end = function(chunk,encoding) {
res.end = end;
res.end(chunk,encoding);
clearTimeout(timer);
};
完整的代码如下:
// request-time.js
/*
* 请求中间件
*
* 选项:
* - ‘time’('Number'):超时阈值(默认100)
*
* @param {object} options
* @api public
*/
module.exports = function(opts) {
var times = opts.time || 100; // 设置默认阈值
return function (req,res,next) { // 返回一个中间件函数
var timer = setTimeout(function() {
console.log('\033[90m %s %s \033[39m \033[31m is taking too long\033[39m',req.method,req.url);
},time);
var end = res.end;
res.end = function(chunk,encoding) {
res.end = end;
res.end(chunk,encoding);
clearTimeout(timer);
};
next();
};
}
为了测试上面的例子,我们需要创建一个Connect应用,并创建两条路由:第一条很快得到响应,另一条一秒后得到响应。
// sample.js
var connect = require('connect'),
time = require('./request-time'),
logger = require('morgan'); // 记录请求情况/日志
// 创建服务器
var server = connect();
// 记录请求情况
server.use(logger());
// 实现中间件
server.use(time({time: 500}));
// 模拟快速响应
server.use(function(req,res,next) {
if (req.url == '/a') {
res.writeHead(200);
res.end('Fast');
} else {
next();
}
});
// 模拟慢速响应
server.use(function(req,res,next) {
if (req.url == '/b') {
setTimeout(function() {
res.writeHead(200);
res.end('Slow!');
},1000);
} else {
next();
}
});
// 监听
server.listen(3000);
运行服务器
node sample.js
访问:localhost:3000/a
访问:localhost:3000/b
查看日志如图:
你会发现访问localhost:3000/b的时候由于定时器设置为1s,所以在服务器返回数据之前就可以执行request-time模块中的定时器(未被清除),然后就被end回浏览器了。
通过这个例子,可以看到可复用的中间件可以理解为一个你模块。
serve-static中间件
以前加载静态文件是static中间件,现在是serve-static,是第三方模块,需要引入。
具体使用:serve-static模块
serve-static中间件提供一个中间件函数来服务给定目录的文件。服务的文件是由req.url和根目录的组合来决定的。当文件没有找到,不会返回404响应,而是会调用下一个next()来驱动下一个中间件。
选项(Options)
acceptRanges
该字段用于启动或禁用指定范围内的请求。禁用指定范围内的请求,将不会发送Accept-Ranges头信息,同时会忽略范围内请求的头信息。
cacheControl
启用或禁用在头信息设置Cache-Control,默认为true。如果禁用的话会忽略不可变的maxAge选项。
lastModified
是否启用Last-Modified头字段。
redirect
当路径是目录的时候重定向到’/’
更多Options请戳serve-static模块
morgan
morgan是记录HTTP请求日志的中间件。
使用给定的格式和选项创建新的morgan日志记录器中间件函数。format参数可以是预定义名称的字符串(参见下面的名称)、格式字符串的字符串或将生成日志条目的函数。
format函数将使用三个参数令牌、req和res来调用,令牌是一个对象,其中所有已定义的令牌都包含在其中,req是HTTP请求,res是HTTP响应。该函数将返回一个字符串,该字符串将作为日志行,或者未定义/ null以跳过日志记录。
具体使用方法:
morgan模块
var morgan = require('morgan');
var connect = require('connect');
var serve = connect();
serve.use(morgan('combined'));
// 记录使用情况
body-parser中间件
解析请求体的中间件。body-parser会检测Contect-Type的值。
body-parser提供以下解析器:
var bodyParser = require('body-parser');
JSON body parser : bodyParser.json([options])
Raw body parser:bodyParser.raw([options])
Text body parser: bodyParser.text([options])
URL-encoded form body parser:bodyParser.urlencoded([options])
在express中使用body-parser中间件借此json文件的写法:
app.use(bodyParser.json({ type: 'application/*+json' }))
具体可戳body-parser中间件
使用body-parser中间件处理文件上传
一般的表单提交中,如果设置了action属性,那么在提交时页面会自动跳转到action属性所对应的页面中。
serve.use(serverStatic(__dirname + ‘/static’)); 表示挂载根路径 “localhost:3000” 到其子路径 “static” 下,它会自动找到其中的 “index.html” 。也就是我们的表单文件。
坑:
(说是坑,实际上还是知识点不扎实。。)
1、这个测试不是直接打开index.html而是打开服务器后访问localhost:3000,利用服务器将静态文件挂载到服务器当前目录下来访问。
上传文件的表单项需要指定为input,type是file
2、要上传文件必须将表单enctype设置为multipart/form-data 这个参数表示表单将会以多部件3、表单的形式上传enctype=”application/x-www-form-urlencoded”是默认值。这个值的意思指将会对表单项的内容进行url编码,所谓url编码就将请求参数转换为二进制编码。
2、点击上传之后,服务器拿不到res.body的值,这说明文件没有上传成功,原因:body-parser 无法解析多部件的请求体,所以无法将请求数据挂载到req对象上,需要使用multer中间件。
Multer向请求对象添加一个body对象和一个或多个文件对象。body对象包含表单文本字段的值,file或files对象包含通过表单上传的文件。
安装Multer模块:npm install --save multer
代码如下:
// index.js
var connect = require('connect');
var serverStatic = require('serve-static');
var multer = require("multer");
var serve = connect();
var upload = multer({ dest: 'uploads/' }); // 将浏览器上传的文件放在uploads文件夹下,如果没有dest属性,则上传的文件会放在内存而不是磁盘中
serve.use(serverStatic(__dirname + '/static'));
serve.use(upload.single('file'));
serve.use(function(req,res,next) {
if (req.method == 'POST') {
console.log(req.file);
res.end('over');
} else {
next();
}
});
serve.listen(3000);
对应的html文件:
// static/index.html
<!-- 这是static下的index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
</head>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
<input type="file" name="file" multiple="multiple"/>
<button>Send files!</button>
</form>
</body>
</html>
浏览器打开localhost:3000
上传文件之后,页面返回:
此时我们看一下服务器文件
可以看到,多了一个uploads目录(文件夹命名由自己设置),然后打开文件,你会发现:
点击链接,出现乱码:
这是由于我们没有设置存入文件的编码格式。(这部分我暂时不知道怎么处理,后续会去找解决方案,可以参考一下:
处理中文乱码
处理乱码
Node不支持gbk编码。
var connect = require('connect');
var serverStatic = require('serve-static');
var multer = require("multer");
var fs = require('fs');
var iconv = require('iconv-lite');
var serve = connect();
var upload = multer({ dest: 'uploads/' ,encoding: 'gbk'}); // 将浏览器上传的文件放在uploads文件夹下,如果没有dest属性,则上传的文件会放在内存而不是磁盘中
serve.use(serverStatic(__dirname + '/static'));
serve.use(upload.single('file'));
serve.use(function(req,res,next) {
if (req.method == 'POST') {
fs.stat(req.file.path,'utf8',function(err,data) {
if (err) {
res.writeHead(500);
res.end('Error');
return ;
}
var content = fs.readFileSync(req.file.path, 'utf8');
res.writeHead(200,{"Content-Type" :"text/plain"});
res.end(content);
});
console.log(req.file);
} else {
next();
}
});
serve.listen(3000);
上面代码实现了浏览器上传文件之后服务器存储在某个文件夹下,并将文件内容返回给浏览器。(普通文本可行,特殊文件该例子没有处理)
使用Multer模块可以参考例子也可以看文档
cookie-parser中间件
cookie-parser中间件可以将cookie信息挂载到req.cookies上。
// cookieHandle.js
var connect = require('connect');
var cookieParser = require('cookie-parser');
var server = connect();
server.use(cookieParser());
server.use(function(req,res,next) {
console.log(req.headers.cookie); // 注意这里不是req.cookie
res.writeHead(200,{
"Content-Type": "text/html",
"Set-Cookie": "name=value"
}); // express中才是使用res.cookie()来设置cookie
next();
});
server.use(function(req,res,next) {
res.end('hello');
});
server.listen(3000);
可戳更详细
Session
绝大多数web应用中,多个请求间共享“用户会话”的概念是非常重要的。“登录”一个网站时,多多少少会使用某种形式的会话系统,它主要通过在浏览器中设置cookie来实现,该cookie信息会在随后所有的请求头信息中被带回服务器。
session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而session保存在服务器上。
客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。
session的【options】
secret:用来对session数据进行加密的字符串.这个属性值为必须指定的属性,来计算 hash 值并放在 cookie 中,使产生的 signedCookie 防篡改。
name:表示cookie的name,默认cookie的name是:connect.sid。
maxAge:cookie过期时间,毫秒。
resave:是指每次请求都重新设置session cookie,假设你的cookie是6000毫秒过期,每次请求都会再设置6000毫秒。
saveUninitialized: 是指无论有没有session cookie,每次请求都设置个session cookie ,默认给个标示为 connect.sid。
为理解session,创建一个简单的登录系统:
- 第一次登录的用户返回登录表单
- 认证
- 已经登录的用户记住身份
- 若退出登录,删除会话
我们把用户凭证信息存放在一个名为user.json的文件中。
users.json (代替数据库查询的session文件)
// users.json
{
"tobi": {
"password": "ferret",
"name": "Tobi Holowaychuk"
}
}
session-login.js实现登录之后记住身份等操作。
// session-login.js
var connect = require('connect'),
bodyParser = require('body-parser'),
cookieParser = require('cookie-parser'),
sessionParser = require('express-session'),
morgan = require('morgan'),
users = require('./users'); // 只要是对外暴露数据,就不需要加上module.exports,直接把数据文件以json形式暴露出来就好了
var server = connect();
server.use(morgan());
server.use(bodyParser());
server.use(cookieParser());
server.use(sessionParser({
secret: 'hubwiz app', //secret的值建议使用随机字符串
cookie: {maxAge: 60 * 1000 * 30}, // 过期时间(毫秒)
resave: true,
saveUninitialized:true
}));
server.use(function (req,res,next) {
if (req.url == '/' && req.session.logged_in) { // 已经登录
res.writeHead(200, {"Content-Type": "text/html"});
res.end('Welcome back, <b>' + req.session.name + '</b>.' + '<a href="/logout">Logout</a>');
} else {
next();
}
});
server.use(function (req,res,next) { // 获取登录页面
if (req.url == '/' && req.method == 'GET') {
res.writeHead(200, {"Content-Type": "text/html"});
res.end([
'<form action="/login" method="POST">' ,
'fieldset',
'<legend>Please log in</legend>',
'<p>User:<input type="text" name="user"></p>',
'<p>Password:<input type="password" name="password"></p>',
'<button>Submit</button>',
'</fieldset>',
'</form>'
].join(''));
} else {
next();
}
});
server.use(function (req,res,next) { // 提交登录信息
if (req.url == '/login' && req.method == 'POST') {
res.writeHead(200);
if (!users[req.body.user] || req.body.password != users[req.body.user].password) {
res.end('Bad username/password');
} else {
req.session.logged_in = true;
req.session.name = users[req.body.user].name;
console.log(req.session);
res.end('Authenticated');
}
} else {
next();
}
});
server.use(function (req,res,next) { // 登出
if (req.url == '/logout') {
req.session.logged_in = false;
res.writeHead(200);
res.end('Logged out');
} else {
next();
}
});
server.listen(3000);
第一次访问,提交正确的登录信息:
成功认证:
重新访问,此时session认证已经生效:
退出之后删除会话,重新访问时显示登录表单的页面: