Express深入解读

本文基于Express 4.13.3

一、使用Express

通常情况下,创建一个简单的服务器,代码如下:

var http = require(‘http‘);

http.createServer(function(req, res) {
res.write(‘hello world‘);
res.end();
})
.listen(4000);

如果使用Express,代码如下:

var express = require(‘express‘);

var app = express();

app.get(‘/‘, function(req, res) {
res.write(‘hello world‘);
res.end();
});

app.listen(4000);

为了对Express的运行机制有所深入了解,不可避免地需要研究部分源码。Express的主要源码结构如下:

- lib/
    - middleware/
        - init.js
        - query.js
    - router/
        - index.js
        - layer.js
        - route.js
    - application.js
    - express.js
    - request.js
    - response.js
    - utils.js
    - view.js
- index.js

首先,需要理解上面例子中的 express 和 app 是什么。部分相关源码如下:

// in express.js
exports = module.exports = createApplication;

function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};

// ... ...

return app;
}

// in application.js
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};

可以看出,调用 express() 返回的 app 其实是一个函数,调用 app.listen() 其实执行的是 http.createServer(app).listen() 。因此, app 其实就是一个请求处理函数,作为 http.createServer 的参数。而 express 其实是一个工厂函数,用来生成请求处理函数。

二、中间件

Express中一个非常核心的概念就是中间件(middleware)。在官方文档中,有这样一句话:

An Express application is essentially a series of middleware calls.

也就是说,一个Express应用,从本质上来说,就是一系列中间件的调用。那么,中间件到底是什么呢?其实,一个中间件,就是一个函数。通常情况下,一个中间件函数的形式如下:

function middlewareName(req, res, next) {
// do something
}

如果是错误处理的中间件,形式如下:

function middlewareName(err, req, res, next) {
// do something
}

参数中, req 和 res 分别表示请求的 request 和 response,next 本身也是一个函数,调用 next() 就会继续执行下一个中间件。其实,请求的处理过程大致如下,就是依次经过各个中间件。

       ↓
---------------
| middleware1 |
---------------
       ↓
---------------
| ... ... ... |
---------------
       ↓
---------------
| middlewareN |
---------------
       ↓

中间件大体上可以分为两种:普通中间件和路由中间件。注册普通中间件,通常是通过 app.use() 方法;而注册路由中间件,通常是通过 app.METHOD() 方法。例如:

app.use(‘/user‘, function(req, res, next) {
// do something
});

app.get(‘/user‘, function(req, res, next) {
// do something
});

例子中的两者有以下区别:

  • 前者匹配所有以 /user 开始的路径,而后者会精确匹配 /user 路径;
  • 前者对于请求的方法没有限制,而后者只能处理方法为GET的请求。

在上面提到了如下源码:

// in express.js
exports = module.exports = createApplication;

function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};

// ... ...

return app;
}

可以看到,所有的请求,其实都是由 app.handle() 来处理的。在了解请求处理的详细过程之前,需要先来了解Router。

三、Router

简单来说,Router(源码在 router/index.js )就是一个中间件的容器。事实上,Router是Express中一个非常核心的东西,基本上就是一个简化版的Express框架。app的很多API,例如: app.use() , app.param() , app.handle() 等,事实上都是对Router的API的一个简单包装。可以通过 app._router 来访问默认的Router对象。

Router对象有一个 stack 属性,为一个数组,存放着所有的中间件。当调用 app.use() 的时候,实际上是执行了 router.use() ,从而向 router.stack 数组中添加中间件。 router.stack 中的每一项是一个Layer(源码在 router/layer.js )对象,它是对中间件函数的一个封装。添加中间件的部分相关源码如下:

// in router/index.js, proto.use()
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);

layer.route = undefined;

this.stack.push(layer);

对于普通的中间件,添加过程就大致如此。不过,对于路由中间件,就稍微复杂了一些。在此之前,先来看一下添加路由中间件的几种方法:

// app.METHOD --> router.route --> route.METHOD
app.get(‘/user‘, function(req, res) {});

// app.all --> router.route --> route.METHOD
app.all(‘/user‘, function(req, res) {});

// app.route --> router.route --> route.METHOD
app.route(‘/user‘)
.get(function(req, res) {})
.post(function(req, res) {});

