Node.js 实战_4 Connect
Connect
Connect是一个框架,它使用被称为中间件的模块化组件,以可重用的方式实现Web程序中的逻辑。在Connect中,中间件组件是一个函数,它拦截HTTP服务器提供的请求和响应对象,执行逻辑,然后或者结束响应,或者把它传递给下一个中间件组件。Connect用分派器把中间件“连接”在一起。
image一、搭建一个 Connect 程序
最小的connect程序:
const connect = require('connect');
var app = connect();
app.listen(3000);
这个裸程序没有中间件,所以分派器会用404 Not Found状态码响应它收到的所有HTTP请求。
原理:Connect 分派器会依次调用所有附着的中间件组件,直到其中一个决定响应该请求。如果直到中间件列表末尾还没有组件决定响应,程序会用404作为响应。
二、Connect 的工作机制
在Connect中,中间件组件是一个JavaScript函数,按惯例会接受三个参数:一个请求对象,一个响应对象,还有一个通常命名为next的参数,它是一个回调函数,表明这个组件已经完成了它的工作,可以执行下一个中间件组件了。
1. 自定义中间件
const connect = require('connect');
var app = connect();
app.use(logger);
app.use(hello);
app.listen(3000);
// logger 中间件组件
function logger(req, res, next) {
// 输出每个HTTP请求的方法和URL··
console.log('%s %s', req.method, req.url);
next();
}
// 响应 “Hello World” 的中间件
function hello(req, res) {
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World!');
}
2. use() 函数支持链式调用
const connect = require('connect');
// logger 中间件组件
function logger(req, res, next) {
// 输出每个HTTP请求的方法和URL并调用 next()
console.log('%s %s', req.method, req.url);
next();
}
// 响应 “Hello World” 的中间件
function hello(req, res) {
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World!');
}
// use() 函数支持链式调用。
connect()
.use(logger)
.use(hello)
.listen(3000);
三、中间件的执行顺序很重要
- 当一个组件不调用
next()
时,命令链中的后续中间件都不会被调用。
1. 用中间件的顺序执行认证
利用中间件的顺序执行机制,添加认证。
假设你已经写了一个叫做 restrictFileAccess 的中间件组件,只允许有效的用户访问文件。有效用户可以继续到下一个中间件组件,如果用户无效,则不会调用 next()
。
const connect = require('connect');
// 利用中间件的位次限制文件访问
connect()
.use(logger)
.use(restrictFileAccess) // 只有有效用户才会调用 next()
.use(serveStaticFiles)
.use(hello);
四、挂载中间件和服务器
挂载:给中间件或整个程序定义一个路径前缀。
const connect = require('connect');
// 当 .use() 的第一个参数是个字符串时,只有 URL 前缀与之匹配时,connect 才会调用后面的中间件
// restrict 组件确保访问页面的是有效用户;
// admin 组件会给用户呈现管理区。
connect()
.use(logger)
.use('/admin', restrict)
.use('/admin', admin)
.use(hello)
.listen(3000);
1. 认证中间件
// 简单的 Basic 认证逻辑
// 借助带着 Base64 编码认证信息的HTTP请求头中的authorization字段进行认证
function restrict(req, res, next) {
var authorization = req.headers.authorization;
if (!authorization) {
// 用 Error 对象做参数调用 next(),相当于通知Connect程序中出现了错误
return next(new Error('Unauthorized'));
}
var parts = authorization.split(' ');
var scheme = parts[0];
var auth = new Buffer(parts[1], 'base64').toString().split(':');
var user = auth[0];
var pass = auth[1];
// 根据数据库中的记录检查认证信息的函数
authenticateWithDatabase(user, pass, function (err) {
if (err) {
return next(err);
}
// 如果认证信息有效,不带参数调用next()
next();
})
}
2. 显示管理面板的中间件
// 路由 admin 请求
function admin(req, res, next) {
switch (req.url) {
case '/':
res.end('try /users');
break;
case '/users':
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(['tobi', 'loki', 'jane']));
break;
}
}
// 💡💡💡
// case 中的字符串是 / 和 /users。
// 而实际请求的字符串是 /admin 和 /admin/users。
// 这是因为在调用中间件之前,Connect从req.url中去掉了前缀,就像URL挂载在/上一样。
const connect = require('connect');
// 在两个不同的挂载点挂载blog程序
connect()
.use(logger)
.use('/blog', blog)
.use('/posts', blog)
.use(hello)
.listen(3000);
五、创建可配置中间件
创建更通用、可重用的中间件。
为了向开发人员提供可配置的能力,中间件通常会遵循一个简单的惯例:用函数返回另一个函数(这是一个强大的JavaScript特性,通常称为闭包)。
function setup(options) {
// 设置逻辑
// 载这里做中间件的初始化
return function(req, res, next) {
// 中间件逻辑
// 即使被外部函数返回了,仍然可以访问options
}
}
// 使用如下
app.use(setup({some: 'options'}));
1. 可配置的logger中间件组件
可配置的中间件可以传入额外的参数来改变他的行为
// 可配置的Connect中间件组件
// setup 函数可以用不同的配置调用多次
function setup(format) {
// logger 组件用正则表达式匹配请求属性
var regexp = /:(\w+)/g;
return function logger(req, res, next) { // Connect使用的真实logger组件
// 用正则表达式格式化请求的日志条目
var str = format.replace(regexp, function (match, property) {
return req[property];
})
console.log(str);// 输出日志条目到控制台
next(); // 将控制权交给下一个中间件组件
}
}
module.exports = setup; // 导出 logger 的 setup 函数
2. 构建路由中间件组件
路由:把请求URL映射到实现业务逻辑的函数上。
// 使用 router 中间件组件
const connect = require('connect');
const router = require('./middleware/router'); // 路由器组件
// 定义路由的对象
var routers = {
GET: {
// 其中的每一项都是对请求URL的映射,并包含要调用的回调函数
'/users': function (req, res) {
res.end('tobi, loki, ferret');
},
'/user/:id': function (req, res, id) {
res.end('users: ' + id);
}
},
DELETE: {
'/user/:id': function (req, res, id) {
res.end('delete user ' + id);
}
}
};
connect()
.use(router(routers)) // 将路由对象传给路由器的 setup 函数
.listen(3000);
路由器组件的复用:
const connect = require('connect');
const router = require('./middleware/router'); // 路由器组件
// 把与用户相关路由、管理员相关路由分到不同的模块中,然后在路由器组件中分别引入
connect()
.use(router(require('./routes/user')))
.use(router(require('./routes/admin')))
.listen(3000);
Jietu20181108-111120
// 简单的路由中间件
const parse = require('url').parse;
module.exports = function route(obj) {
return function (req, res, next) {
// 检查以确保req.method定义了
// 如果未定义,调用next(),并停止一切后续操作
// 程序中自定义的数组中的方法:obj[req.method]
if (!obj[req.method] ){
next();
return;
}
var routes = obj[req.method]; // 查找req.method对应的路径 (数组对象)
var url = parse(req.url); // 解析 URL,以便跟 pathname 匹配
var paths = Object.keys(routes); // 将req.method对应的路径存放到数组中
for (let i = 0; i < paths.length; i++) { // 遍历路径
var path = paths[i];
var fn = routes[path];
path = path.replace(/\//g, '\\/').replace(/:(\w+)/g, '([^\\/]+) ');
var re = new RegExp('^' + path + '$'); // 构造正则表达式
var captures = url.pathname.match(re);
if (captures) {
// 传递被捕获的分组
var args = [req, res].concat(captures.slice(1));
fn.apply(null, args);
return; // 当有相匹配的函数时,返回,以防止后续的next()调用。
}
}
next();
}
}
- 首先检查当前的req.method在routes映射中是否有定义,如果没有则停止进一步处理(即调用next())。
- 之后它会循环遍历已定义的路径,检查是否有跟当前的req.url相匹配的路径。
- 如果找到匹配项,则调用匹配项的回调函数,期望完成对HTTP请求的处理。
构建一个重写URL的中间件组件
重写URL功能,服务端接收的是 /blog/posts/my-post-title 的请求,基于这个URL最后的文章标题查找文章的ID,然后将URL转换为 /blog/posts/id。
'use strict';
const connect = require('connect');
const url = require('url');
const app = connect().use(rewrite).use(showPost).listen(3000);
// 基于缩略名重写请求URL的中间件
var path = url.parse(req.url).pathname;
function rewrite(req, res, next) {
var match = path.match(/^\/blog\/posts\/(.+)/);
// 只针对 /blog/posts 请求执行查找
if (match) {
findPostIdBySlug(match[1], function (err, id) {
if (err) {
return next(err);
}
if (!id) {
return next(new Error('User not found'));
}
req.url = '/blog/posts/' + id; // 重写req.url属性,以便后续中间件可以使用真实的ID
next();
});
} else {
next();
}
}
六、使用错误处理中间件
Connect刻意将错误处理做到最简,让开发人员指明应该如何处理错误。
1. Connect 的默认错误处理器
var connect = require('connect');
connect().use(function hello(req, res) {
foo(); // ---> ReferrenceError
res.setHeader('Content-Type', 'text/plain');
res.end('hello world');
}).listen(3000);
// 500, 'Internal Server Error'
2. 自行处理程序错误
// Connect 中的错误处理中间件
function errorHandler() {
var env = process.env.NODE_ENV || 'development';
// 错误处理中间件定义四个参数
return function (err, req, res, next) {
res.statusCode = 500;
switch (env) {
case 'development':
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(err));
break;
default:
res.end('Server error');
break;
}
}
}
// 使用错误处理
connect()
.use(router(require('./routes/user'))) // 有错误会跳过
.use(router(require('./routes/admin'))) // 有错误会跳过
.use(errorHandler())
.listen(3000);
用 NODE_ENV 设定程序的模式:Connect 通常是用环境变量 NODE_ENV(process.env.NODE_ENV)在不同的服务器环境之间切换,比如生产和开发环境。
3. 使用多个错误处理中间件组件
用中间件的变体做错误处理对于将错误处理问题分离出来很有帮助。