Connect 源码阅读

214 阅读5分钟

PK创意闹新春,我正在参加「春节创意投稿大赛」,详情请看:春节创意投稿大赛

介绍

Connect是一个基于Node可扩展的的HTTP服务器中间件框架, 通过以“中间件”的形式处理请求;

使用方法

安装Connect

yarn add connect

创建app

创建的app应用,将会存储后续添加的所有的中间件, 并且 connect() 返回的是一个函数,因为原生的Node的HTTP的API的createServer的参数就是一个函数

const connect = require('connect')
const app = connect()
// const app = require('connect')()

使用中间件

  1. 使用第三方中间件 比如: 引用 body-parser中间件, body-parser 是一个对请求的req.body进行处理的中间件
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
  1. 自定义中间件
const app = require('connect')()
// 使用中间件
app.use(function middleWare1(req, res, next) {
  // 处理 用户的请求 和 响应
  next()
})
app.use(function middleWare2(req, res, next) {
  next()
})
app.use(function(req, res){
  res.end('<h1>Hello word</h1>')
})
  1. 从app创建服务
// 第一种
const http = require('http')
http.createServer(app).listen(4000)
// 第二种
app.listen(4000)

第二种方式 其实也是 对第一种方式的封装

源码阅读

源码地址: Connect源码地址

引入依赖

// 第16 —— 21行 引入依赖
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');
依赖名称用途来源
debug轻量级的调试工具第三方
finalhandler作为响应的最后一步,触发HTTP的Response第三方
utils-merge合并第三方
parseUrl转译URL第三方
events事件机制Node
http创建HTTP服务Node
// 第35 —— 36行 
var env = process.env.NODE_ENV || 'development'; // 环境变量
var proto = {};
// 第39 —— 40行 延迟函数, 使用 setImmediate 或者 process.nextTick
var defer = typeof setImmediate === 'function'
  ? setImmediate
  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }

创建应用(createServer函数)

// 第28行 模块导出
module.exports = createServer;
// 第50 —— 57行 创建应用 并且返回 
function createServer() {
  function app(req, res, next){ app.handle(req, res, next); } // 当服务器有请求时
  merge(app, proto);
  merge(app, EventEmitter.prototype); 
  app.route = '/'; // 路由 
  app.stack = []; // 中间件
  return app;
}

配置中间件(app.use)

// 第76 —— 110行
proto.use = function use(route, fn) {
  var handle = fn;
  var path = route;

  // default route to '/' 
  // 如果 route 不为空的话, 那么 将 '/' 设置为 默认路径
  if (typeof route !== 'string') {
    handle = route;
    path = '/';
  }

  // wrap sub-apps
  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
  if (handle instanceof http.Server) {
    handle = handle.listeners('request')[0];
  }

  // strip trailing slash
  // 去掉最后一个 '/'
  if (path[path.length - 1] === '/') {
    path = path.slice(0, -1);
  }

  // add the middleware
  debug('use %s %s', path || '/', handle.name || 'anonymous');
  this.stack.push({ route: path, handle: handle });
  
  return this;
};

app.use的函数 将传入的参数 最终 处理成 { route: path, handle: handle }追加到 app.stack数组中, 参数如下:

  1. app.use(route, fn)
app.use('/a', (req, res, next) => {
    next()
})
  1. app.use(fn)
app.use((req, res, next) => {
    next()
})
  1. app.use({ route, handle })
app.use({ handle: (req, res, next) => {
    next()
}})
  1. app.use(http.Server的实例)
const connect = require('connect')
const app = connect()
const http = require('http')
const server = http.createServer((req, res) => {
    res.end()
})
app.use(server)

server.listeners(eventName): 返回 名为 eventName 的事件的 数组拷贝

备注

const server = http.createServer(function a() {})
// 等价于
const server = http.createServer()
server.on('request', function a() {})

server.listeners('request') 就是拿到 ‘request’事件的所有的回调函数组成的数组

处理请求

