Teach you how to use javascript to write a simple page template engine

  • 2020-06-07 03:57:31
  • OfStack

So I wondered if I could write some simple code to improve the template engine and work with other existing logic. AbsurdJS itself is primarily distributed as modules of NodeJS, although it also releases client-side versions. With that in mind, I couldn't use the existing engines directly, since most of them run on NodeJS, not the browser. What I need is a small, pure Javascript thing that runs directly in the browser. When I stumbled across John Resig's post the other day, I was pleasantly surprised to find that it was exactly what I was looking for. I made 1 slight change and the number of lines of code was about 20. The logic is interesting. In this article I will recreate the process of writing the engine step by step, and if you can follow it step by step, you will see how sharp John's idea is!

My initial thought was this:


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

1 simple function, input is our template and data object, output is easy to imagine, 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 the specific data passed to the engine. I decided to use regular expressions to complete this step. But I'm not the best at it, so if you don't write well, feel free to do it.


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

This regular expression will capture all of the < % starts with % > The closing segment. The parameter g (global) at the end means that not just one matches, but all matches. There are many ways to use regular expressions in Javascript, what we need is to output an array containing all the strings based on the regular expression, which is exactly what exec does.


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

If we print the variable match using ES35en.log, 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 contains only the first match. We need to wrap the above logic in an while loop to get all the matches.


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

If you run through the code above, you'll see < %name% > and < %age% > It's all printed out.

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


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

Okay, so we're running, but it's not good enough. Here we use a simple object in the form of data["property"] to pass data, but in reality we would probably need more complex nested objects. So we slightly modified the 1 data object:


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

But you can't just write it like this because you're using it in the template < %profile.age% > The code is replaced with data[' profile.age '], resulting in undefined. So instead of simply using the replace function, we have to use something else. If I could < % and % > It is best to use the Javascript code to evaluate the incoming data, as follows:


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

You might be wondering, how does this work? Here John USES the syntax of new Function to create a function based on a string. Here's an example:


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

fn is a real function. It takes one argument, the body of which is console. log(arg + 1); . The above code is equivalent to the following code:


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

In this way, we can construct a function based on a string, including its arguments and body. That's just what we want! But before we do that, let's look at what the body of the function looks like. As previously thought, the template engine should eventually return a compiled template. Using the previous template string as an example, the return should look like this:


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

Of course, in a real template engine, we would split the template into small pieces of text and meaningful Javascript code. You may have seen me using simple string concatenation to achieve the desired effect, but this is not 100% correct. Since the user is likely to pass more complex Javascript code, we need another loop here, as follows:


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

If string concatenation were used, the code would look like this:


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

Of course, this code cannot be run directly, it will make mistakes. So I used the logic from the John article, putting all the strings in an array and stitching them together at the end of the program.


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

The next step is to collect the different lines of code in the template to generate the function. Using the above methods, we can know which placeholders are in the template and where they are located. Therefore, with one auxiliary variable (cursor, 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 += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
  }
  while(match = re.exec(tpl)) {
    add(tpl.slice(cursor, match.index));
    add(match[1]);
    cursor = match.index + match[0].length;
  }
  add(tpl.substr(cursor, tpl.length - cursor));
  code += 'return r.join("");'; // <-- return the result
  console.log(code);
  return tpl;
}
var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
console.log(TemplateEngine(template, {
  name: "Krasimir Tsonev",
  profile: { age: 29 }
}));

The variable code in the above code holds the body of the function. The first section defines an array. The cursor cursor tells us which position in the template is currently parsed. We need to rely on it to traverse the entire template string. There is also a function called add, which adds the parsed lines to the variable code. One area of particular concern is the need to escape the double quote characters contained in code (escape). Otherwise the generated function code will error. If we run the above code, we will see the following in the console:


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

Wait, that doesn't seem right. this.name and this.profile.age shouldn't have quotes.


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

The contents of the placeholder and a Boolean value of 1 are passed to the add function as arguments to distinguish them. And that gives us the body of the function that we want.


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

All that remains is to create the function and execute it. So, at the end of the template engine, replace the statement that would have returned the template string with the following:

return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);

We don't even need to explicitly pass arguments to this function. We call it using the apply method. It automatically sets the context in which the function executes. That's why we can use this.name in functions. Here this points to the data object.

The template engine is nearly complete, but there is one more point where we need to support more complex statements, such as conditional judgment and loops. Let's continue with the example above.


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

This produces an exception, Uncaught SyntaxError: Unexpected token for. If we debug 1 and print out the code variable, we can see what the problem is.


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

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


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

Here we add a new regular expression. It determines whether the code contains if, for, else, and so on. Add it directly to the script code if you have it, otherwise add it to the array. The operation results are as follows:


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

Of course, the results are correct.


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

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


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

In addition to the above improvements, I made a few tweaks 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 += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
      (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
    return add;
  }
  while(match = re.exec(html)) {
    add(html.slice(cursor, match.index))(match[1], true);
    cursor = match.index + match[0].length;
  }
  add(html.substr(cursor, html.length - cursor));
  code += 'return r.join("");';
  return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);
}

Less code than I expected, only 15 lines!


Related articles: