SoFunction
Updated on 2025-03-08

Teach you how to write a page template engine using javascript

So I thought about whether I could write some simple code to improve this template engine and work in conjunction with other existing logic. AbsurdJS itself is mainly released in the form of NodeJS modules, but it will also release client versions. With all this in mind, I can't use existing engines directly, because most of them run on NodeJS and cannot run on the browser. What I need is a small, purely written in Javascript that can run directly on the browser. When I accidentally discovered this blog by John Resig one day, I was surprised to find that this is what I was looking for! I made some changes and the number of lines of code is about 20. The logic is very interesting. In this article, I will reproduce the process of writing this engine step by step. If you can read it all the way, you will understand how sharp John's idea is!

My initial thought was like this:

var TemplateEngine = function(tpl, data) {
  // magic here ...
}
var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
(TemplateEngine(template, {
  name: "Krasimir",
  age: 29
}));

A simple function, the input is our template and data object. It is estimated that you can easily think of the output, like the following:

<p>Hello, my name is Krasimir. I'm 29 years old.</p>
The first step is to find the template parameters inside and replace them with specific data passed to the engine. I decided to use regular expressions to accomplish this. But I am not the best at this, so if you don’t write well, you are welcome to spit it anytime.

var re = /<%([^%>]+)?%>/g;

This regular expression captures all fragments that start with <% and end with %>. The parameter g (global) at the end means that it matches not only one, but all the corresponding fragments. There are many ways to use regular expressions in Javascript. What we need is to output an array based on the regular expression, containing all the strings, which is exactly what exec does.

var re = /<%([^%>]+)?%>/g;
var match = (tpl);

If we print out the variable match, we will see:

[
  "<%name%>",
  " name ", 
  index: 21,
  input: 
  "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>"
]

However, we can see that the returned array only contains the first match. We need to wrap the above logic with a while loop so that we can get all the matches.

var re = /<%([^%>]+)?%>/g;
while(match = (tpl)) {
  (match);
}

If you run the above code, you will see that <%name%> and <%age%> are printed out.

Below, the interesting part is here. After identifying matches in the template, we replace them with the actual data passed to the function. The easiest way is to use the replace function. We can write like this:

var TemplateEngine = function(tpl, data) {
  var re = /<%([^%>]+)?%>/g;
  while(match = (tpl)) {
    tpl = (match[0], data[match[1]])
  }
  return tpl;
}

OK, so you can run away, but it's not good enough. Here we use a simple object to pass data in the way of data["property"], but in reality we may need more complex nested objects. So we slightly modified the data object:

{
  name: "Krasimir Tsonev",
  profile: { age: 29 }
}

However, if you write this directly, you can't run it, because if you use <%%> in the template, the code will be replaced with data[''], and the result is undefined. In this way, we cannot simply use the replace function, but use other methods. It would be best if you can directly use Javascript code between <% and %>, so that you can directly evaluate the incoming data, like the following:

Copy the codeThe code is as follows:

var template = '<p>Hello, my name is <%%>. I\'m <%%> years old.</p>';

You may be curious, how did this happen? Here John uses the syntax of new Function to create a function based on the string. Let's take a look at an example:

var fn = new Function("arg", "(arg + 1);");
fn(2); // outputs 3

fn is a real function. It accepts an argument, and the function body is (arg + 1);. The above code is equivalent to the following code:

var fn = function(arg) {
  (arg + 1);
}
fn(2); // outputs 3

Through this method, we can construct the function according to the string, including its parameters and function body. Isn’t this exactly what we want! But don't worry, before constructing the function, let's take a look at what the function body looks like. According to previous ideas, the template engine should eventually return a compiled template. Or use the previous template string as an example, then the returned content should be similar to:

return
"<p>Hello, my name is " + 
 + 
". I\'m " + 
 + 
" years old.</p>";

Of course, in the actual template engine, we will divide the template into small segments of text and meaningful Javascript code. You may have seen me using simple string splicing to achieve the desired effect, but this is not 100% in line with our requirements. Since users are likely to pass more complex Javascript code, we need to have another loop here, as follows:

var template = 
'My skills:' + 
'<%for(var index in ) {%>' + 
'<a href=""><%[index]%></a>' +
'<%}%>';

If you use string splicing, the code should look like the following:

return
'My skills:' + 
for(var index in ) { +
'<a href="">' + 
[index] +
'</a>' +
}

Of course, this code cannot be run directly, and it will cause errors if it runs away. So I used the logic written in John's article, put all the strings in an array, and spliced ​​them at the end of the program.

var r = [];
('My skills:'); 
for(var index in ) {
('<a href="">');
([index]);
('</a>');
}
return ('');

