简介

Connect](https://github.com/senchalabs/connect) 是一个可扩展(中间件作为插件)的 Http 服务器框架,Connect 刚出道之时自带了许多中间件,为保证其框架的轻量级以及扩展性,最终还是将这些中间件的实现抛给了社区。可能在搜索 Connect 的相关项目时,你会发现 connect().use(connect.bodyParser())这些的写法,这对于现在的 Connect (最新版本3.6.0) 是不支持的,而只能通过 npm 下载第三方的模块 (如 body-parser) 替代原先的中间价。

基本使用

  1. const connect = require('connect');
  2. var createError = require('http-errors');
  3. var http = require('http');
  4. var app = connect()
  5. app.use('/', function(req, res, next) {
  6. res.writeHead(200,'OK',{
  7. //'content-type': 'text/plain' //纯文本
  8. 'content-type': 'text/html;charset=utf-8'
  9. })
  10. res.write('<h1>你好,欢迎学习connect</h1>')
  11. res.end()
  12. })
  13. // catch 404 and forward to error handler
  14. app.use(function(req, res, next) {
  15. next(createError(404));
  16. });
  17. // error handler
  18. app.use(function(err, req, res, next) {
  19. res.statusCode = 404;
  20. res.end('Not Found');
  21. });
  22. // 两种方式监听指定端口
  23. app.listen(3000) // 这种方式在connect里面调用了http.createServer方法
  24. // http.createServer(app).listen(3000);

源码分析

这篇文章是基于"version": "3.6.6"这个版本来对源码进行分析的,这个版本所有的代码都在index.js文件中。下面我们来看一下connect是怎么工作的。

首先我们看一下这个文件的模块出口,可以看到导出了一个createServer函数,这个函数最终返回的是app,app本身是一个函数,在下面一段代码中我们可以的看到它是作为了request事件的处理函数。同时它既继承了proto、EventEmitter属性和方法,也有自己的route、stack属性。

  1. // 模块出口
  2. module.exports = createServer;
  3. // 判断当前环境,初始化proto 对象
  4. var env = process.env.NODE_ENV || 'development';
  5. var proto = {};
  6. function createServer() {
  7. // app 函数对象,可以添加属性和方法
  8. function app(req, res, next){
  9. app.handle(req, res, next);
  10. }
  11. // merge: 相当于Object.assign
  12. merge(app, proto); // 继承proto的属性和方法
  13. merge(app, EventEmitter.prototype); // 继承EventEmitter(事件派发器)属性和方法
  14. app.route = '/';
  15. app.stack = []; // 存放中间件的数组,中间件会被格式化成形为{route: route , handle : fn}的匿名对象存放
  16. return app;
  17. }
  18. // 监听指定端口号
  19. proto.listen = function listen() {
  20. var server = http.createServer(this); // this ——> app函数对象,作为request事件的处理函数
  21. return server.listen.apply(server, arguments); // 从 arguments 拿到端口号
  22. };

connect框架的核心是use、handle、call三个方法,我们来分析一下这三个方法分别有什么作用。

use方法:添加中间件

  1. // 添加中间件
  2. proto.use = function use(route, fn) {
  3. var handle = fn;
  4. var path = route;
  5. // 如果参数只有一个,那么path默认是 '/', 传入的参数作为处理函数
  6. if (typeof route !== 'string') {
  7. handle = route;
  8. path = '/';
  9. }
  10. // 如果fn为一个app的实例,则将其自身handle方法的包裹给fn
  11. if (typeof handle.handle === 'function') {
  12. var server = handle;
  13. server.route = path;
  14. handle = function (req, res, next) {
  15. server.handle(req, res, next);
  16. };
  17. }
  18. // 如果fn为一个http.Server实例,则fn为其request事件的第一个监听器
  19. if (handle instanceof http.Server) {
  20. handle = handle.listeners('request')[0];
  21. }
  22. // 如果route参数的以 '/' 结尾,则删除 '/'
  23. if (path[path.length - 1] === '/') {
  24. path = path.slice(0, -1);
  25. }
  26. // 把中间件添加到stack数组中
  27. debug('use %s %s', path || '/', handle.name || 'anonymous');
  28. this.stack.push({ route: path, handle: handle });
  29. // 返回自身,以便继续链式调用
  30. return this;
  31. };

handle方法:根据当前路径找到stack中所有与之相匹配的中间件,通过call方法调用中间件处理函数

  1. proto.handle = function handle(req, res, out) {
  2. var index = 0;
  3. var protohost = getProtohost(req.url) || '';
  4. var removed = '';
  5. var slashAdded = false;
  6. var stack = this.stack;
  7. // final function handler
  8. var done = out || finalhandler(req, res, {
  9. env: env,
  10. onerror: logerror
  11. });
  12. // store the original URL
  13. req.originalUrl = req.originalUrl || req.url;
  14. // 调用next方法传递的err信息,可以在下一个中间件处理函数的err参数中获取到
  15. function next(err) {
  16. if (slashAdded) {
  17. req.url = req.url.substr(1);
  18. slashAdded = false;
  19. }
  20. if (removed.length !== 0) {
  21. req.url = protohost + removed + req.url.substr(protohost.length);
  22. removed = '';
  23. }
  24. // 取出第一个中间件,index+1,再次调用取出第二个中间件....
  25. var layer = stack[index++];
  26. // all done
  27. if (!layer) {
  28. defer(done, err);
  29. return;
  30. }
  31. // route data
  32. var path = parseUrl(req).pathname || '/';
  33. var route = layer.route;
  34. // skip this layer if the route doesn't match
  35. if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
  36. return next(err);
  37. }
  38. // skip if route match does not border "/", ".", or end
  39. var c = path.length > route.length && path[route.length];
  40. if (c && c !== '/' && c !== '.') {
  41. return next(err);
  42. }
  43. // trim off the part of the url that matches the route
  44. if (route.length !== 0 && route !== '/') {
  45. removed = route;
  46. req.url = protohost + req.url.substr(protohost.length + removed.length);
  47. // ensure leading slash
  48. if (!protohost && req.url[0] !== '/') {
  49. req.url = '/' + req.url;
  50. slashAdded = true;
  51. }
  52. }
  53. // 执行handler中匹配到的中间件
  54. call(layer.handle, route, err, req, res, next);
  55. }
  56. next();
  57. };

call方法::执行handler中匹配到的中间件

  1. function call(handle, route, err, req, res, next) {
  2. // handle函数的参数个数(3个参数为一般中间件,4个参数为错误处理中间件)
  3. // next参数接收的是上面定义的next函数,然后传入到中间件的handle函数中,handle函数同样通过next参数接 收,所以在中间件中调用next后会继续执行下一个中间件
  4. var arity = handle.length;
  5. var error = err;
  6. var hasError = Boolean(err);
  7. debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
  8. try {
  9. if (hasError && arity === 4) {
  10. // 执行错误处理中间件
  11. handle(err, req, res, next);
  12. return;
  13. } else if (!hasError && arity < 4) {
  14. // 执行一般中间件
  15. handle(req, res, next);
  16. return;
  17. }
  18. } catch (e) {
  19. // replace the error
  20. error = e;
  21. }
  22. // continue
  23. next(error);
  24. }

connect运行过程

通过下面的这张图,总结一下connect工作流程,app.use方法负责把中间件添加到stack数组中,中间件会被格式化成形为{route: route , handle : fn}的匿名对象存放 ;app.handle方法根据当前路径找到stack中所有与之相匹配的中间件,并通过call方法调用中间件处理函数 ;app.call方法根据handle函数的参数个数(3个参数为一般中间件,4个参数为错误处理中间件) 来执行中间件,并把接收的next函数传给中间件。

Connect 实践与源码分析 - 图1

