express.static()做了什么
我们在使用express的静态服务时,都会写express.static('xxx')
, 这么简单的一个函数,就把那么庞大的静态服务搞定。所以有必要研究一下了。
app.use()
大家知道这是应用级中间件,如果对use不清楚的朋友,可以看一下实现express之二: 分析express源码
进入正题:
express.static
其实是express的一个静态方法,入口文件在
// 这里是我自己的实现,大家可以对比express的源码,基本一致
exports.static = require('./static_service');
// static_service.js
return function(req, res, next) {
if (req.method !== 'GET' && req.method !== 'HEAD') {
if(fallthrough) {
return next();
}
// 静态文件不允许响应其它方式
res.statusCode = 405;
res.setHeader('Allow', 'GET, HEAD');
res.setHeader('Content-length', '0');
res.end();
return;
}
let path = url.parse(req.url).pathname;
// 创建 send stream
let stream = send(req, path, opts);
// 监听传入的path是directory时的操作
// TODO 这里还是需要实现
// stream.on('directory', onDirectory);
if(setHeaders) {
stream.on('headers', setHeaders);
}
stream.on('error', function error(err) {
// 如果之前已经确定要拦截,或者明确为服务端错误
if(!fallthrough || !(err.statusCode < 500)) {
next(err);
return;
}
// 传给下个中间件执行
next();
});
stream.pipe(res);
}
上面代码中的 stream
来自send()
方法, 来自send.js
function SendStream(req, path, options = {}) {
let opts = options || {};
this.options = options;
this.path = path;
this.req = req;
this._maxage = opts.maxAge || opts.maxage;
this._root = opts.root
? resolve(opts.root)
: null;
// 是否允许分段传输,默认允许
this._acceptRanges = opts.acceptRanges !== undefined
? Boolean(opts.acceptRanges)
: true;
// 是否设置cacheControl
this._cacheControl = opts.cacheControl !== undefined
? Boolean(opts.cacheControl)
: true;
// 默认设置eTag
this._etag = opts.etag !== undefined
? Boolean(opts.etag)
: true;
// 是否设置缓存的 immutable
this._immutable = opts.immutable !== undefined
? Boolean(opts.immutable)
: false;
// 默认有 lastModified
this._lastModified = opts.lastModified !== undefined
? Boolean(opts.lastModified)
: true
}
在其中,我们能够看到很多重要的点, 比如: Cache-Control
, Etag
, Last-Modified
, 我们在请求静态资源的时候,可不就是跟这些在打交道么~~
初始化好后,执行 stream.pipe(res);
, 进入pipe逻辑,代码如下:
function decode (path) {
try {
return decodeURIComponent(path)
} catch (err) {
return -1
}
}
SendStream.prototype.pipe = function(res) {
this.res = res;
let root = this._root;
// %zhangsan
let path = decode(this.path);
if (path === -1) {
this.error(400);
return res;
}
if (root !== null) {
if (path) {
// 为了兼容windows
path = normalize('.' + sep + path);
}
// '/../abc'
if(UP_PATH_REGEXP.test(path)) {
this.error(403);
return res;
}
path = normalize(join(root, path));
}
this.sendFile(path);
return res;
}
这个函数中,进行了一些判断,主要是400和403
相关的判断.
继续向下走,执行 this.sendFile(path);
这里的path就是要请求的资源路径,比如/css/style.css
SendStream.prototype.sendFile = function(path) {
let i = 0;
const self = this;
fs.stat(path, function(err, stat) {
// 如果文件路径不存在 && 文件结尾不是 .xxx
if (err && err.code === 'ENOENT' && !extname(path)) {
return next(err);
}
if (err) {
return self.onStatError(err);
}
if(stat.isDirectory()) {
return self.redirect(path);
}
// 只有最后符合的才能真正去用stream读取
self.send(path, stat);
});
function next(err) {
return err
? self.onStatError(err)
: self.error(404)
}
}
继续对path判断,如果存在这个路径,并且不是directory,那么进入self.send()
逻辑。 注意:对目录的操作我现在还没有实现,所以先忽略,照着我的思路继续向下走~~
function headersSent (res) {
return typeof res.headersSent !== 'boolean'
? Boolean(res._header)
: res.headersSent
}
// 下面的send函数就是核心了
SendStream.prototype.send = function(path, stat) {
let len = stat.size;
let options = this.options;
let opts = {};
let req = this.req;
let res = this.res;
// 如果已经发送过请求头了,那么提示并返回
if(headersSent(res)) {
this.headerAlreaySent();
return;
}
// 设置header
this.setHeader(path, stat);
// 设置 Content-Type
this.type(path);
// 判断是否有条件的get请求
if (this.isConditionalGET()) {
// 检查请求前提条件是否失败
if (this.isPreconditionFailure()) {
// 客户端错误(先决条件失败), 拒绝为客户端提供服务
this.err(412);
return;
}
if(this.isCachable() && this.isFresh()) {
this.notModified();
return;
}
}
// 走到这里,说明静态资源已经不是fresh的了,需要获取一个最新的
// TODO Range support
// ...
// clone options
for (let prop in options) {
opts[prop] = options[prop]
}
// content-length
res.setHeader('Content-Length', len);
// HEAD请求中,服务器将不传回消息体
// 这个HEAD方法经常用来测试连接的有效性,可用性和最近修改
if(req.method === 'HEAD') {
res.end();
return;
}
this.stream(path, opts);
}
这个方法中,有几个比较重要的点:
this.headerAlreaySent()
这个函数向客户端发送500状态码,并提示Can\'t set headers after they are sent.
this.setHeader(path, stat);
中设置了很多header中非常重要的字段
SendStream.prototype.setHeader = function(path, stat) {
let res = this.res;
this.emit('headers', res, path, stat);
// 支持分段传输
if (this._acceptRanges && !res.getHeader('Accept-Ranges')) {
res.setHeader('Accept-Ranges', 'bytes');
}
// 当cache-Control过期后,res.getHeader('Cache-Control') 就被重置为undefined,所以就会再重新设置Cache-Control
// 注意:当cache-Control的max-age=0,说明不使用强缓存,那么就需要check协商缓存
if (this._cacheControl && !res.getHeader('Cache-Control')) {
let cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000);
// 如果设置了缓存期间的`不可变`属性
if (this._immutable) {
cacheControl += ', immutable';
}
res.setHeader('Cache-Control', cacheControl);
}
// res.getHeader 为读出已排队但未发送到客户端的响应头,所以每一次新的请求到达,都会重新设置Last-Modified
if (this._lastModified && !res.getHeader('Last-Modified')) {
// stat.mtime -> 上次修改此文件的时间戳
let modified = stat.mtime.toUTCString();
res.setHeader('Last-Modified', modified);
}
if(this._etag && !res.getHeader('ETag')) {
// example: W/"417c-1774c2483da"
let val = etag(stat);
res.setHeader('ETag', val);
}
}
this.type(path)
用于设置Content-Type
SendStream.prototype.type = function(path) {
const res = this.res;
if (res.getHeader('Content-Type')) {
return;
}
// 获取到资源属于什么类型,比如 text/css image/png等等
let type = mime.lookup(path);
if (type) {
let charset = mime.charsets.lookup(type);
res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
}
}
this.isConditionalGET()
用于判断是否有条件的查找
SendStream.prototype.isConditionalGET = function() {
return this.req.headers['if-match']
|| this.req.headers['if-unmodified-since']
|| this.req.headers['if-none-match']
|| this.req.headers['if-modified-since']
}
-
this.isPreconditionFailure()
检查请求前提条件是否失败,检查请求前提条件 if-match 和if-unmodified-since (其实这个判断我并没有真正用到,因为没有用到过,现在用的都是if-none-match 和if-modified-since) -
this.isCachable()
检查是否可以进行缓存
SendStream.prototype.isCachable = function() {
// res.statusCode 默认是200
let statusCode = this.res.statusCode;
return (statusCode >= 200 && statusCode < 300)
|| statusCode === 304;
}
this.isFresh()
用来判断Etag和Last-Modified 是否’新鲜(fresh)‘
endStream.prototype.isFresh = function() {
// fresh 去判断 1.etag如果不能与 if-none-match匹配,或者 2.Last-Modified大于if-if-modified-since
// 则判定为不新鲜
return fresh(this.req.headers, {
'etag': this.res.getHeader('ETag'),
'last-modified': this.res.getHeader('Last-Modified')
});
}
this.notModified();
如果发现是可缓存的并且是fresh的,那么就告知客户端可以使用缓存
SendStream.prototype.notModified = function() {
this.removeContentHeaderFields();
// 设置res.statusCode为304
this.res.statusCode = 304;
this.res.end();
}
一步步过关斩将,终于走到了 this.stream(path, opts);
,这时,确定请求已经不新鲜
,并且不是'HEAD'请求,那就就去读最新的文件,并且返回吧~~
SendStream.prototype.stream = function(path, options) {
const res = this.res;
const stream = fs.createReadStream(path, options);
this.emit('stream', stream);
stream.pipe(res);
// 调用destory的原因是:如果可读流在处理时发生错误,目标可写流不会自动关闭,这样就可能会导致内存泄漏
onFinished(res, function() {
destroy(stream);
});
}
以上就是请求一个静态文件的主流程,其中包含了状态码 200
, 3xx
, 4xx
, 5xx
, 当然,为了保持主线的畅通,所以没有对各种边界情况进行详细的讲解。 大家可以按照这种思路,从主干向分支不断推进,相信对静态服务的理解会有一个质的提升。
express的系列文章就写到这里, 大家有疑问,或者我写的有错误地方,欢迎大家私信或评论, 对大家的理解有所感悟的话,也辛苦给个赞喔~~