// router.METHOD/all --> router.route --> route.METHOD/all
router.get(‘/user‘, function(req, res) {});

可以看到,无论是哪一种方法添加路由中间件,都需要通过 router.route() 来创建一条新的路由,然后调用 route.METHOD()/all() 来注册相关的处理函数。因此,首先需要了解Route(源码在 router/route.js )对象。

Route可以简单理解为存放路由处理函数的容器,它也有一个 stack 属性,为一个数组,其中的每一项也是一个Layer对象,是对路由处理函数的包装。下面来看当执行 router.route() 的时候发生了什么:

// in router/index.j
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;
};

也就是说,当调用 router.route() 的时候,实际上是新建了一个 layer 放在 router.stack 中;并设置 layer.route 为新建的Route对象。

下面来看 route.METHOD 的时候发生了什么:

// in router/route.js
var layer = Layer(‘/‘, {}, handle);
layer.method = method;

this.methods[method] = true;
this.stack.push(layer);

即,当调用 route.METHOD() 的时候,新建一个layer放在 route.stack 中。

通过上面的分析可以发现,Router其实是一个二维的结构。例如,一个可能的 router.stack 结构如下所示:

----------------
|    layer1    |
----------------
        ↓
---------------- layer2.route.stack  ------------   ------------   ------------
|    layer2    | ------------------> | layer2-1 |-->| layer2-2 |-->| layer2-3 |
----------------                     ------------   ------------   ------------
        ↓
---------------- layer3.route.stack  ------------   ------------
|    layer3    | ------------------> | layer3-1 |-->| layer3-2 |
----------------                     ------------   ------------
        ↓
----------------
|    ......    |
----------------
        ↓
----------------
|    layerN    |
----------------

四、参数处理

在很多时候,路由中是带有参数的,尤其是Restful风格的API。例如 /user/name ,可能name是个动态的值,从而去数据库中查询出相关的用户。因此,在Router中,通常会涉及到参数的处理。

先看如下例子:

app.get(‘/user/:name‘, function(req, res, next) {
res.send(req.user);
});

app.param(‘name‘, function(req, res, next, val) {
queryUser(val, function(err, user) {
if (err) {
return next(err);
}
req.user = user;
next();
});
});
 

通过 app.param() 可以注册参数处理函数,事实上, app.param() 调用了 router.param() 。Router有一个 params 属性,其值是一个对象,用来存储参数的处理函数。例如,一个可能的 router.params 如下:

{
name: [processName, queryName],
id: [queryId]
}

其中 processName , queryName , queryId 表示的都是函数,即通过 app.param() 或 router.param() 所注册的参数处理函数。

然后在处理客户端请求的时候,会使用相应的与处理函数对请求URL中的参数进行处理。

五、请求处理

前面所提到的,无论是添加中间件,还是参数处理函数,都是应用的构建过程。当应用构建好了之后,客户端发起请求,这个时候,应用就开始使用前面的中间件和参数处理函数,来处理客户端的请求。

前面提到,所有的请求,都是由 app.handle() 来处理的,通过看源码,可以发现,其实 app.handle() 是调用了 router.handle() 。 router.handle 的源码比较复杂,在这里有一个简单的分析。

当请求到来时,经过中间件的顺序大致如下所示:

        ↓
----------------
|    layer1    |
----------------
        ↓
----------------
|    layer2    |
----------------
        ↓
---------------- layer3.route.stack  ------------   ------------   ------------
|    layer3    | ------------------> | layer3-1 |-->| layer3-2 |-->| layer3-3 | ---
----------------                     ------------   ------------   ------------   |
                                                                                  |
        ---------------------------------------------------------------------------
        ↓
---------------- layer4.route.stack  ------------   ------------
|    layer4    | ------------------> | layer4-1 |-->| layer4-2 | ---
----------------                     ------------   ------------   |
                                                                   |
        ------------------------------------------------------------
        ↓
----------------
|    ......    |
----------------
        ↓
----------------
|    layerN    |
----------------
        ↓

即,请求会依次经过各个中间件,如果是路由中间件,则会依次经过其中的各个路由函数。当请求到达一个中间件的时候,会进行如下判断:

                ↓
  No  --------------------
