What does express.static() do

Express.static (‘ XXX ‘) when using a static service for Express, we write express.static(‘ XXX ‘). So it’s worth looking into.

App.use () we know this is application-level middleware, if the use of unclear friends, you can see the implementation of Express two: express source analysis

To get down to business:

Express. static is a static method of express

// Here is my own implementation, you can compare the source of Express, basically the same
exports.static = require('./static_service');

// static_service.js

return function(req, res, next) {
        if(req.method ! = ='GET'&& req.method ! = ='HEAD') {
            if(fallthrough) {
                return next();
            }
            // Static files are not allowed to respond in other ways
            res.statusCode = 405;
            res.setHeader('Allow'.'GET, HEAD');
            res.setHeader('Content-length'.'0');
            res.end();
            return;
        }

        let path = url.parse(req.url).pathname;
        // Create a send stream
        let stream = send(req, path, opts);

        // listen for actions when the path passed in is directory
        // TODO still needs to be implemented
        // stream.on('directory', onDirectory);

        if(setHeaders) {
            stream.on('headers', setHeaders);
        }

        stream.on('error'.function error(err) {
            // If the interception has been determined previously, or is explicitly a server error
            if(! fallthrough || ! (err.statusCode <500)) {
                next(err);
                return;
            }
            // Pass to the next middleware for execution
            next();
        });
        stream.pipe(res);
    }

Copy the code

The stream in the code above comes from the send() method, from 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;
    
    // Whether segmented transmission is allowed
    this._acceptRanges = opts.acceptRanges ! = =undefined
        ? Boolean(opts.acceptRanges)
        : true;
    // Whether to set cacheControl
    this._cacheControl = opts.cacheControl ! = =undefined
        ? Boolean(opts.cacheControl)
        : true;

    // The default setting is eTag
    this._etag = opts.etag ! = =undefined
        ? Boolean(opts.etag)
        : true;

    // Whether to set immutable for cache
    this._immutable = opts.immutable ! = =undefined
        ? Boolean(opts.immutable)
        : false;
    // The default is lastModified
    this._lastModified = opts.lastModified ! = =undefined
        ? Boolean(opts.lastModified)
        : true
}

Copy the code

Cache-control, Etag, and Last-Modified are some of the important points that we are dealing with when requesting static resources

After initialization, execute stream.pipe(res); Enter the pipe logic with the following code:

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) {
            // For compatibility with 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;
}

Copy the code

In this function, we make some judgments, mainly about 400 and 403.

Continue down and execute this.sendFile(path); Where path is the requested resource path, such as/CSS /style.css

SendStream.prototype.sendFile = function(path) {
    let i = 0;
    const self = this;
    fs.stat(path, function(err, stat) {
        // If the file path does not exist && the file does not end with.xxx
        if (err && err.code === 'ENOENT' && !extname(path)) {
            return next(err);
        }
        if (err) {
            return self.onStatError(err);
        }
        if(stat.isDirectory()) {
            return self.redirect(path);
        }
        // Only the last match can be read by stream
        self.send(path, stat);
    });

    function next(err) {
        return err
            ? self.onStatError(err)
            : self.error(404)}}Copy the code

Continuing with path, if the path exists and it is not directory, enter the self.send() logic. Note: I haven’t implemented the directory operation yet, so ignore it for now and continue down as I do


function headersSent (res) {
  return typeofres.headersSent ! = ='boolean'
    ? Boolean(res._header)
    : res.headersSent
}
// The send function is the core

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 the request header has already been sent, prompt and return
    if(headersSent(res)) {
        this.headerAlreaySent();
        return;
    }

    / / set the header
    this.setHeader(path, stat);

    / / set the content-type
    this.type(path);

    // Determine if there is a conditional GET request
    if (this.isConditionalGET()) {
        // Check whether the request prerequisite failed
        if (this.isPreconditionFailure()) {
            // Client error (prerequisite failure), refused to serve the client
            this.err(412);
            return;
        }

        if(this.isCachable() && this.isFresh()) {
            this.notModified();
            return; }}Static resources are no longer fresh and need to be updated

    // TODO Range support
    // ...

    // clone options
    for (let prop in options) {
        opts[prop] = options[prop]
    }

    // content-length
    res.setHeader('Content-Length', len);

    // In the HEAD request, the server will not return the message body
    // This HEAD method is often used to test the validity, availability, and recent changes of the connection
    if(req.method === 'HEAD') {
        res.end();
        return;
    }

    this.stream(path, opts);
}

