express源码分析

Express是一基于Node的一个框架,用来快速创建Web服务的一个工具,为什么要使用Express呢,因为创建Web服务如果从Node开始有很多繁琐的工作要做,而Express为你解放了很多工作,从而让你更加关注于逻辑业务开发。举个例子:

创建一个很简单的网站:

1. 使用Node来开发:

复制代码

var http = require('http');
var url = require("url");

http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-Type': 'text/plain'
    });
    var url_str = url.parse(req.url,true);
    res.end('Hello World\n' + url_str.query);
}).listen(8080, "127.0.0.1");
console.log('Server running at http://127.0.0.1:8080/');

复制代码

这是一个简单的 hello world,运行以后访问http://127.0.0.1会打印相关字符串,这是最普通的页面,但实际上真正的网站要比这个复杂很多,主要有:

(1) 多个页面的路由功能

(2) 对请求的逻辑处理

那么使用node原生写法就要进行以下处理

复制代码

// 加载所需模块
var http = require("http");

// 创建Server
var app = http.createServer(function(request, response) {
  if(request.url == '/'){
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Home Page!\n");
  } else if(request.url == '/about'){
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("About Page!\n");
  } else{
    response.writeHead(404, { "Content-Type": "text/plain" });
    response.end("404 Not Found!\n");
  }
});

// 启动Server
app.listen(1984, "localhost");

复制代码

代码里在createServer函数里传递一个回调函数用来处理http请求并返回结果,在这个函数里有两个工作要做:

(1)路由分析,对于不同的路径需要进行分别处理

(2)逻辑处理和返回,对某个路径进行特别的逻辑处理

在这里会有什么问题?如果一个大型网站拥有海量的网站(也就是路径),每个网页的处理逻辑也是交错复杂,那这里的写法会非常混乱,没法维护,为了解决这个问题,TJ提出了Connect的概念,把Java里面的中间件概念第一次进入到JS的世界,Web请求将一个一个经过中间件,并通过其中一个中间件返回,大大提高了代码的可维护性和开发效率。

复制代码

// 引入connect模块
var connect = require("connect");
var http = require("http");
 
// 建立app
var app = connect();
 
// 添加中间件
app.use(function(request, response) {
    response.writeHead(200, { "Content-Type": "text/plain" });
    response.end("Hello world!\n");
});
// 启动应用 http.createServer(app).listen(1337);

复制代码

但是TJ认为还应该更好一点,于是Express诞生了,通过Express开发以上的例子:

2. 使用Express来开发:

复制代码

var express = require('express');
var app = express();

app.get('/', function (req, res) {
  res.send('Hello World!');
});
app.get('/about', function (req, res) {
  res.send('About');
});
var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; console.log('Example app listening at http://%s:%s', host, port); });

复制代码

从Express例子可以看出,使用Express大大减少了代码函数,而且逻辑更为简洁,所以使用Express可以提高开发效率并降低工程维护成本。

首先Express有几个比较重要的概念:路由,中间件和模版引擎

开发人员可以为Web页面注册路由,将不同的路径请求区分到不同的模块中去,从而避免了上面例子1所说的海量路径问题,例如

复制代码

var express = require("express");
var http = require("http");
var app = express();
 
app.all("*", function(request, response, next) {
    response.writeHead(404, { "Content-Type": "text/plain" });
    next();
});
 
app.get("/", function(request, response) {
    response.end("Welcome to the homepage!");
});
 
app.get("/about", function(request, response) {
    response.end("Welcome to the about page!");
});
 
app.get("*", function(request, response) {
    response.end("404!");
});
 
http.createServer(app).listen(1337);

复制代码

开发人员可以为特定的路由开发中间件模块,中间件模块可以复用,从而解决了复杂逻辑的交错引用问题,例如

复制代码

var express = require('express');
var app = express();

// 没有挂载路径的中间件,应用的每个请求都会执行该中间件
app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