------|    path match    |
|     --------------------
|               ↓ Yes
|     --------------------  Yes  ---------------------  No
|     |     has route    |-------| http method match |------
|     --------------------       ---------------------     |
|               ↓ No                       | Yes           |
|     --------------------                 |               |
|     |  process params  |<-----------------               |
|     --------------------                                 |
|               ↓                                          |
|     --------------------                                 |
|     | execute function |                                 |
|     --------------------                                 |
|               ↓                                          |
|     --------------------                                 |
----->|    next layer    |<---------------------------------
      --------------------
                ↓
  • 首先判断路径是否匹配,如果路径不匹配,则直接跳过该中间件
  • 然后判断是否为路由中间件,如果不是,即如果是普通的中间件,则进行参数处理,并执行中间件函数
  • 如果是路由中间件,则判断HTTP请求方法是否匹配,如果匹配,则进行参数处理,并执行中间件函数(该中间件函数实际上是 route.dispatch() );否则跳过该中间件

会发现,如果有多个中间件匹配的话,在每个中间件函数执行之前,都有一个参数处理的过程,那么,参数与处理函数会不会被多次执行呢?事实上,在 router.handle() 的时候,针对参数的处理,引入了缓存机制,因此,每个参数的处理函数只会执行一次,并将结果保存在缓存中。在处理同一个请求的过程中,如果需要处理某个参数,会首先检查缓存,如果缓存中不存在,才会执行其处理函数。

六、内置中间件

在初始化 app._router 的时候,就加载了 middleware/query.js 和 middleware/init.js 这两个中间件:

// in application.js
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled(‘case sensitive routing‘),
strict: this.enabled(‘strict routing‘)
});

this._router.use(query(this.get(‘query parser fn‘)));
this._router.use(middleware.init(this));
}
};
 

第一个中间件的作用主要是解析URL query。 query(this.get(‘query parser fn‘)) 的作用是设置URL query的解析器,并返回相应的中间件函数。该部分代码比较简单,不做赘述。

第二个中间件的作用主要是将 req 和 res 分别暴露给对方,并且让它们分别继承自 app.request 和 app.response 。涉及到的相关源码为:

// in middleware/init.js
exports.init = function(app) {
return function expressInit(req, res, next) {
// ... ...
req.res = res;
res.req = req;
req.next = next;

req.__proto__ = app.request;
res.__proto__ = app.response;

// ... ...
};
};

// in express.js
function createApplication() {
// ... ...

app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };

// ... ...
}
 

因此,其实是让 req 和 res 分别继承自了 request.js 和 response.js 的导出对象,简单来说,就是对reqres进行了属性和方法的扩展。

另外,Express中还有一个内置的中间件,即 express.static ,它依赖的是serve-static模块,主要用于创建静态资源服务。

七、视图渲染

渲染模板使用的是 res.render() ,例如:

app.get(‘/user‘, function(req, res) {
res.render(‘user‘, { name: ‘alex‘ });
});

其实, res.render() 调用了 app.render() 。在 app.render() 中,先创建一个 view 对象(相关源码为 view.js ),然后调用 view.render() 。如果允许缓存,即 app.enabled(‘view cache‘) 的话,则会优先检查缓存,如果缓存中已有相关视图,则直接取出;否则才会新创建一个视图对象。

该部分比较简单,不做赘述。

转自:zztf

地址:http://div.io/topic/1403

时间: 2024-10-23 19:48:05

Express深入解读的相关文章

详解回调函数——以JS为例解读异步、回调和EventLoop

转自:http://blog.csdn.net/tywinstark/article/details/48447135#comments 很多人在问什么是回调?百度出来的答案基本都不正确,看了只会让人更加迷惑.下文试着用尽量简单的例子帮大家梳理清楚,因为回调并不是一句话下定义就能明白的概念,需要用一段文字像讲故事一样来说明,回调如同很多重要的计算机概念一样,它是有历史文化的,你需要知道它从哪里来,用来干什么,才能理解及在实际生产中应用. 回调,是非常基本的概念,尤其在现今NodeJS诞生与蓬勃发

XenApp/XenDesktop 7.12新功能LHC解读