完整源码

  1. 'use strict';
  2. // 引入依赖
  3. var debug = require('debug')('connect:dispatcher');
  4. var EventEmitter = require('events').EventEmitter;
  5. var finalhandler = require('finalhandler');
  6. var http = require('http');
  7. var merge = require('utils-merge');
  8. var parseUrl = require('parseurl');
  9. // 模块出口
  10. module.exports = createServer;
  11. // 判断当前环境,初始化proto 对象
  12. var env = process.env.NODE_ENV || 'development';
  13. var proto = {};
  14. var defer = typeof setImmediate === 'function'
  15. ? setImmediate
  16. : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
  17. function createServer() {
  18. // app 函数对象,可以添加属性和方法
  19. function app(req, res, next){
  20. app.handle(req, res, next);
  21. }
  22. // 相当于Object.assign
  23. merge(app, proto); // 继承proto的属性和方法
  24. merge(app, EventEmitter.prototype); // 继承EventEmitter(事件派发器)属性和方法
  25. app.route = '/';
  26. app.stack = []; // 存放中间件的数组,中间件会被格式化成形为{route: route , handle : fn}的匿名对象存放
  27. return app;
  28. }
  29. // 监听指定端口号
  30. proto.listen = function listen() {
  31. var server = http.createServer(this); // this ——> app函数对象,作为request事件的处理函数
  32. return server.listen.apply(server, arguments); // 从 arguments 拿到端口号
  33. };
  34. // 添加中间件
  35. proto.use = function use(route, fn) {
  36. var handle = fn;
  37. var path = route;
  38. // 如果参数只有一个,那么path默认是 '/', 传入的参数作为处理函数
  39. if (typeof route !== 'string') {
  40. handle = route;
  41. path = '/';
  42. }
  43. // 如果fn为一个app的实例,则将其自身handle方法给fn
  44. if (typeof handle.handle === 'function') {
  45. var server = handle;
  46. server.route = path;
  47. handle = function (req, res, next) {
  48. server.handle(req, res, next);
  49. };
  50. }
  51. // 如果fn为一个http.Server实例,则fn为其request事件的第一个监听器
  52. if (handle instanceof http.Server) {
  53. handle = handle.listeners('request')[0];
  54. }
  55. // 如果route参数的以 '/' 结尾,则删除 '/'
  56. if (path[path.length - 1] === '/') {
  57. path = path.slice(0, -1);
  58. }
  59. // 把中间件添加到stack数组中
  60. debug('use %s %s', path || '/', handle.name || 'anonymous');
  61. this.stack.push({ route: path, handle: handle });
  62. // 返回自身,以便继续链式调用
  63. return this;
  64. };
  65. // 这个函数作用是根据当前路径找到stack中所有与之相匹配的中间件,并通过call方法调用中间件处理函数
  66. proto.handle = function handle(req, res, out) {
  67. var index = 0;
  68. var protohost = getProtohost(req.url) || '';
  69. var removed = '';
  70. var slashAdded = false;
  71. var stack = this.stack;
  72. // final function handler
  73. var done = out || finalhandler(req, res, {
  74. env: env,
  75. onerror: logerror
  76. });
  77. // store the original URL
  78. req.originalUrl = req.originalUrl || req.url;
  79. function next(err) {
  80. if (slashAdded) {
  81. req.url = req.url.substr(1);
  82. slashAdded = false;
  83. }
  84. if (removed.length !== 0) {
  85. req.url = protohost + removed + req.url.substr(protohost.length);
  86. removed = '';
  87. }
  88. // next callback
  89. var layer = stack[index++];
  90. // all done
  91. if (!layer) {
  92. defer(done, err);
  93. return;
  94. }
  95. // route data
  96. var path = parseUrl(req).pathname || '/';
  97. var route = layer.route;
  98. // skip this layer if the route doesn't match
  99. if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
  100. return next(err);
  101. }
  102. // skip if route match does not border "/", ".", or end
  103. var c = path.length > route.length && path[route.length];
  104. if (c && c !== '/' && c !== '.') {
  105. return next(err);
  106. }
  107. // trim off the part of the url that matches the route
  108. if (route.length !== 0 && route !== '/') {
  109. removed = route;
  110. req.url = protohost + req.url.substr(protohost.length + removed.length);
  111. // ensure leading slash
  112. if (!protohost && req.url[0] !== '/') {
  113. req.url = '/' + req.url;
  114. slashAdded = true;
  115. }
  116. }
  117. // 通过call方法调用中间件的handle函数处理对应的路由
  118. call(layer.handle, route, err, req, res, next);
  119. }
  120. next();
  121. };
  122. function call(handle, route, err, req, res, next) {
  123. // handle函数的参数个数(3个参数为一般中间件,4个参数为错误处理中间件)
  124. // next参数接收的是上面定义的next函数,然后传入到中间件的handle函数中,handle函数同样通过next参数接收,所以在中间件中调用next后会继续执行下一个中间件
  125. var arity = handle.length;
  126. var error = err;
  127. var hasError = Boolean(err);
  128. debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
  129. try {
  130. if (hasError && arity === 4) {
  131. // 执行错误处理中间件
  132. handle(err, req, res, next);
  133. return;
  134. } else if (!hasError && arity < 4) {
  135. // 执行一般中间件
  136. handle(req, res, next);
  137. return;
  138. }
  139. } catch (e) {
  140. // replace the error
  141. error = e;
  142. }
  143. // continue
  144. next(error);
  145. }
  146. function logerror(err) {
  147. if (env !== 'test') console.error(err.stack || err.toString());
  148. }
  149. function getProtohost(url) {
  150. if (url.length === 0 || url[0] === '/') {
  151. return undefined;
  152. }
  153. var searchIndex = url.indexOf('?');
  154. var pathLength = searchIndex !== -1 ? searchIndex : url.length;
  155. var fqdnIndex = url.substr(0, pathLength).indexOf('://');
  156. return fqdnIndex !== -1 ? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) : undefined;
  157. }