Skip to content

Commit

Permalink
Prioritize the module loader script with link headers (zalando#203)
Browse files Browse the repository at this point in the history
* Prioritize the module loader script with link headers

* address review comments and improve code coverage
  • Loading branch information
vigneshshanmugam committed Dec 18, 2017
1 parent 7f7c263 commit a2124ea
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 128 deletions.
11 changes: 8 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const filterReqHeadersFn = require('./lib/filter-headers');
const PIPE_DEFINITION = fs.readFileSync(
path.resolve(__dirname, 'src/pipe.min.js')
);
const { getCrossOrigin } = require('./lib/utils');
const AMD_LOADER_URL =
'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js';

Expand Down Expand Up @@ -38,7 +39,7 @@ module.exports = class Tailor extends EventEmitter {
: Math.max(1, maxAssetLinks);

let memoizedDefinition;
const pipeChunk = (amdLoaderUrl, pipeInstanceName) => {
const pipeChunk = (pipeInstanceName, { host } = {}) => {
if (!memoizedDefinition) {
// Allow reading from fs for inlining AMD
if (amdLoaderUrl.startsWith('file://')) {
Expand All @@ -48,7 +49,10 @@ module.exports = class Tailor extends EventEmitter {
);
memoizedDefinition = `<script>${fileData}\n`;
} else {
memoizedDefinition = `<script src="${amdLoaderUrl}"></script>\n<script>`;
memoizedDefinition = `<script src="${AMD_LOADER_URL}" ${getCrossOrigin(
amdLoaderUrl,
host
)}></script>\n<script>`;
}
}
return Buffer.from(
Expand All @@ -58,6 +62,7 @@ module.exports = class Tailor extends EventEmitter {

const requestOptions = Object.assign(
{
amdLoaderUrl,
fetchContext: () => Promise.resolve({}),
fetchTemplate: fetchTemplate(
templatesPath || path.join(process.cwd(), 'templates')
Expand All @@ -67,7 +72,7 @@ module.exports = class Tailor extends EventEmitter {
handleTag: () => '',
requestFragment: requestFragment(filterRequestHeaders),
pipeInstanceName: 'Pipe',
pipeDefinition: pipeChunk.bind(null, amdLoaderUrl),
pipeDefinition: pipeChunk,
pipeAttributes: getPipeAttributes
},
options
Expand Down
2 changes: 1 addition & 1 deletion lib/process-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ module.exports = function processTemplate(request, options, context) {
};

if (placeholder === 'pipe') {
return pipeDefinition(pipeInstanceName);
return pipeDefinition(pipeInstanceName, request.headers);
}

if (placeholder === 'async') {
Expand Down
91 changes: 35 additions & 56 deletions lib/request-handler.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
'use strict';

const AsyncStream = require('./streams/async-stream');
const LinkHeader = require('http-link-header');
const ContentLengthStream = require('./streams/content-length-stream');
const { TEMPLATE_NOT_FOUND } = require('./fetch-template');
const processTemplate = require('./process-template');
const {
getLoaderScript,
getFragmentAssetsToPreload,
nextIndexGenerator
} = require('./utils');

// Events emitted by fragments on the template
const FRAGMENT_EVENTS = [
'start',
'response',
Expand All @@ -11,54 +20,6 @@ const FRAGMENT_EVENTS = [
'fallback',
'warn'
];
const { TEMPLATE_NOT_FOUND } = require('./fetch-template');

const processTemplate = require('./process-template');

const getCrossOriginHeader = (fragmentUrl, host) => {
if (host && fragmentUrl.indexOf(`://${host}`) < 0) {
return 'crossorigin';
}
return '';
};

// Early preloading of primary fragments assets to improve Performance
const getAssetsToPreload = ({ link }, { headers = {} }) => {
let assetsToPreload = [];

const { refs = [] } = LinkHeader.parse(link);
const scriptRefs = refs
.filter(ref => ref.rel === 'fragment-script')
.map(ref => ref.uri);
const styleRefs = refs
.filter(ref => ref.rel === 'stylesheet')
.map(ref => ref.uri);

// Handle Server rendered fragments without depending on assets
if (!scriptRefs[0] && !styleRefs[0]) {
return '';
}
styleRefs.forEach(uri => {
assetsToPreload.push(`<${uri}>; rel="preload"; as="style"; nopush`);
});
scriptRefs.forEach(uri => {
const crossOrigin = getCrossOriginHeader(uri, headers.host);
assetsToPreload.push(
`<${uri}>; rel="preload"; as="script"; nopush; ${crossOrigin}`
);
});
return assetsToPreload.join(',');
};

function nextIndexGenerator(initialIndex, step) {
let index = initialIndex;

return () => {
let pastIndex = index;
index += step;
return pastIndex;
};
}

/**
* Process the HTTP Request to the Tailor Middleware
Expand All @@ -74,7 +35,8 @@ module.exports = function processRequest(options, request, response) {
fetchTemplate,
parseTemplate,
filterResponseHeaders,
maxAssetLinks
maxAssetLinks,
amdLoaderUrl
} = options;

const asyncStream = new AsyncStream();
Expand Down Expand Up @@ -142,12 +104,16 @@ module.exports = function processRequest(options, request, response) {
}

// Make resources early discoverable while processing HTML
const preloadAssets = headers.link
? getAssetsToPreload(headers, request)
: '';
if (preloadAssets !== '') {
responseHeaders.link = preloadAssets;
}
const assetsToPreload = getFragmentAssetsToPreload(
headers,
request.headers
);

// Loader script must be preloaded before every fragment asset
const loaderScript = getLoaderScript(amdLoaderUrl, request.headers);
loaderScript !== '' && assetsToPreload.unshift(loaderScript);

responseHeaders.link = assetsToPreload.join(',');
this.emit('response', request, statusCode, responseHeaders);

response.writeHead(statusCode, responseHeaders);
Expand Down Expand Up @@ -180,8 +146,10 @@ module.exports = function processRequest(options, request, response) {
extendedOptions,
context
);
let isFragmentFound = false;

resultStream.on('fragment:found', fragment => {
isFragmentFound = true;
FRAGMENT_EVENTS.forEach(eventName => {
fragment.once(eventName, (...args) => {
const prefixedName = 'fragment:' + eventName;
Expand All @@ -202,7 +170,18 @@ module.exports = function processRequest(options, request, response) {
const statusCode = response.statusCode || 200;
if (shouldWriteHead) {
shouldWriteHead = false;
// Preload the loader script when atleast
// one fragment is present on the page
if (isFragmentFound) {
const loaderScript = getLoaderScript(
amdLoaderUrl,
request.headers
);
loaderScript !== '' &&
(responseHeaders.link = loaderScript);
}
this.emit('response', request, statusCode, responseHeaders);

response.writeHead(statusCode, responseHeaders);
resultStream.pipe(contentLengthStream).pipe(response);
}
Expand Down
96 changes: 96 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict';

const LinkHeader = require('http-link-header');

const getCrossOrigin = (url = '', host = '') => {
if (url.includes(`://${host}`)) {
return '';
}
return 'crossorigin';
};

const getPreloadAttributes = ({
assetUrl,
host,
asAttribute,
corsCheck = false,
noPush = true // Disable HTTP/2 Push behaviour until digest spec is implemented by most browsers
}) => {
return (
assetUrl &&
`<${assetUrl}>; rel="preload"; as="${asAttribute}"; ${noPush
? 'nopush;'
: ''} ${corsCheck ? getCrossOrigin(assetUrl, host) : ''}`.trim()
);
};

// Module loader script used by tailor for managing dependency between fragments
const getLoaderScript = (amdLoaderUrl, { host } = {}) => {
if (amdLoaderUrl.startsWith('file://')) {
return '';
}

return getPreloadAttributes({
assetUrl: amdLoaderUrl,
asAttribute: 'script',
corsCheck: true,
host
});
};

// Early preloading of primary fragments assets to improve Performance
const getFragmentAssetsToPreload = ({ link = '' }, { host } = {}) => {
let assetsToPreload = [];

const { refs = [] } = LinkHeader.parse(link);
const scriptRefs = refs
.filter(ref => ref.rel === 'fragment-script')
.map(ref => ref.uri);
const styleRefs = refs
.filter(ref => ref.rel === 'stylesheet')
.map(ref => ref.uri);

// Handle Server rendered fragments without depending on assets
if (!scriptRefs[0] && !styleRefs[0]) {
return assetsToPreload;
}

for (const uri of styleRefs) {
assetsToPreload.push(
getPreloadAttributes({
assetUrl: uri,
asAttribute: 'style'
})
);
}

for (const uri of scriptRefs) {
assetsToPreload.push(
getPreloadAttributes({
assetUrl: uri,
asAttribute: 'script',
corsCheck: true,
host
})
);
}

return assetsToPreload;
};

const nextIndexGenerator = (initialIndex, step) => {
let index = initialIndex;

return () => {
let pastIndex = index;
index += step;
return pastIndex;
};
};

module.exports = {
getCrossOrigin,
getFragmentAssetsToPreload,
getLoaderScript,
nextIndexGenerator
};
Loading

0 comments on commit a2124ea

Please sign in to comment.