跳到主要内容

Express

Express 对 HTTP server 做了一层薄薄的封装,增加 Route 支持较为便利的路由控制分发,支持一些简单的拓展。

整体设计

简单介绍 Express 的目录结构和流程。

目录结构

lib
├── application.js # api 设置
├── express.js # 入口
├── middleware
│ ├── init.js # request 和 response 拓展中间件
│ └── query.js # 请求参数处理中间件
├── request.js # request 拓展的具体内容
├── response.js # response 拓展的具体内容
├── router # 路由处理转换
│ ├── index.js
│ ├── layer.js
│ └── route.js
├── utils.js # 工具函数
└── view.js # 模板引擎支持

相关概念

名称说明
appExpress 实例应用程序
router路由管理对象
route单个路由记录
layer路由或中间件转换后的统一结构,一个匹配层,每层有相应回调函数

流程概述

基础示例如下

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

// 中间件
app.use("/foo", function (req, res, next) {
console.log("foo middleware");
next();
});

// 路由控制
app.get("/foo", function (req, res) {
console.log("foo route");
res.send("hello world");
});

app.get("/bar", function (req, res) {
console.log("bar route");
res.send("how are you");
});

use 方法注册中间件,get 方法注册路由控制器。 不管是中间件还是路由控制器,都是注册回调,在请求满足某些条件时调用回调函数。

在 express 中,中间件和路由控制器都被转换为所谓的 Layer

const layerQueue = [
{
// foo 中间件
},
{
// foo 路由控制器
},
{
// bar 路由控制器
},
];

通过 useget 等方法注册的回调,会被保存在一个数组(有的称这里为一个 Layer Stack,是不太合适的,并非先注册后执行,而是先注册先执行)。 在接收到请求时,根据请求路径,依次判断 layerQueue 中的每一项是否和当前请求 path 匹配,匹配则执行相应的回调。各个 layer 的执行顺序是按照注册的先后顺序,因而在使用中间件和路由时,是需要注意顺序的。

Router

路由的初始化是 lazy 的,第一次使用 use, get 或 post 等进行与匹配相关的设置时才会生成 router。初始化后的路由基本结构如下

{
// ...省略
name:"router"
stack:Array(3) [
{
handle:function query(req, res, next){}
keys:Array(0) []
name:"query"
params:undefined
path:undefined
regexp:/^\/?(?=\/|$)/i {fast_star: false, fast_slash: true, lastIndex: 0}
route:undefined
},
{
handle:function expressInit(req, res, next){}
keys:Array(0) []
name:"expressInit"
params:undefined
path:undefined
regexp:/^\/?(?=\/|$)/i {fast_star: false, fast_slash: true, lastIndex: 0}
route:undefined
},
{
handle:function () {}
keys:Array(0) []
name:"bound dispatch"
params:undefined
path:undefined
regexp:/^\/bar\/?$/i {fast_star: false, fast_slash: false, lastIndex: 0}
route:Route {path: "/bar", stack: Array(1), methods: Object}
}
],
__proto__: {
// ...省略其他方法
get:function(path) {
// ...
},
handle:function handle(req, res, out) {
// ...
}
}
}

关注 stack 和 __proto__ 上的部分方法。

stack 属性包含了由 get, use 等设置的路由,中间件转化而来的所有 Layer。 stack[0] 是在 lib/middleware/query.js 内置的 query 中间件,负责将请求 URL 中的参数转换为对象挂到 req 对象上。 stack[1] 是在 lib/middleware/init.js 内置的初始化中间件,对原始的 req 和 res 进行拓展。

Router.prototype,提供了 get 方法用于根据 path 获取路由,提供了 handle 方法作为路由处理的 main 函数。

最终实例化后 router 被挂载到了 app._router 上,当应用程序接收到请求时,会调用 app.handle 处理,而 app.handle 就交给了 router.handle,router.handle 负责对 stack 数组进行遍历,找到匹配的 layer 然后执行 layer.handle。 调用关系

app.handle - router.handle - layer.handle - callback;

Layer

以下面的 /bar 路由为例

app.get("/bar", function (req, res) {
console.log("bar route");
res.send("how are you");
});

对应的 Layer 如下

{
handle:function () {}
keys:Array(0) []
name:"bound dispatch"
params:undefined
path:undefined
regexp:/^\/bar\/?$/i {fast_star: false, fast_slash: false, lastIndex: 0}
route:Route {
methods:Object {get: true}
path:"/bar"
stack:Array(1) [
{
handle:function(req, res, next) {}
keys:Array(0) []
method:"get"
name:"<anonymous>"
params:undefined
path:undefined
regexp:/^\/?$/i {fast_star: false, fast_slash: false, lastIndex: 0}
}
]
},
__proto__: {
handle_request:function handle(req, res, next) {}
match:function match(path) {}
}
}

处理过程如下

  • 通过 layer.match 方法,使用 regexp 和当前请求 path,判断是否需要执行当前 layer 的回调。
  • 如果 match,调用 handle_request 进入到当前 layer 执行过程。
  • handle_request 则调用 layer.handle,layer.handle 负责对 route.stack 里面配置的多个回调进行顺序调用,依次调用其中的每个 handle。对于中间件,route 为空,这 layer.handle 就是所配置的回调。
  • layer.handle 调用 route.stack[x].handle,依次完成当前 layer 的回调执行。

调用关系

layer.match - layer.handle_request - layer.handle - callback;

Middleware

中间件会被转化为 Layer

app.use(
"/bar",
function (req, res, next) {
console.log("bar middleware");
next();
},
function (req, res, next) {
console.log("foo middleware 2");
next();
}
);

以上事例给 /bar 路由增加了两个回调,这两个回调会被处理成两个 layer。路由类型的 layer 其 handle 函数是一个回调分发处理函数,而中间件类型的 layer,每个 layer 的 handle 就是所配置的一个回调

{
handle:function(req, res, next) {
console.log('bar middleware');
next();
}
keys:Array(0) []
name:"<anonymous>"
params:Object {}
path:"/bar"
regexp:/^\/bar\/?(?=\/|$)/i {fast_star: false, fast_slash: false, lastIndex: 0}
route:undefined
__proto__: {
handle_request:function handle(req, res, next) {}
match:function match(path) {}
}
}

执行过程与前面所述 layer 执行过程基本一致。

req 和 res

NodeJS 会把请求抽象成一个 http.IncomingMessage 实例,把响应抽象成 http.ServerResponse 实例,我们一般称作 req 和 res。

Express 对原始的请求和响应对象做了增强,增加了一些属性和方法,比如说增加了 req.header 方法,用以更方便地获取头部字段;增加了 res.send, res.sendStatus 等方法,提供了更简洁的响应的设置。

总结

Express 通过 layer 的概念,统一了 middleware 和 route

express

应用程序结构很清晰

app
router
layer
route
middleware

对应的调用流程也很清晰

// middleware
app.handle -> router.handle -> layer.handle -> route.handle/middleware.handle

每一层次具有统一的入口函数 handle 做该层的处理。

相对于原生的 NodeJS 而言,Express 主要的功能点

  • req 和 res 的拓展
  • 路由控制分发
  • 中间件的支持

只做了一层基础的封装,并没有做工程级别的限制和约定,使用很灵活的。

  • 优点:可以快速搭建一个具备路由分发和中间件能力的服务端应用程序。
  • 不足:缺少工程级别的设计规范,太过灵活缺乏统一性。