实现express之三: app.use(express.static('public'))的奥秘

1,106 阅读3分钟

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的系列文章就写到这里, 大家有疑问,或者我写的有错误地方,欢迎大家私信或评论, 对大家的理解有所感悟的话,也辛苦给个赞喔~~