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

Fix hooks #159

Merged
merged 2 commits into from
Jun 12, 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
19 changes: 12 additions & 7 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@ Tailor provides four hooks on the client side(Browser) that can be used for prog

## API

### Pipe.onStart(callback(attributes))
+ callback will be called before every fragments gets inserted/piped in the browser.
### Pipe.onStart(callback(attributes, index))
+ callback will be called before every script from fragments gets inserted/piped in the browser.

### Pipe.onBeforeInit(callback(attributes))
+ callback will be called before every fragments on the page/template gets initialized.
### Pipe.onBeforeInit(callback(attributes, index))
+ callback will be called before each script from fragments on the page/template gets initialized.

### Pipe.onAfterInit(callback(attributes))
+ callback will be called after each fragments on the page gets initialized.
### Pipe.onAfterInit(callback(attributes, index))
+ callback will be called after each script from fragments on the page gets initialized.

### Pipe.onDone(callback())
+ callback will be called when all the fragments on the page gets initialized.
+ callback will be called when all the fragment scripts on the page gets initialized.

## Options

#### attributes
+ The attributes that are available from hooks can be customized by overiding `pipeAttributes` function as part of Tailor options.
+ The default attributes that are available through hooks are `primary` and `id`.

#### index
+ The order in which the script tags(sent via `Link Headers` from each fragment) are flushed to the browser

**NOTE: Hooks wont work properly for scripts/modules that are not AMD**

