Connect 深度解析:Node.js 中间件框架的基石

24 阅读7分钟

在 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、初始化与基础设置

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、不指定路径,中间件会对每个请求执行

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

app.use("/user", (req, res, next) => {
  console.log("用户中间件");
  res.setHeader("Content-Type", "text/plain");
  res.end("User area");
});

4、子应用挂载

const adminApp = connect();

adminApp.use((req, res, next) => {
  res.setHeader("Content-Type", "text/plain");
  res.end("Admin area");
});
app.use("/admin", adminApp);

5、错误触发中间件

访问 /error 时主动抛出错误,用于测试错误处理中间件。

app.use("/error", (req, res, next) => {
  throw new Error("Error");
});

5、404中间件

所有未匹配的请求都会返回 404 Not Found

app.use((req, res) => {
  res.statusCode = 404;
  res.end('Not Found');
});

7、错误处理中间件

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.statusCode = 500;
  res.end("Internal Server Error");
});

源码 connect@3.7

/*!
 * 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;
}