diff --git a/README.md b/README.md
index 30a5e1d43..6b6e266eb 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/client/README.md b/client/README.md
index 3ae123a01..8bec474d9 100644
--- a/client/README.md
+++ b/client/README.md
@@ -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:
@@ -67,7 +67,7 @@ client.renderComponent('header', {
headers: {
'accept-language': 'en-GB'
},
- params: {
+ parameters: {
loggedIn: true
},
timeout: 2
@@ -76,3 +76,55 @@ client.renderComponent('header', {
// => "
"
});
```
+
+### 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 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);
+ // => ["Header
",
+ // "Footer
",
+ // "<\/oc-component>"]
+});
+```
diff --git a/client/src/components-renderer.js b/client/src/components-renderer.js
new file mode 100644
index 000000000..9d50c2c1f
--- /dev/null
+++ b/client/src/components-renderer.js
@@ -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);
+ });
+ });
+ });
+ };
+};
\ No newline at end of file
diff --git a/client/src/executor.js b/client/src/executor.js
new file mode 100644
index 000000000..dbcca7bbe
--- /dev/null
+++ b/client/src/executor.js
@@ -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];
+ }
+};
\ No newline at end of file
diff --git a/client/src/get-compiled-template.js b/client/src/get-compiled-template.js
new file mode 100644
index 000000000..3fce7d200
--- /dev/null
+++ b/client/src/get-compiled-template.js
@@ -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);
+ };
+};
\ No newline at end of file
diff --git a/client/src/get-oc-client-script.js b/client/src/get-oc-client-script.js
new file mode 100644
index 000000000..71af12c1e
--- /dev/null
+++ b/client/src/get-oc-client-script.js
@@ -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);
+ };
+};
\ No newline at end of file
diff --git a/client/src/href-builder.js b/client/src/href-builder.js
new file mode 100644
index 000000000..33f3aae47
--- /dev/null
+++ b/client/src/href-builder.js
@@ -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;
+ }
+ };
+};
\ No newline at end of file
diff --git a/client/src/html-renderer.js b/client/src/html-renderer.js
new file mode 100644
index 000000000..6c91c094c
--- /dev/null
+++ b/client/src/html-renderer.js
@@ -0,0 +1,28 @@
+'use strict';
+
+var format = require('stringformat');
+
+var templates = require('./templates');
+
+module.exports = {
+ renderedComponent: function(data){
+
+ if(!!data.name && data.renderInfo !== false){
+ data.html += format(templates.renderInfo, data.name, data.version);
+ }
+
+ if(data.container !== false){
+ data.html = format(templates.componentTag, data.href, data.key, data.name || '', data.version, data.html);
+ }
+
+ return data.html;
+ },
+ unrenderedComponent: function(href, options){
+ if(!href){ return ''; }
+
+ var youCareAboutIe8 = !!options && !!options.ie8,
+ template = templates['componentUnrenderedTag' + (youCareAboutIe8 ? 'Ie8' : '')];
+
+ return format(template, href);
+ }
+};
\ No newline at end of file
diff --git a/client/src/index.js b/client/src/index.js
index 97e2b09f3..5eac6eaa1 100644
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -1,197 +1,38 @@
'use strict';
-var Cache = require('nice-cache');
-var format = require('stringformat');
-var fs = require('fs');
-var path = require('path');
-var querystring = require('querystring');
-var url = require('url');
-var vm = require('vm');
-
-var Handlebars = require('./renderers/handlebars');
-var Jade = require('./renderers/jade');
-
-var request = require('./utils/request');
+var ComponentsRenderer = require('./components-renderer');
var sanitiser = require('./sanitiser');
-var settings = require('./settings');
-var templates = require('./templates');
+var TemplateRenderer = require('./template-renderer');
var validator = require('./validator');
var _ = require('./utils/helpers');
-var isLocal = function(apiResponse){
- return apiResponse.type === 'oc-component-local';
-};
-
-var getRenderedComponent = function(data){
-
- if(!!data.name && data.renderInfo !== false){
- data.html += format(templates.renderInfo, data.name, data.version);
- }
-
- if(data.container !== false){
- data.html = format(templates.componentTag, data.href, data.key, data.name || '', data.version, data.html);
- }
-
- return data.html;
-};
-
-var getUnrenderedComponent = function(href, options){
- if(!href){ return ''; }
-
- var youCareAboutIe8 = !!options && !!options.ie8,
- template = templates['componentUnrenderedTag' + (youCareAboutIe8 ? 'Ie8' : '')];
-
- return format(template, href);
-};
-
module.exports = function(conf){
var config = sanitiser.sanitiseConfiguration(conf),
validationResult = validator.validateConfiguration(config),
- self = this;
+ renderTemplate = new TemplateRenderer(),
+ renderComponents = new ComponentsRenderer(config, renderTemplate);
if(!validationResult.isValid){
- throw validationResult.error;
+ throw new Error(validationResult.error);
}
- this.renderers = {
- handlebars: new Handlebars(),
- jade: new Jade()
- };
-
- var cache = new Cache(config.cache);
-
- var getCompiledTemplate = function(template, tryGetCached, timeout, callback){
- if(!!tryGetCached){
- var compiledTemplate = cache.get('template', template.key);
- if(!!compiledTemplate){
- return callback(null, compiledTemplate);
- }
- }
-
- request(template.src, {}, timeout, function(err, templateText){
-
- if(!!err){ return callback(err); }
-
- var context = { jade: require('jade/runtime.js')};
- vm.runInNewContext(templateText, context);
- var compiledTemplate = context.oc.components[template.key];
- cache.set('template', template.key, compiledTemplate);
- return callback(null, compiledTemplate);
- });
- };
-
- var buildComponentHrefs = function(name, params){
-
- var version = config.components[name],
- versionSegment = !!version ? (version + '/') : '';
-
- var getHref = function(renderMode){
- if(!!config.registries[renderMode]){
- var registryUrl = config.registries[renderMode],
- registrySegment = registryUrl.slice(-1) === '/' ? registryUrl : (registryUrl + '/'),
- qs = !!params ? ('?' + querystring.stringify(params)) : '';
-
- return url.resolve(registrySegment, name + '/') + versionSegment + qs;
- }
- };
-
- return {
- clientRendering: getHref('clientRendering'),
- serverRendering: getHref('serverRendering')
- };
- };
+ this.renderTemplate = renderTemplate;
this.renderComponent = function(componentName, options, callback){
- if(_.isFunction(options)){
- callback = options;
- options = {};
- }
-
- options = options || {};
- options.headers = options.headers || {};
- options.timeout = options.timeout || 5;
-
- if(!_.has(config.components, componentName)){
- config.components[componentName] = '';
- }
-
- self.render(buildComponentHrefs(componentName, options.params), options, callback);
- };
-
- this.render = function(hrefs, options, callback){
- if(_.isFunction(options)){
- callback = options;
- options = {};
- }
-
- options = options || {};
- options.headers = options.headers || {};
- options.headers.accept = 'application/vnd.oc.unrendered+json';
- options.timeout = options.timeout || 5;
-
- var processError = function(){
- var errorDescription = settings.messages.serverSideRenderingFail;
-
- if(!!options.disableFailoverRendering){
- return callback(errorDescription, '');
- }
-
- var unrenderedComponentTag = getUnrenderedComponent(hrefs.clientRendering, options);
-
- if(!unrenderedComponentTag){
- return callback(errorDescription);
- }
-
- fs.readFile(path.resolve(__dirname, './oc-client.min.js'), 'utf-8', function(err, clientJs){
- var clientSideHtml = format(templates.clientScript, clientJs, unrenderedComponentTag);
- return callback(errorDescription, clientSideHtml);
- });
- };
-
- if(options.render === 'client'){
- return callback(null, getUnrenderedComponent(hrefs.clientRendering, options));
- }
-
- request(hrefs.serverRendering, options.headers, options.timeout, function(err, apiResponse){
-
- if(err){
- return processError();
- } else {
-
- try {
- apiResponse = JSON.parse(apiResponse);
- } catch(e){
- return processError();
- }
-
- var data = apiResponse.data,
- local = isLocal(apiResponse);
-
- getCompiledTemplate(apiResponse.template, !local, options.timeout, function(err, template){
-
- if(!!err){ return processError(); }
-
- var renderOptions = {
- href: hrefs.clientRendering,
- key: apiResponse.template.key,
- version: apiResponse.version,
- templateType: apiResponse.template.type,
- container: (options.container === true) ? true : false,
- renderInfo: (options.renderInfo === false) ? false : true,
- name: apiResponse.name
- };
-
- return self.renderTemplate(template, data, renderOptions, callback);
- });
- }
+ if(_.isFunction(options)){ callback = options; }
+
+ renderComponents([{
+ name: componentName,
+ version: options.version,
+ parameters: options.parameters || options.params
+ }], options, function(errors, results){
+ callback(errors[0], results[0]);
});
};
- this.renderTemplate = function(template, data, options, callback){
- this.renderers[options.templateType].render(template, data, function(err, html){
- options.html = html;
- return callback(err, getRenderedComponent(options));
- });
+ this.renderComponents = function(components, options, callback){
+ if(_.isFunction(options)){ callback = options; }
+ renderComponents(components, options, callback);
};
};
\ No newline at end of file
diff --git a/client/src/sanitiser.js b/client/src/sanitiser.js
index 588473edc..5566ab4d5 100644
--- a/client/src/sanitiser.js
+++ b/client/src/sanitiser.js
@@ -1,5 +1,7 @@
'use strict';
+var _ = require('./utils/helpers');
+
module.exports = {
sanitiseConfiguration: function(conf){
conf = conf || {};
@@ -7,5 +9,24 @@ module.exports = {
conf.cache = conf.cache || {};
return conf;
+ },
+ sanitiseGlobalRenderOptions: function(options, config){
+
+ if(_.isFunction(options)){
+ options = {};
+ }
+
+ options = options || {};
+ options.headers = options.headers || {};
+ options.headers.accept = 'application/vnd.oc.unrendered+json';
+ options.timeout = options.timeout || 5;
+ options.container = (options.container === true) ? true : false;
+ options.renderInfo = (options.renderInfo === false) ? false : true;
+
+ if(!!config.registries && !config.registries.clientRendering){
+ options.disableFailoverRendering = true;
+ }
+
+ return options;
}
};
diff --git a/client/src/settings.js b/client/src/settings.js
index e5e4745e4..1b24d89ab 100644
--- a/client/src/settings.js
+++ b/client/src/settings.js
@@ -1,9 +1,10 @@
'use strict';
module.exports = {
- messages: {
- componentMissing: 'Configuration is not valid - Component not found',
- registryUrlMissing: 'Configuration is not valid - Registry location not found',
- serverSideRenderingFail: 'Server-side rendering failed'
- }
+ clientSideRenderingFail: 'Client-side rendering failed',
+ configurationNotValid: 'Configuration is not valid: ',
+ genericError: 'Internal client error',
+ registriesEmpty: 'registries must contain at least one endpoint',
+ registriesIsNotObject: 'registries must be an object',
+ serverSideRenderingFail: 'Server-side rendering failed'
};
\ No newline at end of file
diff --git a/client/src/template-renderer.js b/client/src/template-renderer.js
new file mode 100644
index 000000000..cac915fc8
--- /dev/null
+++ b/client/src/template-renderer.js
@@ -0,0 +1,20 @@
+'use strict';
+
+var Handlebars = require('./renderers/handlebars');
+var htmlRenderer = require('./html-renderer');
+var Jade = require('./renderers/jade');
+
+module.exports = function(){
+
+ var renderers = {
+ handlebars: new Handlebars(),
+ jade: new Jade()
+ };
+
+ return function(template, data, options, callback){
+ renderers[options.templateType].render(template, data, function(err, html){
+ options.html = html;
+ return callback(err, htmlRenderer.renderedComponent(options));
+ });
+ };
+};
\ No newline at end of file
diff --git a/client/src/templates.js b/client/src/templates.js
index 8c0f4eb37..19e5f87b4 100644
--- a/client/src/templates.js
+++ b/client/src/templates.js
@@ -6,7 +6,7 @@ module.exports = {
componentTag: '{4}',
- componentUnrenderedTag: '',
+ componentUnrenderedTag: '',
componentUnrenderedTagIe8: '