Please read Writing Your Own Non-Templating Engine before reading this article.
In the previous article (link at the top), I showed how you can write a very simple function that will turn a given template string into something JavaScript can use. In this article, I'll go into how you can use this in practise.
First, let's just make sure we're on the same page as to what the code is and what it does. And while we're at it, let's wrap it up in a function we can reference in later code examples:
function rewrite(code) { // just rewrites the code
return code.replace(/\{\{([\W\w]*?)}}/g, function(match, HTML) {
return "\"" +
HTML.replace(/[\\"]/g, "\\$&") // fix back-slash escaping and double-quotes
.replace(/\n/g, "\\n") // fix new lines
"\"";
});
}
function compile(code) { // compiles it into a function
return new Function("return function() { return " + rewrite(code) + "};");
}
NOTE: All code examples are just that. Examples. They likely won't be memory-efficient, or standard practice, or even that pretty. When you write your own version, use the concepts described here and make it as super-efficient or super-sloppy as you like. ;P
So now we've got a function that compiles templates into templating functions, we can make use of it:
var template = ...
{{<h1>}} + this.title + {{</h1>
<ul>}} +
this.items.map(function(item) {
return {{<li>
<a href="}} + item.href + {{">}} + item.title + {{</a>
(}} + item.progress + {{ \ }} + item.total + {{)
</li>}};
}).join("")
+ {{</ul>}}
```"f
```js
template = compile(template);
var page = template.call({
title: "Search Engines by Awesomeness",
items: [
{ title: "Google", href: "http://www.google.com/", progress: 10, total: 10 },
{ title: "Ask", href: "http://www.ask.com/", progress: 5, total: 10 }
]
});
page = ...
<h1>Search Engines by Awesomeness</h1>
<ul>
<li>
<a href="http://www.google.com/">Google</a>
(10 \ 10)
</li><li>
<a href="http://www.ask.com/">Ask</a>
(5 \ 10)
</li>
</ul>
As I've mentioned before, don't worry about the odd, lop-sided nature of rendered code. That way lies madness. But the HTML itself looks just as expected, right? You can call this compiled function whenever you want--in response to an Ajax call, on a timer, in a node http server, whatever you like.
Speaking of which...
A common way of using templates is to serve HTML pages. And what do we use for server-side JavaScript? Node. (Or your preferred equivalent. And remember, these principles can be used in any language which can compile itself.)
As Node uses the V8 JavaScript engine, all the same features you'd need are present on the server. It also allows you to read and write files, which will be useful later. Best of all, it has a great require
function that allows a .js file to load in another .js file (or a completely different module). This makes it really easy to separate different parts of your application into different files, and allow them to all talk to each other. This is also presents a minor hurdle we need to pass to get our templating working for any loaded-in .js files. We need to monkey-patch require
.
Let's quickly re-write our template and page variables as files our require can read:
template..js = ...
module.exports = {{<h1>}} + this.title + {{</h1>
<ul>}} +
this.items.map(function(item) {
return {{<li>
<a href="}} + item.href + {{">}} + item.title + {{</a>
(}} + item.progress + {{ \ }} + item.total + {{)
</li>}};
}).join("")
+ {{</ul>}};
page.js = ...
module.exports = require("./template..js").call({
title: "Search Engines by Awesomeness",
items: [
{ title: "Google", href: "http://www.google.com/", item.progress: 10, item.total: 10 },
{ title: "Ask", href: "http://www.ask.com/", item.progress: 5, item.total: 10 }
]
});
Now to write our own require function. Let's just go over a few main features of require
before we start:
- It accepts a path argument.
- There are 3 global variables that exist when running a module:
require
allows the module to require other files / modules.module
is an object that doesn't do much; it just holds a single property,.exports
. The module can add properties tomodule.exports
, or set it to something else.module.exports
is what is returned by therequire
function.- And finally,
exports
is just a shortcut formodule.exports
.
- If the path has already been
require
d, the cached object is returned instead of reading the file and creating a new one. This means it doesn't waste time in reading the same file more than once in the same node process, but also any variables set on the exported object (by the module itself, or by other .js files), or even variables within the module, will stay persistent betweenrequire
s, allowing for more complex behaviours.
This may all sound a bit like hard work, but it's easier than you think! But first, lets create a stub function we can work on.
function _require(path) {
if (/* we need to compile it */) {
// compile and return it here
}
else {
return require.apply(this, arugments);
}
}
So how should we decide if we need to compile the requested file? I decided on adding a little marker to the filename itself. This means that any time I look at that file (in node, in a file explorer, wherever), it's clear to me that it's a compiled file, rather than a straight-up .js file. For our example, I'm just adding a second "." before te "js" extension, but you can use any kind of pattern, marker, anything you like.
Also, remember that other, regular .js files could require
our ..js files, so we'll have to pass through our patched require function to those, too.
The only case where we don't have to worry about compiling ..js files is if we're requiring an outside module.
if (/^(?:\w:|\.\.?\/)/) { // starts with "C:" (or similar), or "./" or "../"; we need to compile it
// compile and return module
var code = require("fs").readFileSync(path, "utf-8"); // returns the text of the file as a string
if (/\.\.js$/.test(path)) { // is a compiled ..js file, so re-write the code
code = rewrite(code);
}
var compiled = new Function("require", "module", "exports", code);
// ^ these arguments take precedence over Node's globals
var module = { exports: {} };
compiled = compiled(_require, module, module.exports); // run it to get the template rendering function
return module.exports;
}
And as simple as that, we have our very own require function that compiles templated ..js files as needed. If you wanted, that could be all you do, and you'd have a fully-functional system that allows requiring and runs perfectly fine on server-side Node.
One last feature we can implement would be to add a caching system similar to that of Node's require
. This will mean that a .js file will be loaded and run only once within the lifetime of the process. This saves time, but more than that, it allows a single instance to be used by all files that require
the same file/module.
All it takes is a couple of lines of code:
var cache = {};
function _require(path) {
if (/^(?:\w:|\.\.?\/)/) {
if (path in cache) { return cache[path]; }
else {
// ...
cache[path] = module.exports;
return module.exports;
}
}
// ...
}
Perfect. Now all you need to do is call your require your first file with your new _require
function and the rest is taken care of.
As a bit of homework, think about how you might add more functionality:
- Pass in the path of the ..js file when it is compiled.
- Get freshly-rendered HTML every time you use the string somewhere (as in a http response, etc.).
- Have a list of ..js pages so that you can render links to the them.
- Write a http server using your templating engine.
- Write a build script that writes flat versions of the ..js files as html or css files (goodbye css pre-processing ;P).