- Connect是一个框架,它使用被称为中间件的模块化组件,以可重用的方式实现Web程序中的逻辑。
- 在Connect中,中间件组件是一个函数,它拦截HTTP服务器提供的请求和响应对象,执行逻辑,然后或者结束响应,或者把它传递给下一个中间件组件。
- 在Connect中,你可以使用自己编写的中间件,但它也提供了几个常用的组件,可以用来做请求日志、静态文件服务、请求体解析、会话管理等。
- 对于想构建自己的高层Web框架的开发人员来说,Connect就像一个抽象层,因为Connect很容易扩展,在其上构建东西也很容易。
搭建一个 Connect 程序
- Connect创建的“程序”实际上是一个JavaScript函数,用来接收HTTP请求并把它派发给你指定的中间件。
- Connect的分派器如何工作,它依次调用所有附着的中间件组件,直到其中一个决定响应该请求。如果直到中间件列表末尾还没有组件决定响应,程序会用404作为响应。
最小的connect程序:
var connect = require('connect');
var app = connect();
app.listen(3000);
复制代码
Connect 的工作机制
在Connect中,中间件组件是一个JavaScript函数,按惯例会接受三个参数:一个请求对象,一个响应对象,还有一个通常命名为 next 的参数,它是一个回调函数,表明这个组件已经完成了它的工作,可以执行下一个中间件组件了。
继续使用前面那个Connect准程序,在其中构建两个简单的中间件层:
- 一个 logger 中间件组件将请求输出到控制台中;
- 一个 hello 中间件组件,用“hello world”响应请求。
做日志的中间件
- 假设你想创建一个日志文件来记录进入服务器的请求方法和URL。为此你需要创建一个函数,我们就叫它 logger 吧,它有三个参数:请求和响应对象,以及回调函数 next 。
- next 函数可以在中间件里调用,告诉分派器这个中间件已经完成了自己的任务,可以把控制权交给下一个中间件组件了。用回调函数,而不是从方法中返回,是为了可以在中间件组件里运行异步逻辑,这样分派器就只能等着前一个中间件组件完成后才会进入下一个中间件组件。用next() 处理中间件组件之间的流程是不错的机制。
对于 logger 中间件组件,你可以带着请求方法和URL调用 console.log() ,输出一些“GET/user/1”之类的东西,然后调用 next() 函数将控制权交给下一个组件:
function logger(req, res, next) {
console.log('%s %s', req.method, req.url);
next();
}
复制代码
要在程序中使用这个中间件,你可以调用. use() 方法,把中间件函数传给它:
var connect = require('connect');
var app = connect();
app.use(logger);
app.listen(3000);
复制代码
响应“hello world”的中间件
使用多个Connect中间件组件:
var connect = require('connect');
function logger(req, res, next) {
console.log('%s %s', req.method, req.url);
next();
}
function hello(req, res) {
res.setHeader('Content-Type', 'text/plain');
res.end('hello world');
}
connect()
.use(logger)
.use(hello)
.listen(3000);
复制代码
- 中间件组件 hello 的参数中没有 next 回调。因为这个组件结束了HTTP响应,从不需要把控制权交回给分派器。
- . use()的链式调用不是必须的
为什么中间件的顺序很重要
中间件什么时候不调用 next()
错误: hello 中间件组件在 logger 组件前面---logger中间件永远不会被调用
var connect = require('connect');
function logger(req, res, next) {
console.log('%s %s', req.method, req.url);
next();
}
function hello(req, res) {
res.setHeader('Content-Type', 'text/plain');
res.end('hello world');
}
var app = connect()
.use(hello)
.use(logger)
.listen(3000);
复制代码
用中间件的顺序执行认证
你可以按照对你有利的顺序安排中间件,比如在需要做认证时。几乎所有程序都会做认证。用户需要通过某种方式登录,而你需要防止没有登录的人访问某些内容。中间件的顺序可以帮你实现认证。
用中间件的位次限制文件访问
挂载中间件和服务器
Connect中有一个挂载的概念,这是一个简单而强大的组织工具,可以给中间件或整个程序定义一个路径前缀。
挂载将只对路径前缀(挂载点)内的请求调用中间件或程序。在后面的代码中,第二个和第三个user()调用中的第一个参数是字符串 '/admin' ,然后是中间件组件。这意味着这些组件只用于带有/admin前缀的请求。我们来看一下Connect中挂载中间件组件或服务器的语法。
下面是一段认证机制代码,测一测理解挂载中间件和服务器:
var connect = require('connect');
function logger(req, res, next) {
console.log('%s %s', req.method, req.url);
next();
}
function hello(req, res) {
res.setHeader('Content-Type', 'text/plain');
res.end('hello world');
}
function authenticateWithDatabase(user, pass, callback) {
var err;
if (user != 'tobi' || pass != 'ferret') {
err = new Error('Unauthorized');
}
callback(err);
}
function restrict(req, res, next) {
var authorization = req.headers.authorization;
if (!authorization) 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();
});
}
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;
}
}
connect()
.use(logger)
.use('/admin', restrict)
.use('/admin', admin)
.use(hello)
.listen(3000);
复制代码
测一测:
$ curl http://localhost
hello world
$ curl http://localhost/foo
hello world
$ curl http://localhost/admin/users
Error:Unauthorized
$ curl http://localhost
Error:Unauthorized
$ curl --user jane:ferret http://localhost/admin/users
['tobi', 'loki', 'jane']
复制代码
创建可配置中间件
- 如何创建更通用的、可重用的中间件。可重用是我们编写中间件的主要原因,并且我们会在这一节创建可以配置日志、路由请求、URL等的中间件。你只要额外做些配置就能在程序中重用这些组件,无需从头实现这些组件来适应你的特定程序。
- 为了向开发人员提供可配置的能力,中间件通常会遵循一个简单的惯例:用函数返回另一个函数(这是一个强大的JavaScript特性,通常称为闭包)。这种可配置中间件的基本结构看起来是这样的:
function setup(options){
<!--设置逻辑(中间件初始化)-->
return function(req,res,next){
<!--中间件逻辑(即使被外部函数返回,仍可访问options)-->
}
}
复制代码
这种中间件用法如下:
app.use(setup({sone:'options'}));
复制代码
下面构建三个可重用、可配置的中间件组件:
- 带有可配置的数据格式的 logger 组件;
- 基于所请求的URL调用函数的 router 组件;
- 将URL中的一段转换为ID的URL rewriter 组件。
创建可配置的 logger 中间件组件
使用:
var app=connect().use(logger(':method:url')).use(hello)
复制代码
定义:
function setup(format) {
var regexp = /:(\w+)/g;
return function logger(req, res, next) {
var str = format.replace(regexp, function(match, property){
return req[property];
});
console.log(str);
next();
}
}
module.exports = setup;
复制代码
构建路由中间件组件
使用:
var connect = require('connect');
var router = require('./middleware/router');
var routes = {
GET: {
'/users': function(req, res){
res.end('tobi, loki, ferret');
},
'/user/:id': function(req, res, id){
res.end('user ' + id);
}
},
DELETE: {
'/user/:id': function(req, res, id){
res.end('deleted user ' + id);
}
}
};
connect()
.use(router(routes))
.listen(3000);
复制代码
定义:
var parse = require('url').parse;
module.exports = function route(obj) {
return function(req, res, next){
if (!obj[req.method]) {
next();
return;
}
var routes = obj[req.method];
var url = parse(req.url);
var paths = Object.keys(routes);
for (var 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();
}
};
复制代码
因为程序里中间件的数量没有限制,中间件组件使用的次数也没有限制,所以在一个程序中有可能会定义几个路由器。这样可能更有利于组织。比如你既有跟用户相关的路由,也有跟管理员相关的路由。则可以把它们分到不同的模块文件中,在路由器组件中分别引入,代码如下所示:
构建一个重写URL的中间件组件
使用:
tip:在构建中间件时,你应该关注那些小型的、可配置的部分。构建大量微小的、模块化的、可重用的中间件组件,合起来搭成你的程序。保持中间件的小型化和专注性真的有助于将复杂的程序逻辑分解成更小的组成部分。
定义:
使用错误处理中间件
- 所有程序都有错误,不管在系统层面还是在用户层面。为错误状况,甚至是那些你没预料到的错误状况而未雨绸缪是明智之举。
- Connect按照常规中间件所用的规则实现了一种用来处理错误的中间件变体,除了请求和响应对象,还接受一个错误对象作为参数。
Connect的默认错误处理器
- 因为函数foo()没有定义,所以它会抛出错误ReferenceError。
- 默认情况下,Connect给出的响应是状态码500,包含文本“Internal Server Error”以及错误自身详细信息的响应主体。
自行处理程序错误
- 在Connect中,你还可以用错误处理中间件自行处理程序错误。
- 错误处理中间件函数必须接受四个参数: err 、 req 、 res 和 next ,而常规的中间件只有三个参数: req 、 res 和 next 。
var connect = require('connect');
function badMiddleware(req, res, next) {
next(new Error('Bad middleware makes error'));
}
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');
}
}
}
connect()
.use(badMiddleware)
.use(errorHandler)
.listen(3000);
复制代码
tip:主要触发错误,错误处理中间件就会触发,并跳过错误触发中间件之后的中间件的执行。
使用多个错误处理中间件组件
- 带有两个错误处理中间件组件的程序布局
现在来实现程序中的所有中间件组件:
- hello 组件会给出响应“Hello World\n.”;
- 如果用户不存在, users 组件会抛出一个 notFoundError ;
- 为了演示错误处理器, pets 会引发一个要抛出的 ReferenceError ;
- errorHandler 组件会处理来自 api 的所有错误;
- errorPage 主机会处理来自主程序 app 的所有错误。
var connect = require('connect');
function hello(req, res, next) {
if (req.url.match(/^\/hello/)) {
res.end('Hello World\n');
} else {
next();
}
}
var db = {
users: [
{ name: 'tobi' },
{ name: 'loki' },
{ name: 'jane' }
]
};
function users(req, res, next) {
var match = req.url.match(/^\/user\/(.+)/);
if (match) {
var user = db.users[match[1]];
if (user) {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(user));
} else {
var err = new Error('User not found');
err.notFound = true;
next(err);
}
} else {
next();
}
}
function pets(req, res, next) {
if (req.url.match(/^\/pet\/(.+)/)) {
foo();
} else {
next();
}
}
function errorHandler(err, req, res, next) {
console.error(err.stack);
res.setHeader('Content-Type', 'application/json');
if (err.notFound) {
res.statusCode = 404;
res.end(JSON.stringify({ error: err.message }));
} else {
res.statusCode = 500;
res.end(JSON.stringify({ error: 'Internal Server Error' }));
}
}
connect()
.use(users)
.use(pets)
.use(errorHandler)
.use(hello)
.listen(3000);
复制代码
- errorPage 组件是这个例子中的第二个错误处理组件。因为前一个错误处理组件从来没调用过 next(err) ,所以这个组件只有在 hello 组件中出现错误时才会被调用。
- 那个组件是最不可能出现错误的,因此这个 errorPage 被调用的几率也很小。它对这个例子来说可有可无。