在 Node.js 生态中,connect 是一个轻量级、可扩展的 HTTP 中间件框架。它虽然代码量不大(核心文件仅数百行),却奠定了 Express、Koa 等现代 Web 框架的中间件设计基础。理解 connect 的源码与设计思想,有助于掌握 Node.js HTTP 开发的底层模式。本文将从概念、使用方法、源码实现、中间件机制以及应用场景五个维度,对 connect 进行全面剖析。
Connect 是什么?
connect ,其定位是"Node.js 的中间件层 "。它本身不是一个完整的 Web 框架,而是一个可插拔的 HTTP 请求处理管道。开发者可以将各种功能(日志、静态文件、路由、代理等)以中间件的形式插入到管道中,按顺序处理请求。
connect 的核心概念:
- 中间件 :一个接受
(req, res, next)的函数,可以修改请求/响应、结束请求或调用下一个中间件。 - 中间件栈 :使用
use方法注册中间件,形成一个数组(栈),请求到来时依次执行。 - 错误处理 :通过
(err, req, res, next)形式的中间件捕获异常。
Connect 的核心是其维护的一个中间件队列(stack),通过use方法将中间件注册到队列中。每个中间件都是一个函数,它能够访问请求对象(req)、响应对象(res)以及控制权传递函数(next)。
中间件处理的核心在于next()函数:
- 调用
next():表示当前中间件已完成处理,将控制权传递给队列中的下一个中间件。 - 不调用
next():表示请求处理链终止,不再继续向下执行。
这种机制确保了每一个中间件只处理它负责的部分,实现了职责分离和灵活组合。
Connect 使用
1、初始化与基础设置
js
const connect = require("connect");
const http = require("http");
const app = connect();
const server = http.createServer(app);
server.listen(3000);
console.log("Server is running on port 3000");
2、不指定路径,中间件会对每个请求执行
js
app.use(function logger(req, res, next) {
const start = Date.now();
const originalUrl = req.url; // 保存原始 URL
// 监听响应结束事件(因为 res.end 是异步的)
res.on("finish", () => {
const duration = Date.now() - start;
console.log(`响应结束 ${req.method} ${originalUrl} - ${res.statusCode} - ${duration}ms`);
});
next();
});
3、路径匹配中间件
请求路径以 /user 开头时触发(如 /user、/user/profile)
js
app.use("/user", (req, res, next) => {
console.log("用户中间件");
res.setHeader("Content-Type", "text/plain");
res.end("User area");
});
4、子应用挂载
js
const adminApp = connect();
adminApp.use((req, res, next) => {
res.setHeader("Content-Type", "text/plain");
res.end("Admin area");
});
app.use("/admin", adminApp);
5、错误触发中间件
访问 /error 时主动抛出错误,用于测试错误处理中间件。
js
app.use("/error", (req, res, next) => {
throw new Error("Error");
});
5、404中间件
所有未匹配的请求都会返回 404 Not Found。
js
app.use((req, res) => {
res.statusCode = 404;
res.end('Not Found');
});
7、错误处理中间件
js
app.use((err, req, res, next) => {
console.error(err.stack);
res.statusCode = 500;
res.end("Internal Server Error");
});
源码 connect@3.7
js
/*!
* connect
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict';
/**
* Module dependencies.
* @private
*/
var debug = require('debug')('connect:dispatcher');
var EventEmitter = require('events').EventEmitter;
var finalhandler = require('finalhandler');
var http = require('http');
var merge = require('utils-merge');
var parseUrl = require('parseurl');
/**
* Module exports.
* @public
*/
module.exports = createServer;
/**
* Module variables.
* @private
*/
var env = process.env.NODE_ENV || 'development';
var proto = {};
/* istanbul ignore next */
var defer = typeof setImmediate === 'function'
? setImmediate
: function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
/**
* Create a new connect server.
* 用于创建一个新的 Connect 服务器实例。
* 这个函数返回一个具有中间件处理能力的函数,该函数可以作为 HTTP 服务器的请求处理器
*
* @return {function}
* @public
*/
function createServer() {
// 创建 app 函数
// 当 app 函数被调用时,它会调用自身的 handle 方法来处理请求
function app(req, res, next){ app.handle(req, res, next); }
// 将 proto 对象的属性合并到 app 函数上
merge(app, proto);
merge(app, EventEmitter.prototype);
app.route = '/';
app.stack = []; // 存储中间件函数
return app;
}
/**
* Utilize the given middleware `handle` to the given `route`,
* defaulting to _/_. This "route" is the mount-point for the
* middleware, when given a value other than _/_ the middleware
* is only effective when that segment is present in the request's
* pathname.
*
* For example if we were to mount a function at _/admin_, it would
* be invoked on _/admin_, and _/admin/settings_, however it would
* not be invoked for _/_, or _/posts_.
*
* @param {String|Function|Server} route, callback or server
* @param {Function|Server} callback or server
* @return {Server} for chaining
* @public
*/
// 向中间件栈中添加一个新的中间件
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// default route to '/'
// 函数重载:如果 route 参数不是字符串,说明是中间件函数,直接赋值给 handle 变量
// 不指定路径,中间件会对每个请求执行。
if (typeof route !== 'string') {
handle = route;
path = '/';
}
// wrap sub-apps
// 子应用包装(Sub-app)
// 传入的 handle 是一个 connect 应用实例(具有 handle 方法),则将其包装成一个中间件函数
if (typeof handle.handle === 'function') {
var server = handle; // 子应用实例
server.route = path;
// 中间件函数
handle = function (req, res, next) {
server.handle(req, res, next);
};
}
// wrap vanilla http.Servers
// HTTP 服务器适配
// 传入的 handle 是 Node.js 原生 http.Server 实例,则提取其 'request' 事件监听器(即第一个处理函数)作为中间件
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}
// strip trailing slash
// 移除路径末尾的斜杠,确保路径格式为 /admin 而不是 /admin/
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}
// add the middleware
debug('use %s %s', path || '/', handle.name || 'anonymous');
// 将中间件对象(包含 route 和 handle)推入 this.stack 数组
this.stack.push({ route: path, handle: handle });
return this;
};
/**
* Handle server requests, punting them down
* the middleware stack.
* 遍历中间件栈(this.stack),根据请求路径匹配中间件,并依次执行
*
* @private
*/
proto.handle = function handle(req, res, out) {
var index = 0; // 当前中间件在栈中的索引
// 请求 URL 中的协议+主机部分(如 http://example.com)
var protohost = getProtohost(req.url) || '';
var removed = ''; // 记录已被匹配并"剥离"的路由前缀
var slashAdded = false; // 标记是否因为路径变换而添加了前导斜杠
var stack = this.stack;
// final function handler
// 最终处理函数(默认 finalhandler),当所有中间件执行完或出错时调用
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
// 保存原始请求 URL
req.originalUrl = req.originalUrl || req.url;
function next(err) {
// 1、恢复 URL 变换
// 因为匹配路由而临时去掉了前导斜杠(slashAdded === true),则将其加回
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
// 之前剥离了路由前缀(removed 非空),则将其重新拼接到 req.url 前面
if (removed.length !== 0) {
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// next callback
// 2、取出当前中间件,索引自增
var layer = stack[index++];
// all done
// 如果已无中间件,则调用 done(可能传递错误),结束处理
if (!layer) {
defer(done, err);
return;
}
// 3、路径匹配检查
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// skip if route match does not border "/", ".", or end
// 4、边界符检查
var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
return next(err);
}
// 5、URL 变换(剥离路由前缀)
// trim off the part of the url that matches the route
if (route.length !== 0 && route !== '/') {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
}
// 6、调用中间件
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
/**
* Listen for connections.
*
* This method takes the same arguments
* as node's `http.Server#listen()`.
*
* HTTP and HTTPS:
*
* If you run your application both as HTTP
* and HTTPS you may wrap them individually,
* since your Connect "server" is really just
* a JavaScript `Function`.
*
* var connect = require('connect')
* , http = require('http')
* , https = require('https');
*
* var app = connect();
*
* http.createServer(app).listen(80);
* https.createServer(options, app).listen(443);
*
* @return {http.Server}
* @api public
*/
proto.listen = function listen() {
// 1、创建 HTTP 服务器实例
// this 指向 connect 应用实例
// http.createServer(this) 创建原生 HTTP 服务器,并将 app 作为请求监听器
// 这意味着每当有 HTTP 请求到达时,就会调用 app(req, res),从而进入 Connect 的中间件处理管道
var server = http.createServer(this);
// 2、启动 HTTP 服务器
return server.listen.apply(server, arguments);
};
/**
* Invoke a route handle.
* @private
*/
function call(handle, route, err, req, res, next) {
var arity = handle.length;
var error = err;
var hasError = Boolean(err);
debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
try {
if (hasError && arity === 4) {
// error-handling middleware
handle(err, req, res, next);
return;
} else if (!hasError && arity < 4) {
// request-handling middleware
handle(req, res, next);
return;
}
} catch (e) {
// replace the error
error = e;
}
// continue
next(error);
}
/**
* Log error using console.error.
*
* @param {Error} err
* @private
*/
function logerror(err) {
if (env !== 'test') console.error(err.stack || err.toString());
}
/**
* Get get protocol + host for a URL.
* 从 URL 字符串中提取协议和主机部分(protocol + host)
*
* @param {string} url
* @private
*/
function getProtohost(url) {
// 检查 URL 是否为空字符串或以 '/' 开头
// 这类 URL 通常是相对路径,不包含协议和主机信息
if (url.length === 0 || url[0] === '/') {
return undefined;
}
// 查找 URL 中的协议分隔符位置
var fqdnIndex = url.indexOf('://')
return fqdnIndex !== -1 && url.lastIndexOf('?', fqdnIndex) === -1
? url.substr(0, url.indexOf('/', 3 + fqdnIndex))
: undefined;
}