Node.js精进(5)——HTTP
<p> HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种获取网络资源(例如图像、HTML文档)的应用层协议,它是互联网数据通信的基础,由请求和响应构成。</p><p> 在 Node.js 中,提供了 3 个与之相关的模块,分别是 HTTP、HTTP2 和 HTTPS,后两者分别是对 HTTP/2.0 和 HTTPS 两个协议的实现。</p>
<p> HTTP/2.0 是 HTTP/1.1 的扩展版本,主要基于 Google 发布的 SPDY 协议,引入了全新的二进制分帧层,保留了 1.1 版本的大部分语义。</p>
<p> HTTPS(HTTP Secure)是一种构建在SSL或TLS上的HTTP协议,简单的说,HTTPS就是HTTP的安全版本。</p>
<p> 本节主要分析的是 HTTP 模块,它是 Node.js 网络的关键模块。</p>
<p> 本系列所有的示例源码都已上传至Github,<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">点击此处</span></span>获取。</p>
<h1>一、搭建 Web 服务器</h1>
<p> Web 服务器是一种让网络用户可以访问托管文件的软件,常用的有 IIS、Nginx 等。</p>
<p> Node.js 与 ASP.NET、PHP 等不同,它不需要额外安装 Web 服务器,因为通过它自身包含的模块就能快速搭建出 Web 服务器。</p>
<p> 运行下面的代码,在浏览器地址栏中输入 http://localhost:1234 就能访问一张纯文本内容的网页。</p>
<div class="cnblogs_code">
<pre>const http = require('http'<span style="color: rgba(0, 0, 0, 1)">);
const server </span>= http.createServer((req, res) =><span style="color: rgba(0, 0, 0, 1)"> {
res.statusCode </span>= 200<span style="color: rgba(0, 0, 0, 1)">;
res.setHeader(</span>'Content-Type', 'text/plain'<span style="color: rgba(0, 0, 0, 1)">);
res.end(</span>'strick'<span style="color: rgba(0, 0, 0, 1)">);
})
server.listen(</span>1234);</pre>
</div>
<p> res.end() 在<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">流一节</span></span>中已分析过,用于关闭写入流。</p>
<p><span style="font-size: 16px"><strong>1)createServer()</strong></span></p>
<p> createServer() 用于创建一个 Web 服务器,源码存于<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">lib/http.js</span></span>文件中,内部就一行代码,实例化一个 Server 类。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> createServer(opts, requestListener) {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Server(opts, requestListener);
}</span></pre>
</div>
<p> Server 类的实现存于<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">lib/_http_server.js</span></span>文件中,由源码可知,http.Server 继承自 net.Server,而 net 模块可创建基于流的 TCP 和 IPC 服务器。</p>
<p> http.createServer() 在实例化 net.Server 的过程中,会监听 request 和 connection 两个事件。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> Server(options, requestListener) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (!(<span style="color: rgba(0, 0, 255, 1)">this</span> <span style="color: rgba(0, 0, 255, 1)">instanceof</span> Server)) <span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Server(options, requestListener);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 当 createServer() 第一个参数类型是函数时的处理(上面示例中的用法)</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">typeof</span> options === 'function'<span style="color: rgba(0, 0, 0, 1)">) {
requestListener </span>=<span style="color: rgba(0, 0, 0, 1)"> options;
options </span>=<span style="color: rgba(0, 0, 0, 1)"> {};
} </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span> (options == <span style="color: rgba(0, 0, 255, 1)">null</span> || <span style="color: rgba(0, 0, 255, 1)">typeof</span> options === 'object'<span style="color: rgba(0, 0, 0, 1)">) {
options </span>=<span style="color: rgba(0, 0, 0, 1)"> { ...options };
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ERR_INVALID_ARG_TYPE('options', 'object'<span style="color: rgba(0, 0, 0, 1)">, options);
}
storeHTTPOptions.call(</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">, options);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 继承于 net.Server 类</span>
<span style="color: rgba(0, 0, 0, 1)">net.Server.call(
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">,
{ allowHalfOpen: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">, noDelay: options.noDelay,
keepAlive: options.keepAlive,
keepAliveInitialDelay: options.keepAliveInitialDelay });
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (requestListener) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 当 req 和 res 两个参数都生成后,就会触发该事件</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.on('request'<span style="color: rgba(0, 0, 0, 1)">, requestListener);
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 官方注释:与此类似的选项,懒得写自己的文档</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> http://www.squid-cache.org/Doc/config/half_closed_clients/</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.httpAllowHalfOpen = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 三次握手后触发 connection 事件</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.on('connection'<span style="color: rgba(0, 0, 0, 1)">, connectionListener);
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.timeout = 0; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 超时时间,默认禁用</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.maxHeadersCount = <span style="color: rgba(0, 0, 255, 1)">null</span>; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 最大响应头数,默认不限制</span>
<span style="color: rgba(0, 0, 255, 1)">this</span>.maxRequestsPerSocket = 0<span style="color: rgba(0, 0, 0, 1)">;
setupConnectionsTracking(</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
</div>
<p><span style="font-size: 16px"><strong>2)listen()</strong></span></p>
<p> listen() 方法用于监听端口,它就是 net.Server 中的 server.listen() 方法。</p>
<div class="cnblogs_code">
<pre>ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);</pre>
</div>
<p><span style="font-size: 16px"><strong>3)req 和 res</strong></span></p>
<p> 实例化 Server 时的 requestListener() 回调函数中有两个参数 req(请求对象) 和 res(响应对象),它们的生成过程比较复杂。</p>
<p> 简单概括就是通过 TCP 协议传输过来的二进制数据,会被 http_parser 模块解析成符合 HTTP 协议的报文格式。</p>
<p> 在将请求首部解析完毕后,会触发一个 parserOnHeadersComplete() 回调函数,在回调中会创建 http.IncomingMessage 实例,也就是 req 参数。</p>
<p> 而在这个回调的最后,会调用 parser.onIncoming() 方法,在这个方法中会创建 http.ServerResponse 实例,也就是 res 参数。</p>
<p> 最后触发在实例化 Server 时注册的 request 事件,并将 req 和 res 两个参数传递到 requestListener() 回调函数中。</p>
<p> 生成过程的顺序如下所示,源码细节在此不做展开。</p>
<div class="cnblogs_code">
<pre>lib/_http_server.js : connectionListener()
lib/_http_server.js : connectionListenerInternal()
<span style="color: rgba(0, 0, 0, 1)">
lib</span>/_http_common.js : parsers = new FreeList('parsers', 1000, function parsersCb() {})
lib/_http_common.js : parserOnHeadersComplete() => parser.onIncoming()
<span style="color: rgba(0, 0, 0, 1)">
lib</span>/_http_server.js : parserOnIncoming() => server.emit('request', req, res)</pre>
</div>
<p> 在上述过程中,parsers 变量使用了<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">FreeList</span></span>数据结构(如下所示),一种动态分配内存的方案,适合由大小相同的对象组成的内存池。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">class FreeList {
constructor(name, max, ctor) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.name =<span style="color: rgba(0, 0, 0, 1)"> name;
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.ctor =<span style="color: rgba(0, 0, 0, 1)"> ctor;
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.max =<span style="color: rgba(0, 0, 0, 1)"> max;
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.list =<span style="color: rgba(0, 0, 0, 1)"> [];
}
alloc() {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">this</span>.list.length > 0 ?
<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.list.pop() :
ReflectApply(</span><span style="color: rgba(0, 0, 255, 1)">this</span>.ctor, <span style="color: rgba(0, 0, 255, 1)">this</span>, arguments);<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 执行回调函数</span>
<span style="color: rgba(0, 0, 0, 1)">}
free(obj) {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.list.length < <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.max) {
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.list.push(obj);
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
}
}</span></pre>
</div>
<p> parsers 维护了一个固定长度(1000)的队列(内存池),队列中的元素都是实例化的 HTTPParser。</p>
<p> 当 Node.js 接收到一个请求时,就从队列中索取一个 HTTPParser 实例,即调用 parsers.alloc()。</p>
<p> 解析完报文后并没有将其马上释放,如果队列还没满就将其压入其中,即调用 parsers.free(parser)。</p>
<p> 如此便实现了 parser 实例的反复利用,当并发量很高时,就能大大减少实例化所带来的性能损耗。</p>
<h1>二、通信</h1>
<p> Node.js 提供了<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">request()</span></span>方法显式地发起 HTTP 请求,著名的第三方库<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">axios</span></span>的服务端版本就是基于 request() 方法封装的。</p>
<p><span style="font-size: 16px"><strong>1)GET 和 POST</strong></span></p>
<p> GET 和 POST 是两个最常用的请求方法,主要区别包含4个方面:</p>
<ul>
<li>语义不同,GET是获取数据,POST是提交数据。</li>
<li>HTTP协议规定GET比POST安全,因为GET只做读取,不会改变服务器中的数据。但这只是规范,并不能保证请求方法的实现也是安全的。</li>
<li> GET请求会把附加参数带在URL上,而POST请求会把提交数据放在报文内。在浏览器中,URL长度会被限制,所以GET请求能传递的数据有限,但HTTP协议其实并没有对其做限制,都是浏览器在控制。</li>
<li>HTTP协议规定GET是幂等的,而POST不是,所谓幂等是指多次请求返回的相同结果。实际应用中,并不会这么严格,当GET获取动态数据时,每次的结果可能会有所不同。</li>
</ul>
<p> 在下面的例子中,发起了一次 GET 请求,访问上一小节中创建的 Server,options 参数中包含域名、端口、路径、请求方法。</p>
<div class="cnblogs_code">
<pre>const http = require('http'<span style="color: rgba(0, 0, 0, 1)">);
const options </span>=<span style="color: rgba(0, 0, 0, 1)"> {
hostname: </span>'localhost'<span style="color: rgba(0, 0, 0, 1)">,
port: </span>1234<span style="color: rgba(0, 0, 0, 1)">,
path: </span>'/test?name=freedom'<span style="color: rgba(0, 0, 0, 1)">,
method: </span>'GET'<span style="color: rgba(0, 0, 0, 1)">
};
const req </span>= http.request(options, res =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(res.statusCode);
res.on(</span>'data', d =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(d.toString()); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> strick</span>
<span style="color: rgba(0, 0, 0, 1)">});
});
req.end();</span></pre>
</div>
<p> res 和 req 都是可写流,res 注册了 data 事件接收数据,而在请求的最后,必须手动关闭 req 可写流。</p>
<p> POST 请求的构造要稍微复杂点,在 options 参数中,会添加请求首部,下面增加了内容的MIME类型和内容长度。</p>
<p> req.write() 方法可发送一块请求内容,如果没有设置 Content-Length,则数据将自动使用 HTTP 分块传输进行编码,以便服务器知道数据何时结束。 Transfer-Encoding: chunked 标头会被添加。</p>
<div class="cnblogs_code">
<pre>const http = require('http'<span style="color: rgba(0, 0, 0, 1)">);
const data </span>=<span style="color: rgba(0, 0, 0, 1)"> JSON.stringify({
name: </span>'freedom'<span style="color: rgba(0, 0, 0, 1)">
});
const options </span>=<span style="color: rgba(0, 0, 0, 1)"> {
hostname: </span>'localhost'<span style="color: rgba(0, 0, 0, 1)">,
port: </span>1234<span style="color: rgba(0, 0, 0, 1)">,
path: </span>'/test'<span style="color: rgba(0, 0, 0, 1)">,
method: </span>'POST'<span style="color: rgba(0, 0, 0, 1)">,
headers: {
</span>'Content-Type': 'application/json'<span style="color: rgba(0, 0, 0, 1)">,
</span>'Content-Length'<span style="color: rgba(0, 0, 0, 1)">: data.length
}
};
const req </span>= http.request(options, res =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(res.statusCode);
res.on(</span>'data', d =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(d.toString()); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> strick</span>
<span style="color: rgba(0, 0, 0, 1)">});
});
req.write(data);
req.end();</span></pre>
</div>
<p> 在 Server 中,若要接收请求的参数,需要做些处理。</p>
<p> GET 请求比较简单,读取 req.url 属性,解析 url 中的参数就能得到请求参数。</p>
<p> POST 请求就需要注册 data 事件,下面代码中只考虑了最简单的场景,直接获取然后字符串格式化。</p>
<div class="cnblogs_code">
<pre>const server = http.createServer((req, res) =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(req.url); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> /test?name=freedom</span>
req.on('data', d =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(d.toString()); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> {"name":"freedom"}</span>
<span style="color: rgba(0, 0, 0, 1)">});
})</span></pre>
</div>
<p> 在 KOA 的插件中有一款<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">koa-bodyparser</span></span>,基于<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">co-body</span></span>库,可解析 POST 请求的数据,将结果附加到 ctx.request.body 属性中。</p>
<p> 而 co-body 依赖了<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">raw-body</span></span>库,它能将多块二进制数据流组合成一块整体,刚刚的请求数据可以像下面这样接收。</p>
<div class="cnblogs_code">
<pre>const getRawBody = require('raw-body'<span style="color: rgba(0, 0, 0, 1)">);
const server </span>= http.createServer((req, res) =><span style="color: rgba(0, 0, 0, 1)"> {
getRawBody(req).then(</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (buf) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> <Buffer 7b 22 6e 61 6d 65 22 3a 22 66 72 65 65 64 6f 6d 22 7d></span>
<span style="color: rgba(0, 0, 0, 1)"> console.log(buf);
});
})</span></pre>
</div>
<p><span style="font-size: 16px"><strong>2)路由</strong></span></p>
<p> 在开发实际的 Node.js 项目时,路由是必不可少的。</p>
<p> 下面是一个极简的路由演示,先实例化<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">URL</span></span>类,再读取路径名称,最后根据 if-else 语句返回响应。</p>
<div class="cnblogs_code">
<pre>const server = http.createServer((req, res) =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 实例化 URL 类</span>
const url = <span style="color: rgba(0, 0, 255, 1)">new</span> URL(req.url, 'http://localhost:1234'<span style="color: rgba(0, 0, 0, 1)">);
const { pathname } </span>=<span style="color: rgba(0, 0, 0, 1)"> url;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 简易路由</span>
<span style="color: rgba(0, 0, 255, 1)">if</span>(pathname === '/'<span style="color: rgba(0, 0, 0, 1)">) {
res.end(</span>'main'<span style="color: rgba(0, 0, 0, 1)">);
}</span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span>(pathname === '/test'<span style="color: rgba(0, 0, 0, 1)">) {
res.end(</span>'test'<span style="color: rgba(0, 0, 0, 1)">);
}
});</span></pre>
</div>
<p> 上述写法,不能应用于实际项目中,无论是在维护性,还是可读性方面都欠妥。下面通过一个开源库,来简单了解下路由系统的运行原理。</p>
<p> 在 KOA 的插件中,有一个专门用于路由的<span style="color: rgba(51, 102, 255, 1)"><span style="color: rgba(51, 102, 255, 1)">koa-router</span></span>(如下所示),先实例化 Router 类,然后注册一个路由,再挂载路由中间件。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">var</span> Koa = require('koa'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">var</span> Router = require('koa-router'<span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 0, 255, 1)">var</span> app = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Koa();
</span><span style="color: rgba(0, 0, 255, 1)">var</span> router = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Router();
router.get(</span>'/', (ctx, next) =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> ctx.router available</span>
<span style="color: rgba(0, 0, 0, 1)">});
app.use(router.routes()).use(router.allowedMethods());</span></pre>
</div>
<p> Router() 构造函数中仅仅是初始化一些变量,在注册路由时会调用 register() 方法,将路径和回调函数绑定。</p>
<div class="cnblogs_code">
<pre>methods.forEach(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (method) {
Router.prototype </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (name, path, middleware) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> middleware;
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">typeof</span> path === 'string' || path <span style="color: rgba(0, 0, 255, 1)">instanceof</span><span style="color: rgba(0, 0, 0, 1)"> RegExp) {
middleware </span>= Array.prototype.slice.call(arguments, 2<span style="color: rgba(0, 0, 0, 1)">);
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
middleware </span>= Array.prototype.slice.call(arguments, 1<span style="color: rgba(0, 0, 0, 1)">);
path </span>=<span style="color: rgba(0, 0, 0, 1)"> name;
name </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
}
</span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.register(path, , middleware, {
name: name
});
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">;
};
});</span></pre>
</div>
<p> 在 register() 函数中,会将实例化一个 Layer 类,就是一个路由实例,并加到内部的数组中,下面是删减过的源码。</p>
<div class="cnblogs_code">
<pre>Router.prototype.register = <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (path, methods, middleware, opts) {
opts </span>= opts ||<span style="color: rgba(0, 0, 0, 1)"> {};
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 路由数组</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> stack = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.stack;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 实例化路由</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> route = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Layer(path, methods, middleware, {
end: opts.end </span>=== <span style="color: rgba(0, 0, 255, 1)">false</span> ? opts.end : <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
name: opts.name,
sensitive: opts.sensitive </span>|| <span style="color: rgba(0, 0, 255, 1)">this</span>.opts.sensitive || <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
strict: opts.strict </span>|| <span style="color: rgba(0, 0, 255, 1)">this</span>.opts.strict || <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
prefix: opts.prefix </span>|| <span style="color: rgba(0, 0, 255, 1)">this</span>.opts.prefix || ""<span style="color: rgba(0, 0, 0, 1)">,
ignoreCaptures: opts.ignoreCaptures
});
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> add parameter middleware</span>
Object.keys(<span style="color: rgba(0, 0, 255, 1)">this</span>.params).forEach(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> (param) {
route.param(param, </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.params);
}, </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 加到数组中</span>
<span style="color: rgba(0, 0, 0, 1)">stack.push(route);
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> route;
};</span></pre>
</div>
<p> 在注册中间件时,首先会调用 router.routes() 方法,在该方法中会执行匹配到的路由(路径和请求方法相同)的回调。</p>
<p> 其中 layerChain 是一个数组,它会先添加一个处理数组的回调函数,再合并一个或多个路由回调(一条路径可以声明多个回调),</p>
<p> 在处理完匹配路由的所有回调函数后,再去运行下一个中间件。</p>
<div class="cnblogs_code">
<pre>Router.prototype.routes = Router.prototype.middleware = <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> () {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> router = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">var</span> dispatch = <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> dispatch(ctx, next) {
</span><span style="color: rgba(0, 0, 255, 1)">var</span> path = router.opts.routerPath || ctx.routerPath ||<span style="color: rgba(0, 0, 0, 1)"> ctx.path;
</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* 找出所有匹配的路由,可能声明了相同路径和请求方法的路由
* matched = {
* path: [], 路径匹配
* pathAndMethod: [], 路径和方法匹配
* route: false 路由是否匹配
* }
</span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> matched =<span style="color: rgba(0, 0, 0, 1)"> router.match(path, ctx.method);
</span><span style="color: rgba(0, 0, 255, 1)">var</span><span style="color: rgba(0, 0, 0, 1)"> layerChain, layer, i;
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
ctx.matched </span>=<span style="color: rgba(0, 0, 0, 1)"> matched.path;
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将 router 挂载到 ctx 上,供其他中间件使用</span>
ctx.router =<span style="color: rgba(0, 0, 0, 1)"> router;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 没有匹配的路由,就运行下一个中间件</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (!matched.route) <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> next();
</span><span style="color: rgba(0, 0, 255, 1)">var</span> matchedLayers = matched.pathAndMethod <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 路径和请求方法都匹配的数组</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 最后一个 matchedLayer</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> mostSpecificLayer = matchedLayers
ctx._matchedRoute </span>=<span style="color: rgba(0, 0, 0, 1)"> mostSpecificLayer.path;
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (mostSpecificLayer.name) {
ctx._matchedRouteName </span>=<span style="color: rgba(0, 0, 0, 1)"> mostSpecificLayer.name;
}
</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
* layerChain 是一个数组,先添加一个处理数组的回调函数,再合并一个或多个路由回调
* 目的是在运行路由回调之前,将请求参数挂载到 ctx.params 上
</span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
layerChain </span>= matchedLayers.reduce(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(memo, layer) {
memo.push(</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(ctx, next) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 正则匹配的捕获数组</span>
ctx.captures =<span style="color: rgba(0, 0, 0, 1)"> layer.captures(path, ctx.captures);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 请求参数对象,key 是参数名,value 是参数值</span>
ctx.params =<span style="color: rgba(0, 0, 0, 1)"> layer.params(path, ctx.captures, ctx.params);
ctx.routerName </span>=<span style="color: rgba(0, 0, 0, 1)"> layer.name;
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> next();
});
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 注册路由时的回调,stack 有可能是数组</span>
<span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> memo.concat(layer.stack);
}, []);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 在处理完匹配路由的所有回调函数后,运行下一个中间件</span>
<span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> compose(layerChain)(ctx, next);
};
dispatch.router </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> dispatch;
};</span></pre>
</div>
<p> 另一个 router.allowedMethods() 会对异常行为做统一的默认处理,例如不支持的请求方法,不存在的状态码等。</p>
<p> </p>
<p> </p>
<p>参考资料:</p>
<p>饿了么网络面试题</p>
<p>深入理解Node.js源码之HTTP</p>
<p>官网HTTP</p>
<p>Node HTTP Server 源码解读</p>
<p>node http server源码解析</p>
<p>Node 源码 —— http 模块</p>
<p>通过源码解析 Node.js 中一个 HTTP 请求到响应的历程</p>
<p>koa-router源码解析</p>
<p>koa-router源码解读</p>
<p> </p><br><br>
来源:https://www.cnblogs.com/strick/p/16243384.html
頁:
[1]