patternjavascriptMajor
Templating libraries' intelligibility and their performance (compared to mine)
Viewed 0 times
minetemplatingintelligibilitylibrariesperformanceandcomparedtheir
Problem
I've just started writing my own little templating library in JavaScript, because as I went through others, there is always this voice in head, which says: "Oh, this is a lot of code and functionality. Is that really necessary or would it slow down the performance?"
If you are able to follow conventions, you would probably run into less issues.
So I ended up with this little script with the upper quote in my head. The convention, here is that data needs an object and its keys must be the same like in template, mentioned between those delimiter-brackets "{{key}}"
```
function Template(url, data) {
this.url = url;
this.data = data;
this.delimiter = ['{{','}}'];
this.load = function() {
var string = new String();
var http = null;
if ( window.XMLHttpRequest ) {
http = new XMLHttpRequest();
} else {
http = new ActiveXObject("Microsoft.XMLHTTP");
}
http.open('GET', url, false);
http.onreadystatechange = function() {
if ( http.readyState === 4 ) {
string = http.response;
}
};
http.send(null);
this.template = string;
return this;
};
this.exchange = function() {
for ( var key in this.data ) {
var bracketedKey = this.delimiter[0] + key + this.delimiter[1];
var indexOfKey = this.template.indexOf(bracketedKey);
var lengthOfKey = bracketedKey.length;
var exchange = this.template.substring(indexOfKey, indexOfKey+lengthOfKey);
this.template = this.template.replace(exchange, this.data[key]);
}
return this;
};
this.build = function(selector) {
this.load();
this.exchange();
var div = document.createElement('div');
div.innerHTML = this.template;
div = div.firstChild;
if ( typeof selector === 'string' ) {
document.querySelector(selector).ap
If you are able to follow conventions, you would probably run into less issues.
So I ended up with this little script with the upper quote in my head. The convention, here is that data needs an object and its keys must be the same like in template, mentioned between those delimiter-brackets "{{key}}"
```
function Template(url, data) {
this.url = url;
this.data = data;
this.delimiter = ['{{','}}'];
this.load = function() {
var string = new String();
var http = null;
if ( window.XMLHttpRequest ) {
http = new XMLHttpRequest();
} else {
http = new ActiveXObject("Microsoft.XMLHTTP");
}
http.open('GET', url, false);
http.onreadystatechange = function() {
if ( http.readyState === 4 ) {
string = http.response;
}
};
http.send(null);
this.template = string;
return this;
};
this.exchange = function() {
for ( var key in this.data ) {
var bracketedKey = this.delimiter[0] + key + this.delimiter[1];
var indexOfKey = this.template.indexOf(bracketedKey);
var lengthOfKey = bracketedKey.length;
var exchange = this.template.substring(indexOfKey, indexOfKey+lengthOfKey);
this.template = this.template.replace(exchange, this.data[key]);
}
return this;
};
this.build = function(selector) {
this.load();
this.exchange();
var div = document.createElement('div');
div.innerHTML = this.template;
div = div.firstChild;
if ( typeof selector === 'string' ) {
document.querySelector(selector).ap
Solution
... as I went through others, there is always this voice in head, which says: "Oh, this is a lot of code and functionality. Is that really necessary or would it slow down the performance?"
This is a reasonable consideration, but premature optimisation is evil for a reason. It might be a good idea to try to roll out your own solution, but it would be beneficial to learn more about the problem and other existing solutions before implementing it yourself.
As for minimalistic templating library, take a look at Underscore
It's literally 45 lines (add one or two helper functions) and does its job well.
As for your solution, I have a few points:
Don't use AJAX to load templates
If you're worried about performance, loading templates via AJAX is a bad idea. This will be a terrible bottleneck, not some hundred more lines of code in a different templating library.
Moreover, your code assumes you need to make an HTTP request each time you render something. If you have a list view with 20 items, that would be 20 requests.
This is absolutely unsuitable for production code.
Don't use synchronous AJAX calls, like, ever
The very point of AJAX is processing things asynchronously, without blocking other scripts from executing. There is rarely a reason to do things synchronously.
Again, please don't do this in production code.
Instead, put templates in HTML or (better) compile them to JS
Most, if not all, templating libraries assume the template is already available in the client code, and this is the correct way to go. Now, there are two ways how you can accomplish this:
The upside is that it is simple (no additional build steps), but the downside is that all your templates will have to be repeatedly passed in HTML with every page.
In this case, your build workflow will include an additional step when a certain command-line script goes over your templates/*.html
_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call
This is a reasonable consideration, but premature optimisation is evil for a reason. It might be a good idea to try to roll out your own solution, but it would be beneficial to learn more about the problem and other existing solutions before implementing it yourself.
As for minimalistic templating library, take a look at Underscore
_.template.It's literally 45 lines (add one or two helper functions) and does its job well.
As for your solution, I have a few points:
Don't use AJAX to load templates
If you're worried about performance, loading templates via AJAX is a bad idea. This will be a terrible bottleneck, not some hundred more lines of code in a different templating library.
Moreover, your code assumes you need to make an HTTP request each time you render something. If you have a list view with 20 items, that would be 20 requests.
This is absolutely unsuitable for production code.
Don't use synchronous AJAX calls, like, ever
The very point of AJAX is processing things asynchronously, without blocking other scripts from executing. There is rarely a reason to do things synchronously.
Again, please don't do this in production code.
Instead, put templates in HTML or (better) compile them to JS
Most, if not all, templating libraries assume the template is already available in the client code, and this is the correct way to go. Now, there are two ways how you can accomplish this:
- You can embed templates in DOM with tricks like `
The upside is that it is simple (no additional build steps), but the downside is that all your templates will have to be repeatedly passed in HTML with every page.
- But really, you should precompile templates to a JS file
In this case, your build workflow will include an additional step when a certain command-line script goes over your templates/*.html
files and compiles each HTML template into a JavaScript function that “takes” data arguments and returns the “rendered” HTML string.
For example, a template like
{{ caption }}
would be compiled in a function like
this["st"]["Template"]["templates/editor/items/gallery_media.html"] = function(obj) {
obj || (obj = {});
var __t, __p = '', __e = _.escape;
with (obj) {
__p += '\n \n\n \n\n \n\n ' +
((__t = ( caption )) == null ? '' : __t) +
'\n \n';
}
return __p
};
which would be placed in a generated templates.js file. This would be blazing fast.
Note that there is no overhead of parsing in this case, because parsing happens on your machine, during the build. The client uses pregenerated functions.
Such functions can be generated by most templating engines.
Don't parse the same template twice
Even if we bundle templates with the HTML (script type="text/template" approach), your code still suffers from the fact it parses the same template every time it is being rendered. That means, for 20 identical items, the same template is parsed 20 times. Of course, parsing is nothing compared to fetching HTML, but it's something that is relatively straightforward to optimize away.
Instead, you should parse the template once, somehow cache the “parsed” version and figure out how to “apply” it to different models. As I advocated before, the natural way to it is to make template build a function that corresponds to your template.
This is exactly what Underscore's _.template does, so you want to look at its implementation (see also an annotated version):
``_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call
Code Snippets
<div class="editor-Gallery-media">
<div class="editor-Gallery-mediaItem"
style="background-image: url({{ thumbnail_url }});"
data-orientation="{{ orientation }}">
<div class="editor-Gallery-deleteMediaItem"><i class="icon-cross"></i></div>
<div class="editor-GrabboxMedia-orientation"></div>
<div class="editor-Gallery-mediaItemCaption">{{ caption }}</div>
</div>
</div>this["st"]["Template"]["templates/editor/items/gallery_media.html"] = function(obj) {
obj || (obj = {});
var __t, __p = '', __e = _.escape;
with (obj) {
__p += '<div class="editor-Gallery-media">\n <div class="editor-Gallery-mediaItem"\n style="background-image: url(' +
((__t = ( thumbnail_url )) == null ? '' : __t) +
');"\n data-orientation="' +
((__t = ( orientation )) == null ? '' : __t) +
'">\n\n <div class="editor-Gallery-deleteMediaItem"><i class="icon-cross"></i></div>\n\n <div class="editor-GrabboxMedia-orientation"></div>\n\n <div class="editor-Gallery-mediaItemCaption">' +
((__t = ( caption )) == null ? '' : __t) +
'</div>\n </div>\n</div>';
}
return __p
};_.template = function(text, data, settings) {
var render;
settings = _.defaults({}, settings, _.templateSettings);
var matcher = new RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset)
.replace(escaper, function(match) { return '\\' + escapes[match]; });
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
}
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
}
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
index = offset + match.length;
return match;
});
source += "';\n";
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n";
try {
render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
return template;
};var string = new String();var template;Context
StackExchange Code Review Q#38435, answer score: 21
Revisions (0)
No revisions yet.