在今天,Citrix发布了期待已久的XenApp/XenDesktop新版本7.12,在7.12中有许多值得期待的新功能(访问Citrix edocs查看7.12文档).其中,本文将在此处解读新功能:Local Host Cache,简称LHC,中文名为本地主机缓存.虽然我们中的许多人都熟悉XenApp 6.5中的LHC功能,但那是基于IMA管理架构下的LHC.作为FMA管理架构下的LHC,和IMA管理架构下的LHC是不同的架构,下面我们就来说说这些关于LHC的内容. 一.IMA架构简述 IMA

delphi ICS控件示例解读

1 {* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 2 3 Author: Fran鏾is PIETTE 4 Object: Demo program to show how to use TWSocket object is a very 5 simple server program. This server just wait for a client to 6 connect, th

ASP.NET 5系列教程(七)完结篇-解读代码

在本文中,我们将一起查看TodoController 类代码. [Route] 属性定义了Controller的URL 模板: [Route("api/[controller]")] 所有符合该模板类型的HTTP 请求都会被路由到该controller.在本例中, 路由的命名规范为对应Controller 的前缀,对于TodoController 类,路由模板为 “api/todo”. HTTP 方法 [HttpGet].[HttpPost]和[HttpDelete] 属性定义为 co

解读ASP.NET 5 &amp; MVC6系列(2):初识项目

原文:解读ASP.NET 5 & MVC6系列(2):初识项目 初识项目 打开VS2015,创建Web项目,选择ASP.NET Web Application,在弹出的窗口里选择ASP.NET 5 Website模板创建项目,图示如下: 我们可以看到,此时Web Forms\MVC\Web API复选框都选择不了,原有是因为在ASP.NET 5中做了大量更改,移除了Web Forms功能,将MVC.Web API.Web Pages这些功能合在了一起,所以自然就不需要这些复选框了.另外由于是CT

《Nodejs开发加密货币》之七:入口程序app.js解读

入口程序app.js解读 发布本文时,比特币价格 ¥2873.95 / $443.95 .为什么一个凭空设计出来的加密货币如此受追捧?为什么微软.IBM等巨头纷纷进入?为什么尝试了解比特币的技术人员,都会被深深吸引?它到底有什么诱人之处?<Nodejs开发加密货币>,让我们一起探索其背后的密码. <Nodejs开发加密货币>,目的是提供加密货币(亿书币)的详尽开发文档,涉及到使用Nodejs开发产品的方方面面,从前端到后台.从服务器到客户端.从PC到移动.从IO密集型到计算密集型.

解读ASP.NET 5 &amp; MVC6系列(3):项目发布与部署

原文:解读ASP.NET 5 & MVC6系列(3):项目发布与部署 本章我们将讲解ASP.NET5项目发布部署相关的内容,示例项目以我们前一章创建的BookStore项目为例. 发布前的设置 由于新版ASP.NET5支持多版本DNX运行环境的发布和部署,所以在部署之前,我们需要设定部署的目标DNX(即之前的KRE). 步骤:右键BookStore项目->属性->Application选项卡,选择DNX的版本,本例中,选择dnx-coreclr-win-x64.1.0.0-beta4.

ASP.NET Core 介绍和项目解读

1. 前言 2. ASP.NET Core 简介 2.1 什么是ASP.NET Core 2.2 ASP.NET Core的特点 2.3 ASP.NET Core 项目文件夹解读 2.3.1 项目文件夹总览 2.3.2 project.json和global.json 2.3.1 Properties——launchSettings.json 2.3.4 Startup.cs (1) 构造函数 (2) ConfigureServices (3) Configure 2.3.5 bundlecon

像asp.net Mvc一样开发nodejs+express Mvc站点

像asp.net Mvc一样开发nodejs+express Mvc站点 首先,我是个c#码农.从事Mvc开发已然4个年头了,这两年前端MVC的兴起,我也跟风学了一些,对前端的框架也了解一些,angularJs,requirejs,commonJs,backbone等等前端的mvc框架也异常流行,与这些前端的流行框架一同火起来的还有node.js. Node.js将javascript作为服务器端的代码开发,由其语言特性(单线程,异步)等以高效率高吞吐著称.这里不会去讨论node.js的好的坏的