Copy the code

There are several important points in this approach:

  • this.headerAlreaySent()This function sends the 500 status code to the client and promptsCan\'t set headers after they are sent.
  • this.setHeader(path, stat);Set a number of very important header fields in the
SendStream.prototype.setHeader = function(path, stat) {
    let res = this.res;
    this.emit('headers', res, path, stat);
    // Support segmented transmission
    if (this._acceptRanges && ! res.getHeader('Accept-Ranges')) {
        res.setHeader('Accept-Ranges'.'bytes');
    }
    // When cache-control expires, res.getheader (' cache-control ') is reset to undefined, so cache-control is reset again
    Cache-control max-age=0 if cache-control max-age=0 if cache-control max-age=0 if cache-control max-age=0 if cache-control max-age=0, cache-control max-age=0
    if (this._cacheControl && ! res.getHeader('Cache-Control')) {
        
        let cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000);

        // If the 'immutable' property is set for cache duration
        if (this._immutable) {
            cacheControl += ', immutable';
        }

        res.setHeader('Cache-Control', cacheControl);
    }
    // res.getheader reads the response header that is queued but not sent to the client, so last-Modified is reset each time a new request arrives
    if (this._lastModified && ! res.getHeader('Last-Modified')) {
        // stat.mtime -> Last time the timestamp of this file was changed
        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); }}Copy the code
  • this.type(path)Used to set the content-Type
SendStream.prototype.type = function(path) {
    const res = this.res;
    if (res.getHeader('Content-Type')) {
        return;
    }
    // What type of resource is obtained, such as text/ CSS image/ PNG, etc
    let type = mime.lookup(path);
    if (type) {
        let charset = mime.charsets.lookup(type);
        res.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ' ')); }}Copy the code
  • this.isConditionalGET()Used to determine if there is a conditional lookup
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']}Copy the code
  • Enclosing isPreconditionFailure () check request premise condition fails, check request premise condition if – match and if – the unmodified – since (actually I don’t really need this judgment, because there is no use, If-none-match and if-modified-since are both used now.

  • This.iscachable () checks if it can be cached


SendStream.prototype.isCachable = function() {
    // res.statusCode defaults to 200
    let statusCode = this.res.statusCode;
    return (statusCode >= 200 && statusCode < 300)
        || statusCode === 304;
}
Copy the code
  • this.isFresh()Used to determine whether Etag and Last-Modified are ‘fresh’
endStream.prototype.isFresh = function() {
    // fresh to determine if 1. Etag does not match if-none-match, or 2. Last-modified is greater than if-if-modified-since
    // It is not fresh
    return fresh(this.req.headers, {
        'etag': this.res.getHeader('ETag'),
        'last-modified': this.res.getHeader('Last-Modified')}); }Copy the code
  • this.notModified();If it is found to be cacheable and fresh, the client is told that caching is available
SendStream.prototype.notModified = function() {
    this.removeContentHeaderFields();
    // Set res.statusCode to 304
    this.res.statusCode = 304;
    this.res.end();
}

Copy the code

This. Stream (path, opts); Make sure the request is stale and not a ‘HEAD’ request, then read the latest file and return

SendStream.prototype.stream = function(path, options) {
    const res = this.res;
    const stream = fs.createReadStream(path, options);
    this.emit('stream', stream);
    stream.pipe(res);

    // The reason for calling deStory is that if an error occurs during the processing of the readable stream, the target writable stream will not shut down automatically, which may result in a memory leak
    onFinished(res, function() {
        destroy(stream);
    });
}

Copy the code

The above is the main process of requesting a static file, which contains status codes 200, 3xx, 4xx, 5xx, of course, in order to keep the main line unblocked, so there is no detailed explanation of various boundary cases. You can follow this line of thinking, from the trunk to the branch, I believe that the understanding of static services will have a qualitative improvement.

This is the end of the express series of articles, if you have any questions, or if I have written something wrong, you are welcome to send a private message or comment, if you have any thoughts on your understanding, please give a thumbs-up