天天看点

手写koa-static源码,深入理解静态服务器原理

这篇文章继续前面的<code>Koa</code>源码系列,这个系列已经有两篇文章了:

第一篇讲解了<code>Koa</code>的核心架构和源码:手写Koa.js源码

第二篇讲解了<code>@koa/router</code>的架构和源码:手写@koa/router源码

本文会接着讲一个常用的中间件----<code>koa-static</code>,这个中间件是用来搭建静态服务器的。

其实在我之前使用Node.js原生API写一个web服务器已经讲过怎么返回一个静态文件了,代码虽然比较丑,基本流程还是差不多的:

通过请求路径取出正确的文件地址

通过地址获取对应的文件

使用<code>Node.js</code>的API返回对应的文件,并设置相应的<code>header</code>

<code>koa-static</code>的代码更通用,更优雅,而且对大文件有更好的支持,下面我们来看看他是怎么做的吧。本文还是采用一贯套路,先看一下他的基本用法,然后从基本用法入手去读源码,并手写一个简化版的源码来替换他。

本文可运行代码已经上传GitHub,大家可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaStatic

<code>koa-static</code>使用很简单,主要代码就一行:

上述代码中的<code>serve</code>就是<code>koa-static</code>,他运行后会返回一个<code>Koa</code>中间件,然后<code>Koa</code>的实例直接引用这个中间件就行了。

<code>serve</code>方法支持两个参数,第一个是静态文件的目录,第二个参数是一些配置项,可以不传。像上面的代码<code>serve('public')</code>就表示<code>public</code>文件夹下面的文件都可以被外部访问。比如我在里面放了一张图片:

手写koa-static源码,深入理解静态服务器原理

跑起来就是这样子:

手写koa-static源码,深入理解静态服务器原理

注意上面这个路径请求的是<code>/test.jpg</code>,前面并没有<code>public</code>,说明<code>koa-static</code>对请求路径进行了判断,发现是文件就映射到服务器的<code>public</code>目录下面,这样可以防止外部使用者探知服务器目录结构。

我们看到<code>koa-static</code>导出的是一个方法<code>serve</code>,这个方法运行后返回的应该是一个<code>Koa</code>中间件,这样<code>Koa</code>才能引用他,所以我们先来写一下这个结构吧:

现在这个中间件是空的,其实他应该做的是将文件返回,返回文件的功能也被单独抽取出来成了一个库----<code>koa-send</code>,我们后面会看他源码,这里先直接用吧。

<code>defer</code>是配置选项<code>opt</code>里面的一个可选参数,他稍微特殊一点,默认为<code>false</code>,如果你传了<code>true</code>,<code>koa-static</code>会让其他中间件先响应,即使其他中间件写在<code>koa-static</code>后面也会让他先响应,自己最后响应。要实现这个,其实就是控制调用<code>next()</code>的时机。在讲Koa源码的文章里面已经讲过了,调用<code>next()</code>其实就是在调用后面的中间件,所以像上面代码那样最后调用<code>next()</code>,就是先执行<code>koa-static</code>然后再执行其他中间件。如果你给<code>defer</code>传了<code>true</code>,其实就是先执行<code>next()</code>,然后再执行<code>koa-static</code>的逻辑,按照这个思路我们来支持下<code>defer</code>吧:

<code>koa-static</code>源码总共就几十行:https://github.com/koajs/static/blob/master/index.js

上面我们看到<code>koa-static</code>其实是包装的<code>koa-send</code>,真正发送文件的操作都是在<code>koa-send</code>里面的。文章最开头说的几件事情<code>koa-static</code>一件也没干,都丢给<code>koa-send</code>了,也就是说他应该把这几件事都干完:

由于<code>koa-send</code>代码也不多,我就直接在代码中写注释了,通过前面的使用,我们已经知道他的使用形式是:

他接收三个参数:

<code>ctx</code>:就是<code>koa</code>的那个上下文<code>ctx</code>。

<code>path</code>:<code>koa-static</code>传过来的是<code>ctx.path</code>,看过<code>koa</code>源码解析的应该知道,这个值其实就是<code>req.path</code>

<code>opts</code>: 一些配置项,<code>defer</code>前面讲过了,会影响执行顺序,其他还有些缓存控制什么的。

下面直接来写一个<code>send</code>方法吧:

上述代码并没有太复杂的逻辑,先拼一个完整的地址,然后使用<code>fs.stat</code>获取文件的基本信息,如果文件不存在,这个API就报错了,直接返回<code>404</code>。如果文件存在,就用<code>fs.stat</code>拿到的信息设置<code>Content-Length</code>和一些缓存控制的header。

<code>koa-send</code>的源码也只有一个文件,百来行代码:https://github.com/koajs/send/blob/master/index.js

上述代码我们看到最后并没有直接返回文件,而只是设置了<code>ctx.type</code>和<code>ctx.body</code>这两个值就结束了,为啥设置了这两个值,文件就自动返回了呢?要知道这个原理,我们要结合<code>Koa</code>源码来看。

之前讲<code>Koa</code>源码的时候我提到过,他扩展了<code>Node</code>原生的<code>res</code>,并且在里面给<code>type</code>属性添加了一个<code>set</code>方法:

