static-expiry provides two things:
- A helper method
furl
for your templates in order to generate fingerprinted URLs for your static assets - Middleware to handle incoming requests for the fingerprinted URLs. It handles them by rewriting the url to it's original value and setting appropriate conditional/unconditional cache headers according to your configuration.
static-expiry does not serve static assets. It is invoked by calling it's function that returns the middleware. It should be placed just before the middleware you use for serving static assets.
static-expiry is meant to be everything you need to set up your app servers as origin servers to your CDN. The two things it provides are key to this: versioned urls and handling the versioned urls. Of course, you don't have to use a CDN and with static-expiry you are still serving static assets using best practices for caching.
static-expiry's cache
static-expiry uses two lookup cache objects. One that maps asset urls to the fingerprinted version, so that the function that generates fingerprinted urls only calculates once per asset. And another that maps the incoming fingerprinted URL to an object that contains the unfingerprinted asset URL, the fingerprint (used for the etag header) and the file stat mtime (used for the last-modified header). The latter is used by the middleware to rewrite the URL in the req
object and set the appropriate cache headers.
Installation
$ npm install static-expiry
Quick Start
var express = require('express');
var app = express()
, expiry = require('static-expiry');
app.use(expiry(app, { dir: path.join(__dirname, 'public') }));
app.use(express.static(staticDir));
app.listen(3000);
The function returned from the require statement takes two arguments, the first being the connect/express app (so that the app local can be set) and the second an object of options.
furl helper
Use the furl
app local in your templates in order to generate the fingerprinted URL.
<link rel="stylesheet" href="{{ furl('/css/style.css') }}" />
<link rel="stylesheet" href="{{ furl('/css/style.css', '/css/style.min.css') }}" />
The second argument will be used when the NODE_ENV is not development.
Options
There are a number of options to control the fingerprinting and middleware.
app.use(expiry(app, {
options are passed in as the second argument
duration
the duration in seconds for the Cache-Control header, max-age value and the Expires header
duration: 31556900,
unconditional
what unconditional cache headers to set
unconditional: 'both'
conditional
what conditional cache headers to set
conditional: 'both',
cacheControl
the value of the Cache-Control header preceding the max-age value
cacheControl: 'cookieless',
any other string value is what will be used always, typically 'public' or 'private'. use zero length, false, or null to not have a value. the conditional option may still mean the Cache-Control header will be present however, e.g. Cache-Control: max-age=31556900
dir
the directory of the static assets
dir: path.join(process.env.PWD, 'public'),
I have no idea how reliable the presence of the PWD environment variable is, so it's probably best to set this
fingerprint
a function to use to generate the fingerprint
fingerprint: md5,
the function takes the file path as it's only argument and should return the fingerprint value only (not the fingerprinted url)
location
the location of the fingerprint in the URL the furl
function generates
location: 'prefile',
the 'path' option could be problematic if you are using relative url references in your css/js files but could work if you supply your own function for generating the fingerprint value and make it static across all assets
host
a domain host value to be used for the fingerprinted URLs.
host: null,
If you use multiple hosts, the one selected is based upon a modulus of the sum of the character codes in the asset URL so that the same host is generated consistently across app servers.
This option is what you will use if setting up your app servers as origin servers to your CDN. The fingerprinted URLs will then properly point to the CDN host(s) but your app servers can still serve the files with the caching strategy you have configured or defaulting to.
loadCache
when to load the urlCache and assetCache
loadCache: 'startup'
the 'startup' value is necessary in a multiple server environment as it is possible for a fingerprinted request to come into a particular server before it has generated a fingerprinted URL for that asset itself.
(i may work a way around this in the future, not too hard to reverse engineer the asset from the fingerprinted url)
debug
create a GET /expiry route that outputs the json of the urlCache and assetCache
debug: process.env.NODE_ENV === 'development'
}));
Enabled vs Disabled (!development vs development)
If both conditional and unconditional have a value of none (the default in development), static-expiry is disabled and the furl
function will not fingerprint the url.
TODO
- Handle file changes in a production mode either with a file watcher or dynamically looking at the file stats on every request.
Credits
The inspiration for this project goes to bminer for https://github.com/bminer/node-static-asset
License
(The MIT License)
Copyright (c) 2013 Paul Walker <github@paulwalker.tv>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.