首先看下 express 源码结构:
回想一下 express 的使用:
可以看出 express 做的其实相当于对原生 nodejs http.createserver
的一系列封装,封装成 app,并提供一系列拓展api(中间件、请求调用)来处理用户请求。
express 入口
再看看express 的入口 express.js
,看下它做了什么:
export 出一个 createApplication
方法,对应着我们使用 express 的使用方式: var app = express()
可以看出这个createApplication
返回了一个 app 函数,这个app函数接收 req、res、next 用来处理用户的请求,怎么处理呢,交给 app.handle
处理。
app 函数会经过两次 mixin 处理,可以看出 app 继承了 eventemitter 和 proto (也就是 application)
并且增加了 request、response 两个分别基于 req和 res拓展的对象,并给他们增加到 app 的引用
application
我们首先来看 application 是怎样一个对象。
最常用的 listen 方法,即对 http.createServer 的封装:
app.use
另一个核心方法 app.use
, 是我们注册中间件的入口。
我们通常有两种形式注册中间件:app.use(fn),app.use(‘path’, fn), 不配置 path 则默认在到跟路径 ’/‘,所以在 7-21行先做了一轮入参处理
随后调用了一次 this.lazyrouter();
实例化一个 router 对象,并在接下来,42-49行可以看到实际上是调用了 router 对象的 use 方法。
所以来看一下 this.lazyrouter();
做了啥:
可以看到 router 对象是通过 Router 构造的, 他是如何构造 router 对象的,我们待会再看。
而第8-9行也是调用了 router 对象的 use方法注册了两个中间件
- query 中间件,基于 parseurl 和 qs 处理请求的 query
- middleware.init 返回的中间件,做的事是重新设置了 req和 res 的原型,分别指向 app.request 和 app.response。
app.route
除了 app.use
, 我们很快还能发现另一个核心方法 app.route
。以及对 apphttp.VERB 这一类方法的代理。
可以看到他们的实质都是是调用 this.lazyrouter();
实例化一个 router 对象,随后调用了 router 对象的 route 方法。所以当我们使用 app.route
app.get
app.post
这一类路由中间件的时候,都是在调用 router 对象的 route 方法。
Router
所以接下来我们看看 Router 构造函数,看看 router对象是如何构造的,以及router 对象的 use 和 route 方法是怎样的。
Router 构造函数的入口在 router/index.js
我们知道中间件就是匹配到路径就会执行的回调函数,可以看到 router 会把这件事直接交给 router.handle
做,再进一步看看 handle 方法。handle 方法很长,但是可以很快找到重点:
可以看到是在遍历一个 stack 数组,为了找到命中路径的 layer对象(第8行),拿到命中路径的 layer 对象之后,会调用 layer.handle_request(req, res, next);
来处理中间件。
再往下还有一段:
出现了几个 route 相关变量,具体是什么先不管,直接看注释我们大概可以明白这是把代理了 apphttp.VERB 的 router.route 方法又交给了 route[method] 处理。
Layer
上面出现了一个新概念 layer 对象,留意一下 Router 目录,其中有一个 layer.js
文件。
可以看到 layer 做的就是维护一个路径 path 和注册的回调函数的关系,同时会把 path 转成正则在请求阶段匹配路径调用注册的回调函数。layer.handle_request
所最终执行的函数即使 layer 对象实例化时传入的函数 fn,注意这里还回把 next 传给 fn
回头再看看 Router 构造函数的入口 router/index.js
在哪些地方 实例化了 layer 对象,两个地方:
- Router.prototype.use
- Router.prototype.route
这样绕了一圈,我们又回到了最初来看 Router 构造函数的目的:来看 use 和 route 这两个方法。
在 Router.prototype.use 中,是这样构造 layer 对象的:
在 Router.prototype.route 中,则是这样构造 layer 对象的:
比较一下两种方式可以发现:
- Router.prototype.use 创建的 layer,注册的回调函数是传入的 fn,layer.route 是 undefined
- Router.prototype.route 创建的 layer,注册的回调函数是一个 route 实例的 route.dispatch,layer.route 是这个 route实例
Route
上面这个 route 实例是通过 Route 创建的,我们再留意一下 Router 目录,其中还有一个 route.js
文件。
为什么要这么设计呢,我们首先来看一下 route.dispatch:
可以看到他的做法跟 router.handle 的做法类似,遍历 stack 数组取出 layer对象(第9行),调用 layer.handle_request(req, res, next);
来处理中间件。
注意他遍历 stack 数组的做法不是直接循环,而是通过 idx++ 和 next 传递,可以回头看看layer.handle_request
的实现里是把 next 传给了定义layer 对象实例时注册的回调函数,这里的回调函数就是 route.dispatch.bind(route),也就是 route.dispatch 自己,从而实现了遍历 stack 数组。
接下来我们就需要看看这个 layer 对象又是从哪来的了,可以很快找到这个 route 创建 layer 的地方:
可以看出他实际是维护了是一个统一 path (’/‘)对应多种 method handle 的关系,而 handle 也正是来自我们之前在 Router 构造函数入口 router/index.js
看到的把代理了 apphttp.VERB 的 router.route 方法又交给了 route[method] 的处理。
application 总结
到此我们终于把从 application 启动服务、注册和调用中间件的源码全部过完了,有关 router、route、layer的概念比较绕,接下来梳理一下。
一个 application 做的事包括三部分:
启动服务
- 调用
http.createServer
注册中间件,application(express)通过 layer 对象维映射路由 path 和中间件回调函数 handle 的关系,并维护了一个 layer stack 数组:
- 自己调用
app.lazyrouter
注册两个默认中间件,最终会回归到 router.use
处理:middleware.init(this)
重设 res 和 req 的原型 query
处理请求参数
- 调用方通过
app.use
, router.use
注册的中间件,最终会回归到 router.use
处理:router.use
会创建一个 layer,注册的回调函数是传入的 fn,layer.route 是 undefined
- 调用方通过
app.all
, app[http.VERB]
, app.route
, router.all
, router[http.VERB]
, router.route
注册中间件,最终回归到 router.route
处理:router.route
会创建一个 layer,这个 layer.route 是这个 route 实例 - 这个 route 实例会根据 http.VERB 再创建一系列 layer ,这些layer注册的回调函数是跟 http.VERB 对应的 fn handle
- 有两种处理的原因就在于,路由相关中间件不仅需要匹配路径还需要匹配 http.VERB
响应请求
- 调用
server.listen
- 中间件已经注册好,遍历 layer stack 数组,根据注册的区别执行中间件回调函数
- 回归到
router.use
处理的中间件,router.handle
路径匹配后拿到 layer layer.handle_request
执行这个 layer 的 fn
- 回归到
router.route
处理的中间router.handle
路径匹配后拿到 layer,这个 layer 中取到 route,这个route 也是一个 layer 实例 - 用这个 route 的
route._handles_method
判断 http.VERB 是否匹配 - 随后
route.dispatch
遍历 route layer 实例的 layer stack,找到匹配 的 layer layer.handle_request
执行这个 layer 的 fn
express 处理请求和注册调用中间件的核心原理即是如此。源码里还有 request.js
response.js
两个文件,内容相对简单,看起来比较快。
request
首先 req 这个对象是拓展自 http.IncomingMessage.prototype
的
先看一些通用方法:
req.get
req.accepts
req.accepts
检查是否支持某种 MIME type,入参兼容数组,逗号连接字符串,不支持会返回 undefined(这时候应用中应该返回 406)
req.range
req.range
- 两个参数 一个 size,一个 options(包含一个参数combine标识 重叠数据是否要合并)。
- 返回的结果
- -2 size 语法错误
- -1 size 无效,对应状态码 416
- undefined,请求头不带 range,没有size
- 一个数组,形式为
[{ start, end }, ...]
, 并包含一个属性 type === 'bytes'
- 不过这些都是靠 range-parser 这个第三方模块实现的
关于range
http1.1中新增字段,如果server支持range,需要在响应头中添加 Accept-ranges: bytes(不支持返回 Accept-ranges: none),之后client才可以发起带range的请求。
server 通过判断请求头中的 Range: bytes=0-xxx,判断是不是要做 range请求
如果 xxx存在且有效,返回文件内容,状态码206(partial content),设置 content-range
如果 xxx 无效,状态码 416(request range not satisfiable),
如果请求头不带range,正常响应,不设置 content-range
req.param
支持从 url params、body、query取值,排序即为优先级 req.is
判断请求是不是符合的 Content-Type ,基于 type-is 库实现
只读属性
同时还定义了一系列只读属性,记录一些值得关注的:
protocal
- 通过
this.connection.encrypted
判断是不是 https - 如果有 trust proxy,会用 trust proxy 获取 protocal,直接返回
- 如果设置了 X-Forwarded-Proto header,会根据 这个 header 取值,如果有多个(,分隔)只取第一个
- ip 和 ips
- ip 根据 trust proxy 获取请求的 ip 地址
- ips 根据trust proxy 获取从最近的一个代理地址到请求 ip地址的列表(不包含 socket 地址)
addrs.reverse().pop()
:[client, proxy1, proxy2] - addrs 的获取通过 proxy-addr 库实现
- hostname
- 如果有 trust proxy,取 trust proxy 的 host
- 如果有 X-Forwarded-Host,取 X-Forwarded-Host 配置的第一个
- 支持 ipv6 地址,增加了对协议头[] 的判断
- fresh 和 stale
- 对于get请求,当 2xx或 304的时候,判断 Etag 和 Last modified 有没有变化(基于fresh库实现)
response
res 对象拓展自 http.ServerResponse.prototype
几个比较重要的方法:
res.send
res.send
- 老接口支持
res.send(body, status)
的方式设置返回值,所以首先通过判断 arguments 做了一系列兼容性处理 - 随后根据入参数据类型,设置 content-type, 其中对于 object 类型的数据,如果是 buffer,content-type: bin,如果是 json,会交给
this.json
方法特殊处理 - 随后计算 content-length
- 随后设置 etag,如果 204和 304,content-type content-length,transfer-encoding会被去除
- 设置
this.end
res.json
对json做特殊处理,类似 Json.stringify。增加了对是否需要 escape 的配置,避免某些字符会导致 html 异常
把 0x3c (<)
、0x3e (>)
、0x26 (&)
转成 unicode 编码,避免被 html当做特殊标签字符解析
res.sendFile
我们调用 sendFile的方式是 res.sendFile(path, options, callback)
(options 和 callback 很多时候不配直接用默认值)
如果 opts.root 没有设置,那么path一定要是一个 绝对路径
接下来是读取文件的关键,通过 send(经典的 send包 ) 创建一个文件流
随后监听文件流的各个事件:
其中 onFinished 会在下一个事件循环开始调用 我们的 callback
最后通过pipe传给 res
res.download
download 的 实现和 sendFile 很接近
不同点在于会设置 ‘content-disposition’ 的响应头 (res.attachment 也会)
随后再调用 res.send
res.redirect
res.redirect
入参是目标 url- 第一步设置 响应头 header 中的 location field
- 调用 res.format 格式化 res.body
- 设置 content-length 和状态码