这段代码的作用是当你给<code>ctx.type</code>设置值的时候,会自动给<code>Content-Type</code>设置值,<code>getType</code>其实是另一个第三方库<code>cache-content-type</code>,他可以根据你传入的文件类型,返回匹配的<code>MIME type</code>。我刚看<code>koa-static</code>源码时,找了半天也没找到在哪里设置的<code>Content-Type</code>,后面发现是在<code>Koa</code>源码里面。所以设置了<code>ctx.type</code>其实就是设置了<code>Content-Type</code>。

<code>koa</code>扩展的<code>type</code>属性看这里:https://github.com/koajs/koa/blob/master/lib/response.js#L308

之前讲<code>Koa</code>源码的时候我还提到过,当所有中间件都运行完了,最后会运行一个方法<code>respond</code>来返回结果,在那篇文章里面,<code>respond</code>是简化版的,直接用<code>res.end</code>返回了结果:

直接用<code>res.end</code>返回结果只能对一些简单的小对象比较合适,比如字符串什么的。对于复杂对象,比如文件,这个就不合适了,因为你如果要用<code>res.write</code>或者<code>res.end</code>返回文件,你需要先把文件整个读入内存,然后作为参数传递,如果文件很大,服务器内存可能就爆了。那要怎么处理呢?回到<code>koa-send</code>源码里面,我们给<code>ctx.body</code>设置的值其实是一个可读流:

这种流怎么返回呢?其实<code>Node.js</code>对于返回流本身就有很好的支持。要返回一个值,需要用到<code>http</code>回调函数里面的<code>res</code>,这个<code>res</code>本身其实也是一个流。大家可以再翻翻<code>Node.js</code>官方文档,这里的<code>res</code>其实是<code>http.ServerResponse</code>类的一个实例,而<code>http.ServerResponse</code>本身又继承自<code>Stream</code>类:

手写koa-static源码,深入理解静态服务器原理

所以<code>res</code>本身就是一个流<code>Stream</code>,那<code>Stream</code>的API就可以用了。<code>ctx.body</code>是使用<code>fs.createReadStream</code>创建的,所以他是一个可读流,可读流有一个很方便的API可以直接让内容流动到可写流:<code>readable.pipe</code>,使用这个API,<code>Node.js</code>会自动将可读流里面的内容推送到可写流,数据流会被自动管理,所以即使可读流更快,目标可写流也不会超负荷,而且即使你文件很大,因为不是一次读入内存,而是流式读入,所以也不会爆。所以我们在<code>Koa</code>的<code>respond</code>里面支持下流式<code>body</code>就行了:

<code>Koa</code>源码对于流的处理看这里:https://github.com/koajs/koa/blob/master/lib/application.js#L267

现在,我们可以用自己写的<code>koa-static</code>来替换官方的了,运行效果是一样的。最后我们再来回顾下本文的要点:

本文是<code>Koa</code>常用静态服务中间件<code>koa-static</code>的源码解析。

由于是一个<code>Koa</code>的中间件,所以<code>koa-static</code>的返回值是一个方法,而且需要符合中间件范式: <code>(ctx, next) =&gt; {}</code>

作为一个静态服务中间件,<code>koa-static</code>本应该完成以下几件事情:

但是这几件事情他一件也没干,都扔给<code>koa-send</code>了,所以他官方文档也说了他只是<code>wrapper for koa-send.</code>

作为一个<code>wrapper</code>他还支持了一个比较特殊的配置项<code>opt.defer</code>,这个配置项可以控制他在所有<code>Koa</code>中间件里面的执行时机,其实就是调用<code>next</code>的时机。如果你给这个参数传了<code>true</code>,他就先调用<code>next</code>,让其他中间件先执行,自己最后执行,反之亦然。有了这个参数,你可以将<code>/test.jpg</code>这种请求先作为普通路由处理,路由没匹配上再尝试静态文件,这在某些场景下很有用。

<code>koa-send</code>才是真正处理静态文件,他把前面说的三件事全干了,在拼接文件路径时还使用了<code>resolvePath</code>来防御常见攻击。

<code>koa-send</code>取文件时使用了<code>fs</code>模块的API创建了一个可读流,并将它赋值给<code>ctx.body</code>,同时设置了<code>ctx.type</code>。

通过<code>ctx.type</code>和<code>ctx.body</code>返回给请求者并不是<code>koa-send</code>的功能,而是<code>Koa</code>本身的功能。由于<code>http</code>模块提供和的<code>res</code>本身就是一个可写流,所以我们可以通过可读流的<code>pipe</code>函数直接将<code>ctx.body</code>绑定到<code>res</code>上,剩下的工作<code>Node.js</code>会自动帮我们完成。

使用流(<code>Stream</code>)来读写文件有以下几个优点:

不用一次性将文件读入内存,暂用内存小。

如果文件很大,一次性读完整个文件,可能耗时较长。使用流,可以一点一点读文件,读到一点就可以返回给<code>response</code>,有更快的响应时间。

<code>Node.js</code>可以在可读流和可写流之间使用管道进行数据传输,使用也很方便。

<code>koa-static</code>文档:https://github.com/koajs/static

<code>koa-static</code>源码:https://github.com/koajs/static/blob/master/index.js

<code>koa-send</code>文档:https://github.com/koajs/send

<code>koa-send</code>源码:https://github.com/koajs/send/blob/master/index.js

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

欢迎关注我的公众号进击的大前端第一时间获取高质量原创~

“前端进阶知识”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

“前端进阶知识”系列文章源码GitHub地址: https://github.com/dennis-jiang/Front-End-Knowledges

手写koa-static源码,深入理解静态服务器原理

继续阅读