send是一个用于从文件系统以流的方式读取文件作为http
响应结果的库。说的再更通俗一些,就是在Node
中提供静态文件的托管服务,比如像express
的static
服务。还有像熟知的serve-static
中间件背后也是依赖send
进行的中间件封装。
本文将基于send
库1.0.0-beta.1
版本的源码做如下几个方面的讲解:
源码/原理解析类的文章代码会比较多,小伙伴要耐心哦!!! 精华都在代码里!!! 下面👇我们先看看该库是如何使用的吧。
基本使用
下面演示一个在Node
中利用send
对于所有http
请求都返回根目录下的static/index.html
文件资源的例子:
const http = require('http');
const path = require('path');
const send = require('send')
// 初始化一个http服务
const server = http.createServer(function onRequest (req, res) {
send(req, './index.html', {
// 指定返回资源的根路径
root: path.join(process.cwd(), 'static'),
}).pipe(res);
});
server.listen(3000, () => {
console.log('server is running at port 3000.');
});
复制代码
复制代码
除了这个示例外,比如像live-server
库中也是利用send
提供了静态文件托管服务。学会了基本使用,下面看看send
静态文件托管服务的实现原理吧。
源码分析
send
库对外暴露一个send
方法,该方法内初始化一个SendStream
类,SendStream
类继承Stream
模块,同时实现pipe
等实例方法。主体代码结构如下:
var path = require('path')
var Stream = require('stream')
/**
* Path 模块一些方法的快捷引用
*/
var extname = path.extname
var join = path.join
var normalize = path.normalize
var resolve = path.resolve
var sep = path.sep
/**
* 对外暴露的send函数
* 没有直接暴露SendStream类的原因主要是去掉new的调用
* @public
*/
module.exports = send
/**
* 对外暴露的send方法,接收req请求,返回`SendStream`得到的文件流
* @param {object} req http模块等req请求
* @param {string} path 要匹配的静态资源路径
* @param {object} [options] 可选参数
*/
function send (req, path, options) {
return new SendStream(req, path, options)
}
function SendStream (req, path, options) {
// ES5方式继承Stream模块
Stream.call(this)
var opts = options || {}
this.options = opts
this.path = path
this.req = req
// ... 其他一些初始化参数赋值的操作
this._root = opts.root
? resolve(opts.root)
: null
}
SendStream.prototype.pipe = function pipe (res) {}
// ES5方式继承Stream模块
util.inherits(SendStream, Stream)
复制代码
复制代码
这里注意 Node 中老语法实现继承的方法:
// 构造函数内调用call
Stream.call(this);
// 构造方法外部调用util.inherits
util.inherits(SendStream, Stream)
复制代码
复制代码
通过一开始的使用示例,我们知道在使用send
库时,主要是通过调用send
函数得到的实例pipe
方法,下面看下pipe
的实现:
SendStream.prototype.pipe = function pipe (res) {
// 根路径
var root = this._root
// 保存res引用
this.res = res
// 对path进行decodeURIComponent解码
var path = decode(this.path)
// 解码失败直接返回res
if (path === -1) {
this.error(400)
return res
}
// null byte(s)
if (~path.indexOf('\0')) {
this.error(400)
return res
}
var parts
if (root !== null) {
// 将path规范化成./path
if (path) {
path = normalize('.' + sep + path)
}
// malicious path
if (UP_PATH_REGEXP.test(path)) {
debug('malicious path "%s"', path)
this.error(403)
return res
}
// 根据路径符合分割path
parts = path.split(sep)
// join / normalize from optional root dir
// 将根路径拼接起来
path = normalize(join(root, path))
} else {
// ".." is malicious without "root"
if (UP_PATH_REGEXP.test(path)) {
debug('malicious path "%s"', path)
this.error(403)
return res
}
// normalize用于规范化path,可以解析..或.等路径符合
// sep提供特定于平台的路径片段分隔符
// parts得到的是根据路径分隔符分割到的字符串数组
parts = normalize(path).split(sep)
// resolve the path
// 系列化为绝对路径
path = resolve(path)
}
// 处理点开通的文件,例如.cache
if (containsDotFile(parts)) {
debug('%s dotfile "%s"', this._dotfiles, path)
switch (this._dotfiles) {
case 'allow':
break
case 'deny':
this.error(403)
return res
case 'ignore':
default:
this.error(404)
return res
}
}
// 处理pathname以"/"结尾的情况
if (this._index.length && this.hasTrailingSlash()) {
this.sendIndex(path)
return res
}
this.sendFile(path)
return res
}
复制代码
复制代码
这里有一个小细节需要注意,就是按路径分隔符分割 url 路径时,没有直接使用/
符合,而是使用了跨平台的path.sep
:
parts = path.split(sep)
复制代码
复制代码
接下来我们继续往后看,sendIndex
方法的主要逻辑是根据要匹配的path
参数为/
结尾时,尝试匹配path/index.html
或以用户设置的index
值优先。
/**
* 尝试从path转换成index值
* Eg:path/ => path/index.html
* @param {String} path
* @api private
*/
SendStream.prototype.sendIndex = function sendIndex (path) {
var i = -1
var self = this
function next (err) {
// 如果用户设置的所有index值都没有匹配到,则抛出错误
// index默认值是["index.html"],即当访问path/时,指定到path/index.html
if (++i >= self._index.length) {
if (err) return self.onStatError(err)
return self.error(404)
}
// path拼接index
var p = join(path, self._index[i])
debug('stat "%s"', p)
// 判断新的index路径是否存在
fs.stat(p, function (err, stat) {
// 不存在则继续尝试下一个index
if (err) return next(err)
// 如果新的index路径是文件夹,继续尝试下一个index
if (stat.isDirectory()) return next()
// 如果是文件,则emit file事件
self.emit('file', p, stat)
// 调用send返回流数据
self.send(p, stat)
})
}
next()
}
复制代码
复制代码
sendIndex
内部在尝试拼接path/index
后,如果资源存在,则判断是文件夹还是文件资源:
在确定路径最终映射到资源后,最终调用 send 进行处理的,那么我们接着看send
方法实现:
SendStream.prototype.send = function send (path, stat) {
var len = stat.size
var options = this.options
var opts = {}
var res = this.res
var req = this.req
var ranges = req.headers.range
var offset = options.start || 0
// 无法发送的抛错处理
if (res.headersSent) {
// impossible to send now
this.headersAlreadySent()
return
}
debug('pipe "%s"', path)
// 设置res的headers请求头相关字段
this.setHeader(path, stat)
// 设置请求头的Content-Type值
this.type(path)
// conditional GET support
if (this.isConditionalGET()) {
if (this.isPreconditionFailure()) {
this.error(412)
return
}
if (this.isCachable() && this.isFresh()) {
this.notModified()
return
}
}
// adjust len to start/end options
len = Math.max(0, len - offset)
if (options.end !== undefined) {
var bytes = options.end - offset + 1
if (len > bytes) len = bytes
}
// Range support
if (this._acceptRanges && BYTES_RANGE_REGEXP.test(ranges)) {
// parse
ranges = parseRange(len, ranges, {
combine: true
})
// If-Range support
if (!this.isRangeFresh()) {
debug('range stale')
ranges = -2
}
// unsatisfiable
if (ranges === -1) {
debug('range unsatisfiable')
// Content-Range
res.setHeader('Content-Range', contentRange('bytes', len))
// 416 Requested Range Not Satisfiable
return this.error(416, {
headers: { 'Content-Range': res.getHeader('Content-Range') }
})
}
// valid (syntactically invalid/multiple ranges
// are treated as a regular response)
if (ranges !== -2 && ranges.length === 1) {
debug('range %j', ranges)
// Content-Range
res.statusCode = 206
res.setHeader(
'Content-Range',
contentRange('bytes', len, ranges[0])
)
// adjust for requested range
offset += ranges[0].start
len = ranges[0].end - ranges[0].start + 1
}
}
// clone options
for (var prop in options) {
opts[prop] = options[prop]
}
// set read options
opts.start = offset
opts.end = Math.max(offset, offset + len - 1)
// 设置Content-Length
res.setHeader('Content-Length', len)
/**
* 支持HEAD请求
* HEAD请求也是用于请求资源,但是服务器不会返回请求资源的实体数据,
* 只会传回响应头,也就是元信息
*/
if (req.method === 'HEAD') {
res.end()
return
}
// 调用stream方法返回文件流数据
this.stream(path, opts)
}
复制代码
复制代码
send
方法代码稍微长了些,首先是设置了返回资源的请求头相关字段:
这里有意思的是,send
内部支持了HEAD
请求,HEAD
请求与GET
请求的区别在于HEAD
只返回请求头相关信息,不返回资源的实体数据:
/**
* 支持HEAD请求
* HEAD请求也是用于请求资源,但是服务器不会返回请求资源的实体数据,
* 只会传回响应头,也就是元信息
*/
if (req.method === 'HEAD') {
// 注意在此之前的代码只处理了响应头相关数据,但是并未处理响应体数据
// 因此在调用end之后是没有实体数据的
res.end()
return
}
复制代码
复制代码
send
方法内部最后调用stream
方法返回文件流数据,下面看下stream
方法的实现:
SendStream.prototype.stream = function stream (path, options) {
// TODO: this is all lame, refactor meeee
var finished = false
var self = this
var res = this.res
/**
* 创建一个可读流
* emit一个stream事件,让外部可以在该事件钩子中继续处理stream
*/
var stream = fs.createReadStream(path, options)
this.emit('stream', stream)
// 将流传递给res响应
stream.pipe(res)
// response finished, done with the fd
// 响应结束,销毁流
onFinished(res, function onfinished () {
finished = true
destroy(stream)
})
// 错误处理,销毁流
stream.on('error', function onerror (err) {
// request already finished
if (finished) return
// clean up stream
finished = true
destroy(stream)
// error
self.onStatError(err)
})
// 流读取结束
stream.on('end', function onend () {
self.emit('end')
})
}
复制代码
复制代码
stream
内部的实现才是本库的核心部分,首先通过fs
模块创建一个可读流读取文件内容,同时对外暴露一个stream
事件,让外部有机会在创建流后做一些处理逻辑:
/**
* 创建一个可读流
* emit一个stream事件,让外部可以在该事件钩子中继续处理stream
*/
var stream = fs.createReadStream(path, options)
this.emit('stream', stream)
// 将流传递给res响应
stream.pipe(res)
复制代码
复制代码
最后在流出错或者响应结束时销毁流,在流读取结束时暴露一个end
事件。
下面我们回到pipe
方法内部,对于path
不是/
结尾的调用sendFile
逻辑:
SendStream.prototype.pipe = function pipe (res) {
// ... 省略前面的代码
// 处理pathname以"/"结尾的情况
if (this._index.length && this.hasTrailingSlash()) {
this.sendIndex(path)
return res
}
this.sendFile(path)
return res
}
复制代码
复制代码
下面看下sendFile
逻辑:
SendStream.prototype.sendFile = function sendFile (path) {
var i = 0
var self = this
debug('stat "%s"', path)
fs.stat(path, function onstat (err, stat) {
// 如果文件资源不存在,且没有文件后缀名,
// 则调用next方法拼接.html等后缀名继续尝试尝试
if (err && err.code === 'ENOENT'
&& !extname(path)
&& path[path.length - 1] !== sep
) {
// not found, check extensions
return next(err)
}
if (err) return self.onStatError(err)
// 如果是文件夹,则重定向
if (stat.isDirectory()) return self.redirect(path)
// 如果是文件,则emit file事件,
self.emit('file', path, stat)
// 利用send方法返回流
self.send(path, stat)
})
function next (err) {
if (self._extensions.length <= i) {
return err
? self.onStatError(err)
: self.error(404)
}
var p = path + '.' + self._extensions[i++]
debug('stat "%s"', p)
fs.stat(p, function (err, stat) {
if (err) return next(err)
if (stat.isDirectory()) return next()
self.emit('file', p, stat)
self.send(p, stat)
})
}
}
复制代码
复制代码
这时的主要做法是判断path
对应的资源是否存在:
send 静态服务原理总结
send
库的核心还是在于根据path
路径映射的资源,通过fs.createReadStream
进行读取流,然后通过stream.pipe(res)
进行消费流。
另一个比较有意思的点就是实现了HEAD
请求,只返回请求头,不返沪请求的实体数据。
最后
如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163 相互学习,我们会有专业的技术答疑解惑
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点 star:http://github.crmeb.net/u/defu不胜感激 !
PHP 学习手册:https://doc.crmeb.com
技术交流论坛:https://q.crmeb.com
评论