// 第119 —— 187行
proto.handle = function handle(req, res, out) {
  var index = 0;
  var protohost = getProtohost(req.url) || '';
  var removed = '';
  var slashAdded = false;
  var stack = this.stack;

  // final function handler
  // 最终处理函数
  var done = out || finalhandler(req, res, {
    env: env,
    onerror: logerror
  });

  // store the original URL
  // 保存原始URL
  req.originalUrl = req.originalUrl || req.url;

  // next函数
  function next(err) {
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    if (removed.length !== 0) {
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // next callback
    var layer = stack[index++];

    // all done
    if (!layer) {
      defer(done, err);
      return;
    }

    // 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
    var c = path.length > route.length && path[route.length];
    if (c && c !== '/' && c !== '.') {
      return next(err);
    }

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

    // call the layer handle
    call(layer.handle, route, err, req, res, next);
  }

  next();
};

这边是Connect框架的核心, 他会根据请求的pathname, 去遍历MiddleWare Stack, 当 匹配的到的路由route === pathname 或者 routepathname路径上的一部分(换句话说就是 pathnameroute的子路由, 那么 回调函数就会触发,

举个例子: 访问路径 localhost:3000/a/b/cd

const app = require('connect')()
app.use('/a/b', function(req, res, next) {
  console.log('/a/b', '会触发, 因为/a/b/cd 是 /a/b的子路由')
  next()
})
app.use('/a/b/c', function(req, res, next) {
  console.log('/a/b/c', '不会触发, 因为 /a/b/c 与 /a/b/cd 不相等, 同时不是 /a/b/cd 也不是 /a/b/c 子路由')
  next()
})
app.use('/a/b/cd', function(req, res) {
  console.log('/a/b/cd', '会触发, 因为匹配到了')
  res.end('/a/b/cd')
})
app.listen(3000)

image.png

重点 在我们看源码的时候,会看到如下的代码, c && c!=='/' && c!== '.' 也就是当 路径/a/b/c.d也被看到成 /a/b/c的子路由, 这样会调用 /a/b/c注册的中间件; /a/b/c.d -> /a/b/c

var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
  return next(err);
}
graph LR
/a/b/c.d(路径/a/b/c/d) -- 中间件 --> / -- 中间件 --> /a -- 中间件 --> /a/b -- 中间件 -->/a/b/c --> !(最终路由/a/b/c.d)
/a/b -- 不会到达 --> /a/b/d(/a/b/d)

通过app.listen直接创建应用

// 第215 —— 218行
proto.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

触发路由处理

// 第225 —— 249行
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) { // 如果有错,并且中间件的会掉函数是4个
      // error-handling middleware // 错误优先
      handle(err, req, res, next);
      return;
    } else if (!hasError && arity < 4) { // 如果没有错误, 并且中间件的 参数小于4个
      // request-handling middleware
      handle(req, res, next);
      return;
    }
  } catch (e) { // 捕获错误 
    // replace the error
    error = e;
  }

  // continue // 继续下一个 中间件
  next(error);
}

错误日志

// 第258 —— 260行
function logerror(err) {
  if (env !== 'test') console.error(err.stack || err.toString());
}

获取协议和主机

对传入的url,获取 协议和主机, 比如http://www.baid.com/ahha/12321 会返回 http://www.baid.com

// 第269 —— 279行
function getProtohost(url) {
  // 如果长度为0 或 或者 以 '/' 开头的字符串 直接返回 undefined
  if (url.length === 0 || url[0] === '/') { 
    return undefined;
  }
  var fqdnIndex = url.indexOf('://')
  return fqdnIndex !== -1 && url.lastIndexOf('?', fqdnIndex) === -1
    ? url.substr(0, url.indexOf('/', 3 + fqdnIndex))
    : undefined;
}
  1. String.prototype.lastIndexOf(search, fromIndex): 从指定的索引fromIndex开始向前查询search最后一次出现的索引

    1.1 fromIndex 大于 字符串的长度,fromIndex = s.length 1.2 fromIndex 小于 0, 等同于 fromIndex = 0 1.3 search 为空, 则返回 fromIndex 的值