Skip to content
This repository has been archived by the owner on Dec 5, 2022. It is now read-only.

Prioritize the module loader script with link headers #203

Merged
merged 2 commits into from
Dec 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason not to use explicit if () {} here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid the typecasting.


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 !== '' &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason not to use explicit if () {} here?

(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