koa
源码浏览的第四篇,涉及到向接口请求方供应文件数据。
第一篇:
koa源码浏览-0第二篇:
koa源码浏览-1-koa与koa-compose第三篇:
koa源码浏览-2-koa-router
处置惩罚静态文件是一个烦琐的事变,由于静态文件都是来自于服务器上,肯定不能摊开一切权限让接口来读取。
种种途径的校验,权限的婚配,都是须要考虑到的处所。
而koa-send
和koa-static
就是协助我们处置惩罚这些烦琐事变的中间件。 koa-send
是koa-static
的基本,可以在NPM
的界面上看到,static
的dependencies
中包含了koa-send
。
koa-send
主假如用于更随意马虎的处置惩罚静态文件,与koa-router
之类的中间件差别的是,它并非直接作为一个函数注入到app.use
中的。
而是在某些中间件中举行挪用,传入当前请求的Context
及文件对应的位置,然后完成功用。
koa-send的GitHub地点
在Node
中,假如运用原生的fs
模块举行文件数据传输,大抵是如许的操纵:
const fs = require('fs')
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
const file = './test.log'
const port = 12306
router.get('/log', ctx => {
const data = fs.readFileSync(file).toString()
ctx.body = data
})
app.use(router.routes())
app.listen(port, () => console.log(`Server run as http://127.0.0.1:${port}`))
或许用createReadStream
替代readFileSync
也是可行的,区分会在下边提到
这个简朴的示例仅针对一个文件举行操纵,而假如我们要读取的文件是有许多个,甚至于多是经由过程接口参数通报过来的。
所以很难保证这个文件肯定是实在存在的,而且我们可以还须要增添一些权限设置,防备一些敏感文件被接口返回。
router.get('/file', ctx => {
const { fileName } = ctx.query
const path = path.resolve('./XXX', fileName)
// 过滤隐蔽文件
if (path.startsWith('.')) {
ctx.status = 404
return
}
// 推断文件是不是存在
if (!fs.existsSync(path)) {
ctx.status = 404
return
}
// balabala
const rs = fs.createReadStream(path)
ctx.body = rs // koa做了针对stream范例的处置惩罚,概况可以看之前的koa篇
})
增添了种种逻辑推断今后,读取静态文件就变得平安不少,然则这也只是在一个router
中做的处置惩罚。
假如有多个接口都邑举行静态文件的读取,势必会存在大批的反复逻辑,所以将其提炼为一个大众函数将是一个很好的挑选。
这就是koa-send
做的事变了,供应了一个封装非常完美的处置惩罚静态文件的中间件。
这里是两个最基本的运用例子:
const path = require('path')
const send = require('koa-send')
// 针对某个途径下的文件猎取
router.get('/file', async ctx => {
await send(ctx, ctx.query.path, {
root: path.resolve(__dirname, './public')
})
})
// 针对某个文件的猎取
router.get('/index', async ctx => {
await send(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
供应了许多便民的选项,撤除经常使用的root
以外,另有也许小十个的选项可供运用:
options | type | default | desc |
---|---|---|---|
maxage | Number | 0 | 设置浏览器可以缓存的毫秒数 对应的 Header : Cache-Control: max-age=XXX |
immutable | Boolean | false | 关照浏览器该URL对应的资本不可变,可以无限期的缓存 对应的 Header : Cache-Control: max-age=XXX, immutable |
hidden | Boolean | false | 是不是支撑隐蔽文件的读取. 开首的文件被称为隐蔽文件 |
root | String | – | 设置静态文件途径的根目次,任何该目次以外的文件都是制止接见的。 |
index | String | – | 设置一个默许的文件名,在接见目次的时刻见效,会自动拼接到途径后边 (此处有一个小彩蛋) |
gzip | Boolean | true | 假如接见接口的客户端支撑gzip ,而且存在.gz 后缀的同名文件的情况下会通报.gz 文件 |
brotli | Boolean | true | 逻辑同上,假如支撑brotli 且存在.br 后缀的同名文件 |
format | Boolean | true | 开启今后不会强请求途径末端的/ ,/path 和/path/ 示意的是一个途径 (仅在path 是一个目次的情况下见效) |
extensions | Array | false | 假如通报了一个数组,会尝试将数组中的一切item 作为文件的后缀举行婚配,婚配到哪一个就读取哪一个文件 |
setHeaders | Function | – | 用来手动指定一些Headers ,意义不大 |
有些参数的搭配可以完成一些奇异的效果,有一些参数会影响到Header
,也有一些参数是用来优化机能的,相似gzip
和brotli
的选项。
koa-send
的重要逻辑可以分为这几块:
path
途径有用性的搜检gzip
等紧缩逻辑的运用在函数的开首部份有如许的逻辑:
const resolvePath = require('resolve-path')
const {
parse
} = require('path')
async function send (ctx, path. opts = {}) {
const trailingSlash = path[path.length - 1] === '/'
const index = opts.index
// 此处省略种种参数的初始值设置
path = path.substr(parse(path).root.length)
// ...
// normalize path
path = decode(path) // 内部挪用的是`decodeURIComponent`
// 也就是说传入一个转义的途径也是可以一般运用的
if (index && trailingSlash) path += index
path = resolvePath(root, path)
// hidden file support, ignore
if (!hidden && isHidden(root, path)) return
}
function isHidden (root, path) {
path = path.substr(root.length).split(sep)
for (let i = 0; i
}
return false
}
起首是推断传入的path
是不是为一个目次,_(末端为/
会被以为是一个目次)_。
假如是目次,而且存在一个有用的index
参数,则会将index
拼接到path
后边。
也就是也许如许的操纵:
send(ctx, './public/', {
index: 'index.js'
})
// ./public/index.js
resolve-path 是一个用来处置惩罚途径的包,用来协助过滤一些非常的途径,相似path//file
、/etc/XXX
如许的歹意途径,而且会返回处置惩罚后绝对途径。
isHidden
用来推断是不是须要过滤隐蔽文件。
由于但通常.
开首的文件都邑被以为隐蔽文件,同理目次运用.
开首也会被以为是隐蔽的,所以就有了isHidden
函数的完成。
实在我个人以为这个运用一个正则就可以处理的题目。。为何还要分割为数组呢?
function isHidden (root, path) {
path = path.substr(root.length)
return new RegExp(`${sep}\\.`).test(path)
}
已给社区提交了PR
。
在上边的这一坨代码实行完今后,我们就获得了一个有用的途径,_(假如是无效途径,resolvePath
会直接抛出非常)_
接下来做的事变就是搜检是不是有可用的紧缩文件运用,此处没有什么逻辑,就是简朴的exists
操纵,以及Content-Encoding
的修正 _(用于开启紧缩)_。
后缀的婚配:
if (extensions && !/\.[^/]*$/.exec(path)) {
const list = [].concat(extensions)
for (let i = 0; i
if (typeof ext !== 'string') {
throw new TypeError('option extensions must be array of strings or false')
}
if (!/^\./.exec(ext)) ext = '.' + ext
if (await fs.exists(path + ext)) {
path = path + ext
break
}
}
}
可以看到这里的遍历是完整根据我们挪用send
是传入的递次来走的,而且还做了.
标记的兼容。
也就是说如许的挪用都是有用的:
await send(ctx, 'path', {
extensions: ['.js', 'ts', '.tsx']
})
假如在增添了后缀今后可以婚配到实在的文件,那末就以为这是一个有用的途径,然后举行了break
的操纵,也就是文档中所说的:First found is served.
。
在终了这部份操纵今后会举行目次的检测,推断当前途径是不是为一个目次:
let stats
try {
stats = await fs.stat(path)
if (stats.isDirectory()) {
if (format && index) {
path += '/' + index
stats = await fs.stat(path)
} else {
return
}
}
} catch (err) {
const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']
if (notfound.includes(err.code)) {
throw createError(404, err)
}
err.status = 500
throw err
}
可以发明一个很有意义的事变,假如发明当前途径是一个目次今后,而且明白指定了format
,那末还会再尝试拼接一次index
。
这就是上边所说的谁人彩蛋了,当我们的public
途径构造长得像如许的时刻:
└── public
└── index
└── index # 现实的文件 hello
我们可以经由过程一个简朴的体式格局猎取到最底层的文件数据:
router.get('/surprises', async ctx => {
await send(ctx, '/', {
root: './public',
index: 'index'
})
})
// > curl http://127.0.0.1:12306/surprises
// hello
这里就用到了上边的几个逻辑处置惩罚,起首是trailingSlash
的推断,假如以/
末端会拼接index
,以及假如当前path
婚配为是一个目次今后,又会拼接一次index
。
所以一个简朴的/
加上index
的参数就可以直接猎取到/index/index
。
一个小小的彩蛋,现实开辟中应当很少会这么玩
末了终究来到了文件读取的逻辑处置惩罚,起首就是挪用setHeaders
的操纵。
由于经由上边的层层挑选,这里拿到的path
和你挪用send
时传入的path
不是同一个途径。
不过倒也没有必要必须在setHeaders
函数中举行处置惩罚,由于可以看到在函数终了时,将现实的path
返回了出来。
我们完整可以在send
实行终了后再举行设置,至于官方readme
中所写的and doing it after is too late because the headers are already sent.
。
这个不须要忧郁,由于koa
的返回数据都是放到ctx.body
中的,而body
的剖析是在一切的中间件悉数实行完今后才会举行处置惩罚。
也就是说一切的中间件都实行完今后才会最先发送http
请求体,在此之前设置Header
都是有用的。
if (setHeaders) setHeaders(ctx.res, path, stats)
// stream
ctx.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 directives = ['max-age=' + (maxage / 1000 | 0)]
if (immutable) {
directives.push('immutable')
}
ctx.set('Cache-Control', directives.join(','))
}
if (!ctx.type) ctx.type = type(path, encodingExt) // 接口返回的数据范例,默许会掏出文件后缀
ctx.body = fs.createReadStream(path)
return path
以及包含上边的maxage
和immutable
都是在这里见效的,然则要注意的是,假如Cache-Control
已存在值了,koa-send
是不会去掩盖的。
在末了给body
赋值的位置可以看到,是运用的Stream
而并非是readFile
,运用Stream
举行传输能带来最少两个优点:
toString
是有长度限定的,假如是一个庞大的文件,toString
挪用会抛出非常的。Wait
的状况,没有任何数据返回。可以做一个相似如许的Demo:
const http = require('http')
const fs = require('fs')
const filePath = './test.log'
http.createServer((req, res) => {
if (req.url === '/') {
res.end('')
} else if (req.url === '/sync') {
const data = fs.readFileSync(filePath).toString()
res.end(data)
} else if (req.url === '/pipe') {
const rs = fs.createReadStream(filePath)
rs.pipe(res)
} else {
res.end('404')
}
}).listen(12306, () => console.log('server run as http://127.0.0.1:12306'))
起首接见首页http://127.0.0.1:12306/
进入一个空的页面 _(主假如懒得搞CORS
了)_,然后在控制台挪用两个fetch
就可以获得如许的对照效果了:
可以看出在下行传输的时候相差无几的同时,运用readFileSync
的体式格局会增添肯定时候的Waiting
,而这个时候就是服务器在举行文件的读取,时候是非取决于读取的文件大小,以及机械的机能。
koa-static
是一个基于koa-send
的浅封装。
由于经由过程上边的实例也可以看到,send
要领须要自身在中间件中挪用才行。
手动指定send
对应的path
之类的参数,这些也是属于反复性的操纵,所以koa-static
将这些逻辑举行了一次封装。
让我们可以经由过程直接注册一个中间件来完成静态文件的处置惩罚,而不再须要体贴参数的读取之类的题目:
const Koa = require('koa')
const app = new Koa()
app.use(require('koa-static')(root, opts))
opts
是透传到koa-send
中的,只不过会运用第一个参数root
来掩盖opts
中的root
。
而且增添了一些细节化的操纵:
index.html
if (opts.index !== false) opts.index = opts.index || 'index.html'
HEAD
和GET
两种METHOD
if (ctx.method === 'HEAD' || ctx.method === 'GET') {
// ...
}
defer
选项来决议是不是先实行其他中间件。假如defer
为false
,则会先实行send
,优先婚配静态文件。
不然则会比及其他中间件先实行,肯定其他中间件没有处置惩罚该请求才会去寻觅对应的静态资本。
只需指定root
,剩下的事情交给koa-static
,我们就无需体贴静态资本应当怎样处置惩罚了。
koa-send
与koa-static
算是两个非常轻量级的中间件了。
自身没有太庞杂的逻辑,就是一些反复的逻辑被提炼成的中间件。
不过确切可以削减许多一样平常开辟中的任务量,可以让人更专注的关注营业,而非这些边边角角的功用。