Skip to content

Commit

Permalink
Merge pull request #182 from opentable/node-client-post
Browse files Browse the repository at this point in the history
Node.js client post and various improvements
  • Loading branch information
lukasz-lysik committed Feb 11, 2016
2 parents ea54d6f + be5c8b5 commit b004a45
Show file tree
Hide file tree
Showing 22 changed files with 1,140 additions and 537 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ http.createServer(function (req, res) {
```
Open `http://localhost:4000/` and enjoy!

More docs about the node.js client [here](client/README.md).

## Server-side rendering with Ruby

* [Ruby library](https://github.com/opentable/ruby-oc)
Expand Down
58 changes: 55 additions & 3 deletions client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ Options:
|`disableFailoverRendering`|`boolean`|no|Disables the automatic failover rendering in case the registry times-out (in case configuration.registries.clientRendering contains a valid value.) Default false|
|`headers`|`object`|no|An object containing all the headers that must be forwarded to the component|
|`ie8`|`boolean`|no|Default false, if true puts in place the necessary polyfills to make all the stuff work with ie8|
|`params`|`object`|no|An object containing the parameters for component's request|
|`render`|`string`|no|Default `server`. When `client` will produce the html to put in the page for post-poning the rendering to the browser|
|`parameters`|`object`|no|An object containing the parameters for component's request|
|`render`|`string`|no|Default `server`. When `server`, it will return html. When `client` will produce the html to put in the page for post-poning the rendering to the browser|
|`timeout`|`number` (seconds)|no|Default 5. When request times-out, the callback will be fired with a timeout error and a client-side rendering response (unless `disableFailoverRendering` is set to `true`)|

Example:
Expand All @@ -67,7 +67,7 @@ client.renderComponent('header', {
headers: {
'accept-language': 'en-GB'
},
params: {
parameters: {
loggedIn: true
},
timeout: 2
Expand All @@ -76,3 +76,55 @@ client.renderComponent('header', {
// => "<div>This is the header. <a>Log-out</a></div>"
});
```

### Client#renderComponents(components [, options], callback)

It will make a request to the registry, and will render the components. The callback will contain an array of errors (array of `null` in case there aren't any) and an array of rendered html snippets. It will follow the same order of the request. This method will make **1** request to the registry + **n** requests for each component to get the views of components that aren't cached yet. After caching the views, this will make just **1** request to the registry.

Components parameter:

|Parameter|type|mandatory|description|
|---------|----|---------|-----------|
|`components`|`array of objects`|yes|The array of components to retrieve and render|
|`components[index].name`|`string`|yes|The component's name|
|`components[index].version`|`string`|no|The component's version. When not speficied, it will use globally specified one (doing client initialisation); when not specified and not globally specified, it will default to "" (latest)|
|`components[index].parameters`|`object`|no|The component's parameters|
|`components[index].render`|`string`|no|The component's render mode. When not specified, it will be the one specified in the options (for all components); if none is specified in options, it will default to `server`. When `server`, the rendering will be performed on the server-side and the result will be component's html. If `client`, the html will contain a promise to do the rendering on the browser.|

Options:

|Parameter|type|mandatory|description|
|---------|----|---------|-----------|
|`container`|`boolean`|no|Default true, when false, renders a component without its <oc-component> container|
|`disableFailoverRendering`|`boolean`|no|Disables the automatic failover rendering in case the registry times-out (in case configuration.registries.clientRendering contains a valid value.) Default false|
|`headers`|`object`|no|An object containing all the headers that must be forwarded to the component|
|`ie8`|`boolean`|no|Default false, if true puts in place the necessary polyfills to make all the stuff work with ie8|
|`render`|`string`|no|Default `server`. When `server`, it will return html. When `client` will produce the html to put in the page for post-poning the rendering to the browser|
|`timeout`|`number` (seconds)|no|Default 5. When request times-out, the callback will be fired with a timeout error and a client-side rendering response (unless `disableFailoverRendering` is set to `true`)|

Example:
```js
...
client.renderComponents([{
name: 'header',
parameters: { loggedIn: true }
}, {
name: 'footer',
version: '4.5.X'
}, {
name: 'advert',
parameters: { position: 'left' },
render: 'client'
}], {
container: false,
headers: {
'accept-language': 'en-US'
},
timeout: 3.0
}, function(errors, htmls){
console.log(html);
// => ["<div>Header</div>",
// "<p>Footer</p>",
// "<oc-component href=\"\/\/registry.com\/advert\/?position=left\"><\/oc-component>"]
});
```
226 changes: 226 additions & 0 deletions client/src/components-renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
'use strict';

var Cache = require('nice-cache');
var format = require('stringformat');

var GetCompiledTemplate = require('./get-compiled-template');
var GetOCClientScript = require('./get-oc-client-script');
var HrefBuilder = require('./href-builder');
var htmlRenderer = require('./html-renderer');
var request = require('./utils/request');
var sanitiser = require('./sanitiser');
var settings = require('./settings');
var templates = require('./templates');
var _ = require('./utils/helpers');

module.exports = function(config, renderTemplate){

var cache = new Cache(config.cache),
getOCClientScript = new GetOCClientScript(cache),
getCompiledTemplate = new GetCompiledTemplate(cache),
buildHref = new HrefBuilder(config),
serverRenderingFail = settings.serverSideRenderingFail;

return function(components, options, callback){

options = sanitiser.sanitiseGlobalRenderOptions(options, config);

var toDo = [];

_.each(components, function(component, i){
component.version = component.version || config.components[component.name];
toDo.push({
component: component,
pos: i,
render: component.render || options.render || 'server',
result: {}
});
});

var makePostRequest = function(components, cb){
request({
url: config.registries.serverRendering,
method: 'post',
headers: options.headers,
timeout: options.timeout,
json: true,
body: { components: components }
}, cb);
};

var getComponentsData = function(cb){

var serverRendering = {
components: [],
positions: []
};

_.each(toDo, function(action){
if(action.render === 'server'){
serverRendering.components.push(action.component);
serverRendering.positions.push(action.pos);
}
});

if(_.isEmpty(serverRendering.components)){
return cb();
} else if(!config.registries.serverRendering){
_.each(toDo, function(action){
action.result.error = serverRenderingFail;
if(!!options.disableFailoverRendering){
action.result.html = '';
action.done = true;
} else {
action.render = 'client';
action.failover = true;
}
});

return cb(serverRenderingFail);
}

makePostRequest(serverRendering.components, function(error, responses){
if(!!error || !responses || _.isEmpty(responses)){
responses = [];
_.each(serverRendering.components, function(){
responses.push({ status: -1 });
});
}

_.each(responses, function(response, i){
var action = toDo[serverRendering.positions[i]];

if(action.render === 'server'){
if(response.status !== 200){
action.result.error = serverRenderingFail;
if(!!options.disableFailoverRendering){
action.result.html = '';
action.done = true;
} else {
action.render = 'client';
action.failover = true;
}
} else {
action.apiResponse = response.response;
}
}
});
cb();
});
};

var fetchTemplateAndRender = function(component, cb){

var data = component.data,
isLocal = component.type === 'oc-component-local',
useCache = !isLocal;

getCompiledTemplate(component.template, useCache, options.timeout, function(err, template){
if(!!err){ return cb(err); }

var renderOptions = {
href: component.href,
key: component.template.key,
version: component.version,
templateType: component.template.type,
container: options.container,
renderInfo: options.renderInfo,
name: component.name
};

renderTemplate(template, data, renderOptions, cb);
});
};

var renderComponents = function(cb){

var toRender = [];

_.each(toDo, function(action){
if(action.render === 'server' && !!action.apiResponse){
toRender.push(action);
}
});

if(_.isEmpty(toRender)){
return cb();
}

_.eachAsync(toRender, function(action, next){
fetchTemplateAndRender(action.apiResponse, function(err, html){
if(!!err){
action.result.error = serverRenderingFail;
if(!!options.disableFailoverRendering){
action.result.html = '';
action.done = true;
} else {
action.render = 'client';
action.failover = true;
}
} else {
action.result.html = html;
action.done = true;
}
next();
});
}, cb);
};

var processClientReponses = function(cb){
var toProcess = [];

_.each(toDo, function(action){
if(action.render === 'client' && !action.done){
toProcess.push(action);
}
});

if(_.isEmpty(toProcess)){
return cb();
}

getOCClientScript(function(clientErr, clientJs){
_.each(toDo, function(action){
if(action.render === 'client'){
if(!!clientErr || !clientJs){
action.result.error = settings.genericError;
action.result.html = '';
} else {
var componentClientHref = buildHref.client(action.component);

if(!componentClientHref){
action.result.error = settings.clientSideRenderingFail;
action.result.html = '';
} else {
var unrenderedComponentTag = htmlRenderer.unrenderedComponent(componentClientHref, options);

if(action.failover){
action.result.html = format(templates.clientScript, clientJs, unrenderedComponentTag);
} else {
action.result.error = null;
action.result.html = unrenderedComponentTag;
}
}
}
}
});
cb();
});
};

getComponentsData(function(){
renderComponents(function(){
processClientReponses(function(){
var errors = [],
results = [];

_.each(toDo, function(action){
errors.push(action.result.error);
results.push(action.result.html);
});
callback(errors, results);
});
});
});
};
};
11 changes: 11 additions & 0 deletions client/src/executor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

var vm = require('vm');

module.exports = {
template: function(code, template){
var context = template.type === 'jade' ? { jade: require('jade/runtime.js')} : {};
vm.runInNewContext(code, context);
return context.oc.components[template.key];
}
};
29 changes: 29 additions & 0 deletions client/src/get-compiled-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

var executor = require('./executor');
var request = require('./utils/request');
var TryGetCached = require('./try-get-cached');

module.exports = function(cache){

var tryGetCached = new TryGetCached(cache);

return function(template, useCache, timeout, callback){

var getTemplateFromS3 = function(cb){
request({
url: template.src,
timeout: timeout
}, function(err, templateText){
if(!!err){ return cb(err); }
cb(null, executor.template(templateText, template));
});
};

if(!!useCache){
return tryGetCached('template', template.key, getTemplateFromS3, callback);
}

return getTemplateFromS3(callback);
};
};
17 changes: 17 additions & 0 deletions client/src/get-oc-client-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

var fs = require('fs');
var path = require('path');

var TryGetCached = require('./try-get-cached');

module.exports = function(cache){

var tryGetCached = new TryGetCached(cache);

return function(callback){
tryGetCached('scripts', 'oc-client', function(cb){
fs.readFile(path.resolve(__dirname, './oc-client.min.js'), 'utf-8', cb);
}, callback);
};
};
22 changes: 22 additions & 0 deletions client/src/href-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

var querystring = require('querystring');
var url = require('url');

module.exports = function(config){

return {
client: function(component){
if(!config.registries.clientRendering){
return null;
}

var versionSegment = !!component.version ? ('/' + component.version) : '',
registryUrl = config.registries.clientRendering,
registrySegment = registryUrl.slice(-1) === '/' ? registryUrl : (registryUrl + '/'),
qs = !!component.parameters ? ('/?' + querystring.stringify(component.parameters)) : '';

return url.resolve(registrySegment, component.name) + versionSegment + qs;
}
};
};
Loading

0 comments on commit b004a45

Please sign in to comment.