Skip to content

Commit

Permalink
feat(cache): Cache resources based on ETag headers
Browse files Browse the repository at this point in the history
After parsing a response's HTML, save the parsed subresources into
lru-cache with the `Etag` header as the key. Subsequent responses can
lookup and reuse the already parsed resources.
  • Loading branch information
terinjokes committed Feb 3, 2016
1 parent f8fe400 commit 0a34ec7
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 59 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@ express()
.listen(1337);
```

## Node.js 0.10 Requirements
## ⚠️ Node.js 0.10 Requirements ⚠️

This module utilizes the [`posthtml`][posthtml] module to find images, scripts, and stylesheets in your HTML response.
PostHTML requires a native Promise implementation or shim, so users of Node.js 0.10 will need to ensure a Promise shim has been configured.

## Options

* **images**, **scripts**, and **styles**: `Boolean`:

If `true` the corresponding subresources are parsed and added as a Preload Link headers.

* **cache**: `Object`:

Object passed straight to [`lru-cache`][lru-cache]. It is highly recommended to set `cache.max` to an integer.

## License
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://www.tldrlegal.com/l/mit) see `LICENSE.md`.

[preload]: https://www.w3.org/TR/preload/
[posthtml]: https://github.com/posthtml/posthtml
[posthtml]: https://github.com/posthtml/posthtml#readme
[lru-cache]: https://github.com/isaacs/node-lru-cache#readme
58 changes: 38 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ var defaults = require('lodash.defaults');
var hijackresponse = require('hijackresponse');
var bl = require('bl');

var LRU = require('lru-cache');

module.exports = function netjet(options) {
options = defaults(options, {
images: true,
scripts: true,
styles: true
styles: true,
cache: {}
});

var cache = new LRU(options.cache);

return function (req, res, next) {
function appendHeader(field, value) {
var prev = res.getHeader(field);
Expand All @@ -25,50 +30,63 @@ module.exports = function netjet(options) {
res.setHeader(field, value);
}

function insertLinks(urls, asType) {
urls.forEach(function (url) {
function insertLinkArray(entries) {
entries.forEach(function (entry) {
var url = entry[0];
var asType = entry[1];

appendHeader('Link', '<' + unescape(url) + '>; rel=preload; as=' + asType);
});
}

function processBody(body) {
var found = {};

posthtml().use(posthtmlPreload(options, found)).process(body);
var foundEntries = [];

if (options.images) {
insertLinks(found.images, 'image');
}

if (options.scripts) {
insertLinks(found.scripts, 'script');
}
posthtml().use(posthtmlPreload(options, foundEntries)).process(body);

if (options.styles) {
insertLinks(found.styles, 'style');
}
return foundEntries;
}

hijackresponse(res, function (err, res) {
/* istanbul ignore next */
// `err` from hijackresponse is currently hardcoded to "null"
if (err) {
res.unhijack();
return next(err);
next(err);
return;
}

// Only hijack HTML responses
if (!/^text\/html(?:;|\s|$)/.test(res.getHeader('Content-Type'))) {
return res.unhijack();
res.unhijack();
return;
}

var etag = res.getHeader('etag');
var entries;

// reuse previous parse if the etag already exists in cache
if (etag) {
entries = cache.get(etag);

if (entries) {
insertLinkArray(entries);
res.pipe(res);
return;
}
}

res.pipe(bl(function (err, data) {
if (err) {
res.unhijack();
return next(err);
next(err);
return;
}

processBody(data.toString());
entries = processBody(data.toString());

insertLinkArray(entries);
cache.set(etag, entries);

res.end(data);
}));
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
"dependencies": {
"bl": "^1.0.1",
"hijackresponse": "^1.0.2",
"lodash.assign": "^4.0.1",
"lodash.defaults": "^4.0.0",
"lodash.unescape": "^3.1.1",
"lru-cache": "^4.0.0",
"posthtml": "^0.8.1"
},
"devDependencies": {
Expand All @@ -43,6 +43,8 @@
"istanbul": "^0.4.2",
"mocha": "^2.4.5",
"supertest": "^1.1.0",
"supertest-as-promised": "^2.0.2",
"testdouble": "^0.7.3",
"xo": "^0.12.1"
},
"xo": {
Expand Down
17 changes: 5 additions & 12 deletions posthtml-preload.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
'use strict';
var assign = require('lodash.assign');

module.exports = function (options, found) {
module.exports = function (options, foundEntries) {
return function (tree) {
assign(found, {
images: [],
scripts: [],
styles: []
});

var matchers = [];

if (options.images) {
Expand All @@ -27,20 +20,20 @@ module.exports = function (options, found) {
tree.match(matchers, function (node) {
switch (node.tag) {
case 'img':
found.images.push(node.attrs.src);
foundEntries.push([node.attrs.src, 'image']);
break;
case 'script':
found.scripts.push(node.attrs.src);
foundEntries.push([node.attrs.src, 'script']);
break;
case 'link':
found.styles.push(node.attrs.href);
foundEntries.push([node.attrs.href, 'style']);
break;
// no default
}
return node;
});
}

return found;
return foundEntries;
};
};
Loading

0 comments on commit 0a34ec7

Please sign in to comment.