// 挂载至 /user/:id 的中间件,任何指向 /user/:id 的请求都会执行它
app.use('/user/:id', function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

// 路由和句柄函数(中间件系统),处理指向 /user/:id 的 GET 请求
app.get('/user/:id', function (req, res, next) {
  res.send('USER');
});


var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

复制代码

同时Express对Request和Response对象进行了增强,添加了很多工具函数。

其中路由和中间件还有很多细节问题,可以参考http://www.expressjs.com.cn/来学习

下面我们来看看Express的工作原理

我们首先来看看Express的源码结构:

 

 

 

 

 

 

 

 

 

 

简单介绍下:

Middleware:中间件 

init.js 初始化request,response

query.js 格式化url,将url中的rquest参数剥离, 储存到req.query中

Router:路由相关 

index.js: Router类,用于存储中间件数组

layer.js 中间件实体类

route.js route类,用于处理不同Method

 

Application.js 对外API

Express.js 入口

Request.js 请求增强

Response.js 返回增强

Utils.js 工具函数

View.js 模版相关

现在看不明白没关系,可以先看看后面的解释然后再回头看就明白了:

我们前面有说道路由和中间件,那么我们就需要有地方来保存这些信息,比如路由信息,比如中间件回调函数等等,express中有一个对象Router对象专门用来存储中间件对象,他有一个数组叫stack,保存了所有的中间件对象,而中间件对象是Layer对象。

Router对象就是router/index.js文件,他的代码是:

Router对象的主要作用就是存储中间件数组,对请求进行处理等等。

 

 

Layer对象在router/layer.js文件中,是保存中间件函数信息的对象,主要属性有:

源码见:

这里面的细节先不多考虑,只需要了解关键的信息path,handler和route

handler是保存中间件回调函数的地方,path是路由的url,route是一个指针,指向undefined或者一个route对象,为何会有两种情况呢,是因为中间件有两种类型:

(1)普通中间件:普通中间件就是不管是什么请求,只要路径匹配就执行回调函数

(2)路由中间件:路由中间件就是区分了HTTP请求的类型,比如get/post/put/head 等等(有几十种)类型的中间件,就是说还有区分请求类型才执行。

所以有两种Layer,一种是普通中间件,保存了name,回调函数已经undefined的route变量。

另外一种是路由中间件,除了保存name,回调函数,route还会创建一个route对象:

route对象在router/route.js文件中,

我们看到route对象有path变量,一个methods对象,也有一个stack数组,stack数组其实保存的也是Layer对象,这个Layer对象保存的是对于不同HTTP方法的不同中间件函数(handler变量)。

也许你会问,这个route的数组里面的Layer和上面router的数组里面的Layer有何不同,他们有一些相同之处也有一些不同之处,主要是因为他们的作用不同:

相同之处:他们都是保存中间件的实例对象,当请求匹配到指定的中间件时,该对象实例将会触发。

不同之处:

Router对象的Layer对象有route变量,如果为undefined表示为普通中间件,如果指向一个route对象表示为路由中间件,没有method对象。而route对象的Layer实例是没有route变量的,有method对象,保存了HTTP请求类型。

所以Router对象中的Layer对象是保存普通中间件的实例或者路由中间件的路由,而route对象中的Layer是保存路由中间件的真正实例。

我们来看个例子,加入有段设置路由器的代码:

app.use("/index.html",function(){ //此处省略一万行代码});

app.use("/contract.html",function(){ //此处省略一万行代码});
app.get("/index.html",function(){ //此处省略一万行代码});
app.post("/index.html",function(){ //此处省略一万行代码});
app.get("/home.html",function(){ //此处省略一万行代码});

代码中注册了2个普通中间件about.html和contract.html,两个路由中间件,index.html和home.html,对index.html有get和post两种中间件函数,对home.html只有get中间件函数,在内存中存储的形式就是:

 

我们上面看到了几种注册中间件的方式,下面就来介绍下路由器的几个动作逻辑:

 route对象:

router.METHOD(path,callback);//METHOD是HTTP请求方法(get/post等),他的实现过程在这里:

methods变量是一个数组包含了几十个http请求类型,这段代码给route对象添加了几十个方法,主要逻辑就是创建一个Layer对象,保存中间件函数对象和Method方法,添加到route的stack数组中去。

我们再来看看Router对象的方法:

复制代码

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate router.use([fn])
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var callbacks = flatten(slice.call(arguments, offset));

  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires middleware functions');
  }

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
    }

    // add the middleware
    debug('use %s %s', path, fn.name || '<anonymous>');

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    this.stack.push(layer);
  }

  return this;
};

复制代码

这个就是app.use的实现方法,实际上app.use就是调用了router.use,后面详细介绍,先看看这个方法做了什么,当我们调用app.use(function(){XXX});的时候,这里的函数首先判断了参数类型,看看有没有path传递进来,没有path就是"/"有的话保存到path变量,然后对后面的所有中间件函数进行了以下处理:

创建了一个layer对象,保存了路径,中间件函数并且设置了route变量为undefined,最后把这个变量保存到router的stack数组中去,到此一个普通中间件函数创建完成,为何要设置route变量为undefined,因为app.use创建的中间件肯定是普通中间件,app.METHOD创建的才是路由中间件。

当我面调用app.get("",function(){XXX})的时候调用的其实是router对象的route方法:

复制代码

proto.route = function route(path) {
  var route = new Route(path);

  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);
  return route;
};

复制代码

route方法也创建了一个layer对象,但是因为本身是路由中间件,所以还会创建一个route对象,并且保存到layer的route变量中去。

现在我们总结一下:

1. route对象的主要作用是创建一个路由中间件,并且创建多个方法的layer保存到自己的stack数组中去。

2. router对象的主要作用是创建一个普通中间件或者路由中间件的引导着(这个引导着Layer对象链接到一个route对象),然后将其保存到自己的stack数组中去。

所以route对象的stack数组保存的是中间件的方法的信息(get,post等等)而router对象的stack数组保存的是路径的信息(path)

好了,说完了这些基础组件,下面说一下真正暴露给开发者的对外接口,很显然刚才说的都是内部实现细节,我们开发者通常不需要了解这些细节,只需要使用application提供的对外接口。

 

application在application.js文件下,主要保存了一些配置信息和配置方法,然后是一些对外操作接口,也就是我们说的app.use,app.get/post等等,有几个重要的方法:

复制代码

app.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate app.use([fn])
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var fns = flatten(slice.call(arguments, offset));

  if (fns.length === 0) {
    throw new TypeError('app.use() requires middleware functions');
  }

  // setup router
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    // non-express app
    if (!fn || !fn.handle || !fn.set) {
      return router.use(path, fn);
    }

    debug('.use app under %s', path);
    fn.mountpath = path;
    fn.parent = this;

    // restore .app property on req and res
    router.use(path, function mounted_app(req, res, next) {
      var orig = req.app;
      fn.handle(req, res, function (err) {
        req.__proto__ = orig.request;
        res.__proto__ = orig.response;
        next(err);
      });
    });

    // mounted an app
    fn.emit('mount', this);
  }, this);

  return this;
};

复制代码

我们看到app.use在进行了一系列的参数处理后,最终调用的是router的use方法创建一个普通中间件。

复制代码

methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

复制代码

同route一样,将所有的http请求的方法创建成函数添加到application对象中去,从而可以使用app.get/post/等等,最终的效果是调用router的route方法创建一个路由中间件。

所有的方法再通过express入口文件暴露在对外接口中去。而middleware中的两个文件是对application做的一些初始化操作,request.js和response.js是对请求的两个对象的一些增强。

 

express的中间件中第三个参数next,关于next主要从三点来进行说明:

  1. next的作用是什么?
  2. 我们应该在何时使用next?
  3. next的内部实现机制是什么?

Next的作用

我们在定义express中间件函数的时候都会将第三个参数定义为next,这个next就是我们今天的主角,next函数主要负责将控制权交给下一个中间件,如果当前中间件没有终结请求,并且next没有被调用,那么请求将被挂起,后边定义的中间件将得不到被执行的机会。

何时使用Next

从上边的描述我们已经知道,next函数主要是用来确保所有注册的中间件被一个接一个的执行,那么我们就应该在所有的中间件中调用next函数,但有一个特例,如果我们定义的中间件终结了本次请求,那就不应该再调用next函数,否则就可能会出问题,我们来看段代码

app.get('/a', function(req, res, next) {
    res.send('sucess');
    next();
});

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  console.log(404);
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.render('error', {
    message: err.message,
    error: {}
  });
});

发送请求"/a",控制台打印日志如下:

404
GET /a 500 6.837 ms - -
Error: Can't set headers after they are sent.
    at ServerResponse.OutgoingMessage.setHeader (_http_outgoing.js:345:11)

为什么代码会抛异常呢,就是因为我们在res.send之后调用了next函数,虽然我们本次的请求已经被终止,但后边的404中间件依旧会被执行,而后边的中间件试图去向res的headers中添加属性值,所以就会抛出上边的异常。

读到这你可能会有个疑问,如果我不在res.send后边调用next函数,那后边定义的404中间件是不是永远都不会被执行到。现在我们删除res.send后边next函数调用,发送请求"/xxx",我们就会发现404中间件被执行了,(ㄒoㄒ),这不是和我们之前说的矛盾了吗,我们的自定义中间件没有调用next,但后边定义的中间件仍旧被执行了,这究竟是为什么呢。看来只能求助源码了~~~

Next的内部机制

function next(err) {
    ... //此处源码省略
    // find next matching layer
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }
      ... //此处源码省略
    }
	... //此处源码省略
    // this should be done for the layer
    if (err) {
        layer.handle_error(err, req, res, next);
    } else {
	    layer.handle_request(req, res, next);
    }
  }

