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

Commit

Permalink
Merge pull request #159 from iilei/fix_hooks
Browse files Browse the repository at this point in the history
Fix hooks
  • Loading branch information
vigneshshanmugam authored Jun 12, 2017
2 parents 1f1998d + 4892792 commit be78a4c
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 120 deletions.
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];
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();
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

0 comments on commit be78a4c

Please sign in to comment.