项目作者: liuduanyang

项目描述 :
解读nodejs的connect模块
高级语言: JavaScript
项目地址: git://github.com/liuduanyang/learn-connect.git
创建时间: 2017-11-23T14:48:18Z
项目社区:https://github.com/liuduanyang/learn-connect

开源协议:MIT License

下载


connect


导言:

connect模块是由TJ大神所写,TJ的另一个杰作 express框架在connect模块的基础上构建。模块源码十分精简,只有二百多行。

一、前期介绍(准备阶段)

为什么 connect ?

http创建服务器接收请求时,所有的响应都要写在一个回调函数里面,对于不同的请求路径,所返回的响应信息都是通过if和else来区分,所有的逻辑都是在一个函数中,当逻辑复杂起来会有各种回调,极容易出现问题,故有了让问题简单起来的connect中间件的产生,connect把所有的请求信息都拆分开,形成多个中间件,http请求就相当于是水流一样流过中间件,当路径相同时,就会响应该请求,否则就继续往下流,直到结束。

何为中间件?

中间件就是一个函数,该函数用来响应请求,可通过判断路径来决定是否执行。

connect 执行流程

  1. url请求---> 中间件A--->中间件B--->中间件C--->中间件D--->中间件E
  2. ^
  3. |
  4. 与中间件A配置的路由进行判断,相同则执行A函数,直至end函数被调用;
  5. 如果不同则继续匹配下一个中间件B,重复执行,直至匹配完所有中间件

二、源码解读

connect源码可分为六部分

  • 1.源码的准备阶段

引入模块依赖

  1. var debug = require('debug')('connect:dispatcher'); //用于调试代码
  2. var EventEmitter = require('events').EventEmitter; //用于触发响应事件
  3. var finalhandler = require('finalhandler'); //用于最后执行url请求
  4. var http = require('http'); //用于创建http服务
  5. var merge = require('utils-merge'); //用于继承(对象之间属性融合)
  6. var parseUrl = require('parseurl'); //用于解析url

暴露模块接口

  1. module.exports = createServer;

判断当前环境 根据是development或production 而做出不同的配置处理 development为默认环境

  1. var env = process.env.NODE_ENV || 'development';

声明proto对象

  1. var proto = {};

使用istanbul代码覆盖率测试工具时 忽略这段代码

  1. var defer = typeof setImmediate === 'function'
  2. ? setImmediate
  3. : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
  • 2.createServer用于创建一个 connect 实例(是一个函数对象)

-

  1. function createServer(){
  2. //创建一个app函数 接收三个参数 函数体为执行app函数对象的handle方法
  3. function app(req, res, next){ app.handle(req, res, next); }
  4. //app函数对象继承proto对象
  5. merge(app, proto);
  6. //app函数对象继承EventEmitter.prototype对象
  7. merge(app, EventEmitter.prototype);
  8. //为app函数对象添加route属性 存放路径地址
  9. app.route = '/';
  10. //为app函数对象添加stack属性 存放中间件
  11. app.stack = [];
  12. return app;
  13. }
  • 3.proto对象的use方法 用来添加中间件,在createServer函数中继承给了app函数对象

-

  1. proto.use = function use(route, fn) {
  2. var handle = fn;
  3. var path = route;
  4. // 如果第一个参数不是字符串类型 则将第一个参数传给第二个参数(中间件函数) 并设置路径默认为'/'
  5. if (typeof route !== 'string') {
  6. handle = route;
  7. path = '/';
  8. }
  9. //对fn可能的几种特殊情况进行判断
  10. // 如果fn也为一个中间件时,那么堆栈中存储的handle为这个子中间件的fn.handle()方法
  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类的实例时,那么handle为该httpServer的request事件的第一个监听者
  19. if (handle instanceof http.Server) {
  20. handle = handle.listeners('request')[0];
  21. }
  22. // 删除req.url末尾多余的'/'
  23. if (path[path.length - 1] === '/') {
  24. path = path.slice(0, -1);
  25. }
  26. // 添加这个中间件
  27. debug('use %s %s', path || '/', handle.name || 'anonymous');
  28. //将中间件函数和路径包裹成一个对象,并将其添加到用于存储中间件的stack数组(栈)中
  29. this.stack.push({ route: path, handle: handle });
  30. //this指该方法(use)的调用者
  31. return this;
  32. };
  • 4.proto对象的handle方法 用来操作发送给服务器的请求,让他们匹配stack栈中中间件

