当前位置: 首页 > 后端技术 > Node.js

koa源码阅读[3]-koa-send及其派生(静态)

时间:2023-04-03 11:19:47 Node.js

koa源码阅读第四部分涉及向接口请求方提供文件数据。第一篇:koa源码阅读-0第二篇:koa源码阅读-1-koa和koa-compose是从服务端来的,一定不能放过接口阅读的所有权限。各种路径的验证,权限的匹配,都是需要考虑的事情。而koa-send和koa-static就是帮助我们处理这些繁琐事情的中间件。koa-send是koa-static的基础,在npm界面可以看到,koa-send包含在静态依赖中。koa-send主要用来更方便的处理静态文件。与koa-router等中间件不同,它不是直接作为函数注入到app.use中。而是在一些中间件中调用,传入当前请求的Context和文件对应的位置,然后实现功能。koa-send原生文件读取和传输方式的GitHub地址在Node.js中。如果使用原生的fs模块进行文件数据传输,操作大致是这样的:constfs=require('fs')constkoa=require('koa')constRouter=require('koa-router')constapp=newKoa()constrouter=newRouter()constfile='./test.log'constport=12306router.get('/log',ctx=>{constdata=fs.readFileSync(file).toString()ctx.body=data})app.use(router.routes())app.listen(port,()=>console.log(`服务器运行为http://127.0.0.1:${port}`))或者使用createReadStream而不是readFileSync也是可行的。下面会讲到区别。这个简单的例子只对一个文件进行操作,如果我们要读取的文件有很多,甚至可能通过接口参数传递。所以很难保证这个文件一定真的存在,我们可能还需要添加一些权限设置,防止一些敏感文件被接口返回。router.get('/file',ctx=>{const{fileName}=ctx.queryconstpath=path.resolve('./XXX',fileName)//过滤隐藏文件if(path.startsWith('.')){ctx.status=404return}//判断文件是否存在if(!fs.existsSync(path)){ctx.status=404return}//balabalaconstrs=fs.createReadStream(path)ctx.body=rs//koa已经对stream类型做了处理,详见之前的koa文章})加入各种逻辑判断后,读取静态文件就安全多了,但这只是在router中做的处理。如果有多个读取静态文件的接口,必然会有很多重复的逻辑,将其提炼成一个公共函数会是一个不错的选择。koa-send的方式koa-send就是这样做的,提供了一个处理静态文件的中间件,封装的非常完整。下面是两个最基本的用法示例:constpath=require('path')constsend=require('koa-send')//getrouter.get('/file',asyncfor某个路径下的文件ctx=>{awaitsend(ctx,ctx.query.path,{root:path.resolve(__dirname,'./public')})})//Getrouter.get('/index'forafile,asyncctx=>{awaitsend(ctx,'./public/index.log')})假设我们的目录结构是这样的,simple-send.js就是可执行文件:.├──public│├──a.log│├──b.log│└──index.log└──simple-send.js可以通过/file?path=XXX轻松访问public下的文件。并且你可以通过访问/index来获取/public/index.log文件的内容。koa-send提供的函数koa-send提供了很多方便的选项,除了常用的root,大概有十个选项可以使用:optionstypedefaultdescmaxageNumber0设置浏览器可以缓存的毫秒数
对应的Header:Cache-Control:max-age=XXXimmutableBooleanfalse通知浏览器该URL对应的资源是不可变的,可以无限缓存
对应的Header:Cache-Control:max-age=XXX,immutablehiddenBooleanfalse支持隐藏文件Files从
开始。称为隐藏文件。rootString-设置静态文件路径的根目录,禁止访问该目录以外的任何文件。indexString-设置一个默认文件名,在访问目录时生效,自动拼接在路径后面(这里是一个小彩蛋)gzipBooleantrue如果访问接口的客户端支持gzip,和有一个.gz后缀的同名文件下面将把.gz文件brotliBooleantrue传给上面一样的逻辑。如果支持brotli并且有.br后缀的同名文件formatBooleantrue,开启后不会强制在路径末尾添加/。/path和/path/代表一个路径(仅当path是目录时)extensionsArrayfalse如果传入一个数组,它会尝试匹配数组中的所有项作为文件的后缀,并读取它匹配的文件。setHeadersFunction——用来手动指定一些headers,无意义的参数有些参数的具体表现可以达到一些神奇的效果,有些参数会影响Header,有些参数用来优化性能,类似于gzip和brotli选项。koa-send的主要逻辑可以分为这几个部分:路径的有效性检查,gzip等压缩逻辑的应用文件后缀,默认入口文件的匹配。读取文件数据在函数开头有这样的逻辑:constresolvePath=require('resolve-path')const{parse}=require('path')asyncfunctionsend(ctx,path.opts={}){consttrailingSlash=path[path.length-1]==='/'constindex=opts.index//这里省略各种参数的初始值设置path=path.substr(parse(path).root.length)//...//规范化路径path=decode(path)//内部调用的是`decodeURIComponent`//也就是说传入转义路径也可以正常使用if(index&&trailingSlash)path+=indexpath=resolvePath(root,path)//隐藏文件支持,忽略if(!hidden&&isHidden(root,path))return}functionisHidden(root,path){path=path.substr(root.length).split(sep)for(leti=0;i{awaitsend(ctx,'/',{root:'./public',index:'index'})})//>curlhttp://127.0.0.1:12306/surprises//hello这里使用了上面的逻辑处理,首先是trailingSlash的判断,如果以/结尾,则拼接索引,如果当前路径匹配到某个目录,则重新拼接索引。所以简单的/加上index参数就可以直接得到/index/index.一个小小的彩蛋,实际开发中应该很少这么玩的。最后的文件读取操作终于来到了文件读取的逻辑处理。首先是调用setHeaders的操作。由于上面的层层筛选,这里得到的路径和你调用send时传入的路径不是同一个路径。不过在setHeaders函数中没必要处理,因为可以看到函数最后返回的是实际路径。send执行完我们就可以完全设置了。至于官方readme中写的是什么,之后再做就来不及了,因为标题已经发送了。这个不用担心,因为koa的返回数据是放在ctx.body里面的,所有的中间件都执行完才会处理body的解析。也就是说http请求体要等到所有的中间件都执行完才会发送,之前设置的Header才有效。if(setHeaders)setHeaders(ctx.res,path,stats)//streamctx.set('Content-Length',stats.size)if(!ctx.response.get('Last-Modified'))ctx.set('Last-Modified',stats.mtime.toUTCString())if(!ctx.response.get('Cache-Control')){const指令=['max-age='+(maxage/1000|0)]如果(不可变){directives.push('immutable')}ctx.set('Cache-Control',directives.join(','))}if(!ctx.type)ctx.type=type(path,encodingExt)//接口返回的数据类型默认会取出文件后缀ctx.body=fs.createReadStream(path)。返回路径和上面的maxage和immutable在这里都是有效的,但是需要注意的是,如果Cache-Control的值已经存在,koa-send不会覆盖它。使用Stream和使用readFile的区别可以在body赋值的最后一个地方看出。它使用Stream而不是readFile。使用Stream进行传输至少可以带来两个好处:第一种方式,如果是大文件,在读取完成后会暂存在内存中,toString是有长度限制的。如果它是一个巨大的文件,toString调用将抛出异常。第一种读取文件的方式是读取完所有数据后返回给接口调用者。数据读取期间,接口处于Wait状态,无数据返回。你可以做一个这样的演示:consthttp=require('http')constfs=require('fs')constfilePath='./test.log'http.createServer((req,res)=>{if(req.url==='/'){res.end('')}elseif(req.url==='/sync'){constdata=fs.readFileSync(filePath).toString()res.end(data)}elseif(req.url==='/pipe'){constrs=fs.createReadStream(filePath)rs.pipe(res)}else{res.end('404')}}).listen(12306,()=>console.log('serverrunashttp://127.0.0.1:12306'))首先访问首页http://127.0.0.1:12306/进入一个空页面_(主要是懒得搞CORS了)_,然后在控制台调用两次fetch得到这样的对比结果:可以看出虽然下行传输时间差不多,但是方法使用readFileSync会增加一定的等待时间,而这个时间是服务器正在读取文件的时候。时间的长短取决于要读取的文件的大小和机器的性能。koa-statickoa-static是一个基于koa-send的浅包。因为从上面的例子也可以看出,中间件本身需要调用send方法。手动指定发送对应的路径等参数。这些也是重复的操作,所以koa-static将这些逻辑封装了一次。让我们通过直接注册一个中间件来完成对静态文件的处理,不再需要关心参数的读取:constKoa=require('koa')constapp=newKoa()app.use(require('koa-static')(root,opts))opts是透明传递给koa-send的,但是第一个参数root会用来覆盖opts中的root。并添加了一些详细操作:默认添加一个index.htmlif(opts.index!==false)opts.index=opts.index||‘index.html’默认只针对HEAD和GET先执行其他中间件。如果defer为false,则先执行send,先匹配静态文件。否则,会等到其余的中间件先执行完毕,在确认其他中间件没有处理完请求后,再去寻找对应的静态资源。只需要指定root,剩下的工作就交给koa-static,这样我们就不用关心静态资源应该怎么处理了。总结koa-send和koa-static是两个非常轻量级的中间件。它本身并没有太复杂的逻辑,只是将一些重复的逻辑提炼成中间件。不过,它确实可以减少很多日常开发中的任务量,让人更专注于业务而不是这些角落的功能。

猜你喜欢