上边就是express中next的源码,为了更容易说明问题,对代码进行了删减。从上边的源码可以发现,next函数内部有个while循环,每次循环都会从stack中拿出一个layer,这个layer中包含了路由和中间件信息,然后就会用layer和请求的path就行匹配,如果匹配成功就会执行layer.handle_request,调用中间件函数。但如果匹配失败,就会循环下一个layer(即中间件)。

现在我们就能解释上边提出的问题了,为什么我们的自定义中间件中没调用next函数,但后边的404中间件仍旧会被执行到,因为我们请求的"/xxx"匹配不到我们注册的"/a"路由中间件,所以while循环会继续往下执行,匹配404中间件成功,所以会执行404中间件。

注意:app.use注册的中间件,如果path参数为空,则默认为"/",而path为"/"的中间件默认匹配所有的请求。

有一点需要特别指出,其实我们在定义路由中间件的时候函数的第三个参数next和我们定义非路由中间件的函数的第三个参数next不是同一个next,我们在上边看到的是非路由中间件的next,而路由中间件的next函数是这样的

function next(err) {
    if (err && err === 'route') {
      return done();
    }

    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }

    if (layer.method && layer.method !== method) {
      return next(err);
    }

    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }

这个next比上边的那个next要简单很多,它负责同一个路由的多个中间件的控制权的传递,并且它会接收一个参数"route",如果调用next(“route”),则会跳过当前路由的其它中间件,直接将控制权交给下一个路由。

最后有必要再说一说next(err),next(err)是如何将控制权传递到错误处理中间件的,从前边的代码我们知道,当调用next(err)是,express内部会调用layer.handle_error,那我们来看看它的源码

Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;

  if (fn.length !== 4) {
    // not a standard error handler
    return next(error);
  }

  try {
    fn(error, req, res, next);
  } catch (err) {
    next(err);
  }
};

代码中的fn就是中间件函数,express会对fn的参数个数进行判断,如果参数个数不等于4则认为不是错误处理中间件,则继续调用next(err),这样就会进入到下一个中间件函数,继续进行参数个数判断,如此方式一直到某个中间件函数的参数个数是4,就认为找到了错误处理中间件,然后执行此中间件函数。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值