Check out the [Performance](https://github.com/zalando/tailor/tree/master/docs/Performance.md) document on how to measure fragment initialzation time as well as capturing the time to interactivity of the page.
1 change: 0 additions & 1 deletion examples/fragment-performance/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
</script>
<script>
define('word', function () {
// Example dependency for the fragments
return 'initialised';
});
</script>
Expand Down
19 changes: 12 additions & 7 deletions examples/multiple-fragments-with-custom-amd/fragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,26 @@ const defineFn = (module, fragmentName) => {
return `define (['${module}'], function (module) {
return function initFragment (element) {
element.className += ' fragment-${fragmentName}-${module}';
element.innerHTML += module;
element.innerHTML += ' ' + module;
}
})`;
};

module.exports = (fragmentName, fragmentUrl) => (request, response) => {
module.exports = (fragmentName, fragmentUrl, modules = 1) => (request, response) => {
const pathname = url.parse(request.url).pathname;
const moduleLinks = [];

for (var i=0; i<modules; i++) {
moduleLinks[i] = `<${fragmentUrl}/module-${i+1}.js>; rel="fragment-script"`;
}

switch (pathname) {
case '/fragment-1.js':
case '/module-1.js':
// serve fragment's JavaScript
response.writeHead(200, jsHeaders);
response.end(defineFn('js1', fragmentName));
break;
case '/fragment-2.js':
case '/module-2.js':
// serve fragment's JavaScript
response.writeHead(200, jsHeaders);
response.end(defineFn('js2', fragmentName));
Expand All @@ -48,15 +54,14 @@ module.exports = (fragmentName, fragmentUrl) => (request, response) => {
default:
// serve fragment's body
response.writeHead(200, {
'Link': `<${fragmentUrl}/fragment.css>; rel="stylesheet",` +
`<${fragmentUrl}/fragment-1.js>; rel="fragment-script",` +
`<${fragmentUrl}/fragment-2.js>; rel="fragment-script"`,
'Link': `<${fragmentUrl}/fragment.css>; rel="stylesheet",${moduleLinks.join(',')}`,
'Content-Type': 'text/html'
});
response.end(`
<div class="fragment-${fragmentName}">
Fragment ${fragmentName}
</div>
`);

}
};
6 changes: 3 additions & 3 deletions examples/multiple-fragments-with-custom-amd/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ console.log('Tailor started at port 8080');
tailor.on('error', (request, err) => console.error(err));

const fragment1 = http.createServer(
serveFragment('hello', 'http://localhost:8081')
serveFragment('fragment1', 'http://localhost:8081')
);
fragment1.listen(8081);
console.log('Fragment1 started at port 8081');

const fragment2 = http.createServer(
serveFragment('world', 'http://localhost:8082')
serveFragment('fragment2', 'http://localhost:8082')
);
fragment2.listen(8082);
console.log('Fragment2 started at port 8082');

const fragment3 = http.createServer(
serveFragment('body-start', 'http://localhost:8083')
serveFragment('fragment3', 'http://localhost:8083', 2)
);
fragment3.listen(8083);
console.log('Fragment3 started at port 8083');
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,38 @@
return 'js1';
});
define('js2', function () {
return '-js2';
return 'js2';
});
// Testing hook calls via logs
var log = {
'fragment-0' : [],
'fragment-custom-id' : [],
'fragment-4' : [],
'common': [],
};
var fragmentIds = [0, 'custom-id', 4];
var result = '';

Pipe.onStart((attributes, index) => {
log['fragment-' + attributes.id].push('onStart-' + index);
});
Pipe.onBeforeInit((attributes, index) => {
log['fragment-' + attributes.id].push('onBeforeInit-' + index);
});
Pipe.onAfterInit((attributes, index) => {
log['fragment-' + attributes.id].push('onAfterInit-' + index);
});
Pipe.onDone(() => {
log.common.push('onDone');

var logNode = document.getElementById('hook-logs');
fragmentIds.forEach(function(id, index) {
var key = 'fragment-' + id;
result += 'fragment-' + index + ' hooks: ' + log[key].join(',') +';\n';
});
result += ' common hooks: ' + log.common.join(',') +';';
logNode.innerText = result;
logNode.className += ' all-done';
});
</script>
</head>
Expand All @@ -24,10 +55,11 @@
document.body.appendChild(document.createElement('script'));
</script>
<h2>Fragment 1:</h2>
<fragment src="http://localhost:8081" primary></fragment>
<fragment src="http://localhost:8081" primary id="custom-id"></fragment>
<h2>Fragment 2:</h2>
<fragment async src="http://localhost:8082"></fragment>
<div>All done!</div>
</div>
<pre id="hook-logs" class="logs"></pre>
</body>
</html>
32 changes: 23 additions & 9 deletions examples/multiple-fragments-with-custom-amd/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,23 @@ describe('Frontend test', function () {
);
}

function logForFragment(id) {
const result = 'fragment-' + id +' hooks: ';
switch (id) {
case '0':
return result + 'onStart-0,onStart-1,onBeforeInit-0,onAfterInit-0,onBeforeInit-1,onAfterInit-1;';
case '1':
return result + 'onStart-2,onBeforeInit-2,onAfterInit-2;';
case '2':
return result + 'onStart-4,onBeforeInit-4,onAfterInit-4;';
}
}

before(() => {
server = http.createServer(tailor.requestHandler);
fragment1 = http.createServer(fragment('hello', 'http://localhost:8081'));
fragment2 = http.createServer(fragment('world', 'http://localhost:8082'));
fragment3 = http.createServer(fragment('body-start', 'http://localhost:8083'));
fragment1 = http.createServer(fragment('fragment1', 'http://localhost:8081'));
fragment2 = http.createServer(fragment('fragment2', 'http://localhost:8082'));
fragment3 = http.createServer(fragment('fragment3', 'http://localhost:8083'));
return Promise.all([
server.listen(8080),
fragment1.listen(8081),
Expand All @@ -81,12 +93,14 @@ describe('Frontend test', function () {
.then((title) => {
assert.equal(title, 'Test Page', 'Test page is not loaded');
})
.waitForElementByCss('.fragment-hello-js1', asserters.textInclude('js1'), 2000)
.waitForElementByCss('.fragment-hello-js2', asserters.textInclude('js2'), 2000)
.waitForElementByCss('.fragment-world-js1', asserters.textInclude('js1'), 2000)
.waitForElementByCss('.fragment-world-js2', asserters.textInclude('js2'), 2000)
.waitForElementByCss('.fragment-body-start-js1', asserters.textInclude('js1'), 2000)
.waitForElementByCss('.fragment-body-start-js2', asserters.textInclude('js2'), 2000);
.waitForElementByCss('.fragment-fragment1-js1', asserters.textInclude('js1'), 2000)
.waitForElementByCss('.fragment-fragment2-js1', asserters.textInclude('js1'), 2000)
.waitForElementByCss('.fragment-fragment3-js1', asserters.textInclude('js1'), 2000)
.waitForElementByCss('.fragment-fragment3-js2', asserters.textInclude('js2'), 2000)
.waitForElementByCss('.logs.all-done', asserters.textInclude(logForFragment('0')), 2000)
.waitForElementByCss('.logs.all-done', asserters.textInclude(logForFragment('1')), 2000)
.waitForElementByCss('.logs.all-done', asserters.textInclude(logForFragment('2')), 2000)
.waitForElementByCss('.logs.all-done', asserters.textInclude('common hooks: onDone;'), 2000);

});
});
Expand Down
1 change: 0 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const filterHeadersFn = require('./lib/filter-headers');
const PIPE_DEFINITION = fs.readFileSync(path.resolve(__dirname, 'src/pipe.min.js'));
const AMD_LOADER_URL = 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.22/require.min.js';


const stripUrl = (fileUrl) => path.normalize(fileUrl.replace('file://', ''));
const getPipeAttributes = (attributes) => {
const { primary, id } = attributes;
Expand Down
22 changes: 12 additions & 10 deletions lib/fragment.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ module.exports = class Fragment extends EventEmitter {
insertStart () {
this.styleRefs.forEach(uri => {
this.stream.write(
this.attributes.async
? `<script>${this.pipeInstanceName}.loadCSS("${uri}")</script>`
this.attributes.async
? `<script>${this.pipeInstanceName}.loadCSS("${uri}")</script>`
: `<link rel="stylesheet" href="${uri}">`
);
});
Expand All @@ -183,11 +183,12 @@ module.exports = class Fragment extends EventEmitter {
return;
}

const range = [this.index, this.index + this.scriptRefs.length - 1];
const fragmentId = this.attributes.id || range[0];
this.scriptRefs.forEach((uri)=> {
const id = this.index;
const attributes = Object.assign({}, this.getPipeAttributes(), { id: this.attributes.id || id });
const attributes = Object.assign({}, this.getPipeAttributes(), { id: fragmentId, range });
this.stream.write(
`<script data-pipe>${this.pipeInstanceName}.start(${id}, "${uri}", ${JSON.stringify(attributes)})</script>`
`<script data-pipe>${this.pipeInstanceName}.start(${this.index}, "${uri}", ${JSON.stringify(attributes)})</script>`
);
this.index++;
});
Expand All @@ -197,17 +198,18 @@ module.exports = class Fragment extends EventEmitter {
* Insert the placeholder for pipe assets at the end of fragment stream
*/
insertEnd () {
this.index--;
if (this.scriptRefs.length > 0) {
const range = [this.index - this.scriptRefs.length, this.index - 1];
this.index--;
const fragmentId = this.attributes.id || range[0];
Copy link
Collaborator

Choose a reason for hiding this comment

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

By keeping the id consistent, It ll become easy to measure the fragment perf. I will update the fragment-performance script to handle this case.

this.scriptRefs.reverse().forEach((uri)=> {
const id = this.index--;
const attributes = Object.assign({}, this.getPipeAttributes(), { id: this.attributes.id || id });
const attributes = Object.assign({}, this.getPipeAttributes(), { id: fragmentId, range });
this.stream.write(
`<script data-pipe>${this.pipeInstanceName}.end(${id}, "${uri}", ${JSON.stringify(attributes)})</script>`
`<script data-pipe>${this.pipeInstanceName}.end(${this.index--}, "${uri}", ${JSON.stringify(attributes)})</script>`
);
});
} else {
this.stream.write(`<script data-pipe>${this.pipeInstanceName}.end(${this.index})</script>`);
this.stream.write(`<script data-pipe>${this.pipeInstanceName}.end(${this.index-1})</script>`);
}
}

Expand Down
28 changes: 17 additions & 11 deletions src/pipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,21 @@
starts[index] = currentScript();
if (script) {
initState.push(index);
hooks.onStart(attributes);
hooks.onStart(attributes, index);
require([script]);
}
}

// OnDone will be called once the document is completed parsed and there are no other fragments getting streamed.
function fireDone() {
if (initState.length === 0
&& doc.readyState
&& (doc.readyState === 'complete'
|| doc.readyState === 'interactive') ) {
hooks.onDone();
}
}

function end(index, script, attributes) {
var placeholder = placeholders[index];
var start = starts[index];
Expand All @@ -63,10 +73,12 @@
start.parentNode.removeChild(start);
end.parentNode.removeChild(end);
script && require([script], function (i) {
// Exposed fragment initialization Function/Promise
// Exported AMD fragment initialization Function/Promise
var init = i && i.__esModule ? i.default : i;
// early return
if (typeof init !== 'function') {
initState.pop();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will fix the issue when onDone was not called for script without dependencies.

fireDone();
return;
}

Expand All @@ -76,18 +88,12 @@
}

function doInit(init, node) {
hooks.onBeforeInit(attributes);
hooks.onBeforeInit(attributes, index);
var fragmentRendering = init(node);
var handlerFn = function() {
initState.pop();
hooks.onAfterInit(attributes);
// OnDone will be called once the document is completed parsed and there are no other fragments getting streamed.
if (initState.length === 0
&& doc.readyState
&& (doc.readyState === 'complete'
|| doc.readyState === 'interactive') ) {
hooks.onDone();
}
hooks.onAfterInit(attributes, index);
fireDone(attributes);
};
// Check if the response from fragment is a Promise to allow lazy rendering
if (isPromise(fragmentRendering)) {
Expand Down
Loading