The next step is to collect different lines of code in the template to generate functions. Through the methods introduced above, we can know which placeholders are in the template (translator's note: or regular expression matches) and their locations. So, relying on a auxiliary variable (cursor) we can get the desired result.

var TemplateEngine = function(tpl, data) {
  var re = /<%([^%>]+)?%>/g,
    code = 'var r=[];\n',
    cursor = 0;
  var add = function(line) {
    code += '("' + (/"/g, '\\"') + '");\n';
  }
  while(match = (tpl)) {
    add((cursor, ));
    add(match[1]);
    cursor =  + match[0].length;
  }
  add((cursor,  - cursor));
  code += 'return ("");'; // <-- return the result
  (code);
  return tpl;
}
var template = '<p>Hello, my name is <%%>. I\'m <%%> years old.</p>';
(TemplateEngine(template, {
  name: "Krasimir Tsonev",
  profile: { age: 29 }
}));

The variable code in the above code saves the function body. The beginning part defines an array. The cursor tells us where the current parsed in the template is. We need to rely on it to iterate through the entire template string. There is also a function add, which is responsible for adding parsed lines of code to the variable code. There is one thing that needs to be paid special attention to, that is, the double quote characters included in the code need to be escaped (escape). Otherwise, the generated function code will error. If we run the above code, we will see the following content in the console:

var r=[];
("<p>Hello, my name is ");
("");
(". I'm ");
("");
return ("");

Wait, it seems that it is not right, and there shouldn't be quotes, so let's change it.

var add = function(line, js) {
  js? code += '(' + line + ');\n' :
    code += '("' + (/"/g, '\\"') + '");\n';
}
while(match = (tpl)) {
  add((cursor, ));
  add(match[1], true); // <-- say that this is actually valid js
  cursor =  + match[0].length;
}

The content of the placeholder is passed as an argument to the add function and is used as a distinction. This will generate the function body we want.

var r=[];
("<p>Hello, my name is ");
();
(". I'm ");
();
return ("");

All that's left to do is create the function and execute it. Therefore, at the end of the template engine, replace the statement that originally returned to the template string with the following content:

Copy the codeThe code is as follows:
return new Function((/[\r\t\n]/g, '')).apply(data);

We don't even need to explicitly pass parameters to this function. We use the apply method to call it. It automatically sets the context of function execution. This is why we can use it in functions. Here this points to the data object.

The template engine is close to completion, but there is another point that we need to support more complex statements, such as conditional judgments and loops. Let's continue writing the above example.

var template = 
'My skills:' + 
'<%for(var index in ) {%>' + 
'<a href="#"><%[index]%></a>' +
'<%}%>';
(TemplateEngine(template, {
  skills: ["js", "html", "css"]
}));

An exception will be generated here, Uncaught SyntaxError: Unexpected token for. If we debug and print out the code variable, we will find out the problem.

var r=[];
("My skills:");
(for(var index in ) {);
("<a href=\"\">");
([index]);
("</a>");
(});
("");
return ("");

The line with the for loop should not be placed directly into the array, but should be run directly as part of the script. So we need to make an extra judgment before adding the content to the code variable.

var re = /<%([^%>]+)?%>/g,
  reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,
  code = 'var r=[];\n',
  cursor = 0;
var add = function(line, js) {
  js? code += (reExp) ? line + '\n' : '(' + line + ');\n' :
    code += '("' + (/"/g, '\\"') + '");\n';
}

Here we have added a new regular expression. It will determine whether the code contains keywords such as if, for, else, etc. If there is one, add it directly to the script code, otherwise add it to the array. The operation results are as follows:

var r=[];
("My skills:");
for(var index in ) {
("<a href=\"#\">");
([index]);
("</a>");
}
("");
return ("");

Of course, the compiled result is also correct.

Copy the codeThe code is as follows:

My skills:<a href="#">js</a><a href="#">html</a><a href="#">css</a>

The last improvement can make our template engine more powerful. We can use complex logic directly in templates, for example:

var template = 
'My skills:' + 
'<%if() {%>' +
  '<%for(var index in ) {%>' + 
  '<a href="#"><%[index]%></a>' +
  '<%}%>' +
'<%} else {%>' +
  '<p>none</p>' +
'<%}%>';
(TemplateEngine(template, {
  skills: ["js", "html", "css"],
  showSkills: true
}));

In addition to the improvements mentioned above, I also made some optimizations to the code itself, and the final version is as follows:

var TemplateEngine = function(html, options) {
  var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0;
  var add = function(line, js) {
    js? (code += (reExp) ? line + '\n' : '(' + line + ');\n') :
      (code += line != '' ? '("' + (/"/g, '\\"') + '");\n' : '');
    return add;
  }
  while(match = (html)) {
    add((cursor, ))(match[1], true);
    cursor =  + match[0].length;
  }
  add((cursor,  - cursor));
  code += 'return ("");';
  return new Function((/[\r\t\n]/g, '')).apply(options);
}

The code is less than I expected, only 15 lines!