-

  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. // 设置最后一个响应request请求
  8. var done = out || finalhandler(req, res, {
  9. env: env,
  10. onerror: logerror
  11. });
  12. // 存储初始url地址
  13. req.originalUrl = req.originalUrl || req.url;
  14. /*
  15. next函数用来做流控制,即用来触发下一个中间件的回调函数,调用next()后,
  16. 程序会继续从app.stack堆栈中调用下一个中间件的回调函数
  17. */
  18. function next(err) {
  19. if (slashAdded) {
  20. req.url = req.url.substr(1);
  21. slashAdded = false;
  22. }
  23. if (removed.length !== 0) {
  24. req.url = protohost + removed + req.url.substr(protohost.length);
  25. removed = '';
  26. }
  27. // 访问下一个中间件对象
  28. var layer = stack[index++];
  29. // 错误处理
  30. if (!layer) {
  31. defer(done, err);
  32. return;
  33. }
  34. // 分别获取中间件的路由以及请求的资源地址
  35. var path = parseUrl(req).pathname || '/';
  36. var route = layer.route;
  37. // 如果二者不匹配 则调用下一个中间件
  38. if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
  39. return next(err);
  40. }
  41. // url处理
  42. var c = path.length > route.length && path[route.length];
  43. if (c && c !== '/' && c !== '.') {
  44. return next(err);
  45. }
  46. if (route.length !== 0 && route !== '/') {
  47. removed = route;
  48. req.url = protohost + req.url.substr(protohost.length + removed.length);
  49. if (!protohost && req.url[0] !== '/') {
  50. req.url = '/' + req.url;
  51. slashAdded = true;
  52. }
  53. }
  54. // 匹配成功则调用中间件
  55. call(layer.handle, route, err, req, res, next);
  56. }
  57. next();
  58. };
  • 5.proto对象的listen方法调用了http模块(也支持https模块)的createServer()和listen()方法

-

  1. proto.listen = function listen() {
  2. var server = http.createServer(this);
  3. return server.listen.apply(server, arguments);
  4. };
  • 6.call函数在handle方法中被调用

当发生错误且传递了4个参数时就会调用handle(err,req, res, next)这个函数,当没有发生错误且传递的参数小于4个时就会调用handle(req, res, next),这里的handle函数是传进来的一个函数

  1. function call(handle, route, err, req, res, next) {
  2. var arity = handle.length;
  3. var error = err;
  4. var hasError = Boolean(err);
  5. debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl);
  6. try {
  7. if (hasError && arity === 4) {
  8. // 调用带有错误对象的handle方法
  9. handle(err, req, res, next);
  10. return;
  11. } else if (!hasError && arity < 4) {
  12. // 正常调用handle方法
  13. handle(req, res, next);
  14. return;
  15. }
  16. } catch (e) {
  17. // 将错误对象赋值给error
  18. error = e;
  19. }
  20. // 调用next方法 并把error(e)错误对象传入
  21. next(error);
  22. }
  • 7.打印错误日志

-

  1. function logerror(err) {
  2. if (env !== 'test') console.error(err.stack || err.toString());
  3. }
  • 8.获取主机

-

  1. function getProtohost(url) {
  2. if (url.length === 0 || url[0] === '/') {
  3. return undefined;
  4. }
  5. //如果req.url中有'?'则将'?'所在的下标作为长度 否则将url.req的长度作为长度
  6. var searchIndex = url.indexOf('?');
  7. var pathLength = searchIndex !== -1
  8. ? searchIndex
  9. : url.length;
  10. //如果有'://'则从头开始截取一直到'://'之后出现'/'的位置
  11. var fqdnIndex = url.substr(0, pathLength).indexOf('://');
  12. return fqdnIndex !== -1
  13. ? url.substr(0, url.indexOf('/', 3 + fqdnIndex))
  14. : undefined;
  15. }