This post was first published on my blog: Icyfish.me

The body of the

JavaScript Template Engine in Just 20 lines JavaScript Template Engine in Just 20 lines JavaScript Template Engine in Just 20 lines

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
}));Copy the code

Now we’ll implement the TemplateEngine function, which takes two parameters, template and data. The following results occur after executing the above code:

<p>Hello, my name is Krasimir. I'm 29 years old.</p>Copy the code

First we must get the dynamic part of the template, and then we will replace the dynamic part with the real data of the two parameters, which can be achieved using regular expressions.

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

The above expression extracts everything that starts with <% and ends with %>, with g(global) at the end indicating that all items are matched. Then use the regexp.prototype.exec () method to store all the matching strings into an array.

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

Printing match yields something like this:

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

We extract the data, but only get one array element, we need to deal with all the matches, so we use a while loop:

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

After executing the above code, both <%name%> and <%age%> are extracted.

The next step is to replace placeholders with real data. The simplest way to do this is to use the string.prototype.replace () method:

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

For the example at the beginning of this article, the current approach (data[“property”]) would have done just fine for simple objects, but in reality you would have encountered more complex multilevel nested objects, such as:

{
    name: "Krasimir Tsonev".profile: { age: 29}}Copy the code

After changing the function’s second argument to the form above, there is no way to solve the problem using the above method, because when we enter <%profile.age%>, we get [“profile.age”] with undefined. The replace() method no longer applies. It would be nice to treat anything between <% and %> as JavaScript code that can be executed directly and return a value, such as:

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

Using the new Function() syntax, the constructor:

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

The fn function takes one argument and the body of the function is console.log(arg + 1). The above code is equivalent to:

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

Now we know that we can construct a simple function from a string in the same way. However, while implementing our requirements, we need to spend some time thinking about how to build the function body we want. This function returns a compiled template. Start experimenting with how to do this:

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

Separate templates into parts of text and JavaScript code. The desired results can be obtained with a simple merge. However, this method still does not meet our requirements 100%. Because if the content between <% and %> is not a simple variable, but something more complex, such as a loop statement, you will not get the expected result, for example:

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

If you use a simple merge, the result looks like this:

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

For (var index in this.skills) {does not execute properly, so instead of adding everything to the array, just add what is needed, and merge the array:

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

So the next step is to add lines of code to the body of the constructed function as appropriate, after extracting some relevant information from the template: the contents of the placeholders and where they are located. Then, defining an auxiliary variable (cursor) will achieve the desired result.

var TemplateEngine = function(tpl, data) {
    var re = [^ % > / < % (] +)? %>/g,
        code = 'var r=[]; \n',
        cursor = 0, 
        match;
    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}}));Copy the code

The value of the code variable is the body of our self-constructed function, which first defines an empty array. The cursor variable stores an index of the position of text in the template after content of the form <%this.name%>. We then created the Add function to add lines of code to the code variable. After that, we run into the tricky problem of using escape to solve the double quote “problem:

var r=[];
r.push("<p>Hello, my name is ");
r.push("this.name");
r.push(". I'm ");
r.push("this.profile.age");
return r.join("");Copy the code

This. name and this.profile.age should not be enclosed by double quotes. The add function can be modified to solve this problem:

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

If the contents of the placeholder are JS code, we pass it to the add function along with the Boolean true, which gives us the expected result:

var r=[];
r.push("<p>Hello, my name is ");
r.push(this.name);
r.push(". I'm ");
r.push(this.profile.age);
return r.join("");Copy the code

Then all we need to do is create this function and execute it. Instead of returning TPL in the TemplateEngine function, we return the function we created dynamically:

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

Instead of passing parameters directly into a function, call the function using the Apply method and pass in the parameters. This will create the correct scope and this. Name will execute correctly, with this pointing to the data object.

Finally, we want to implement some complex operations, such as if/else declarations and loops:

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

Uncaught SyntaxError: Unexpected Token for

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("");Copy the code

The line containing the for loop should not have been added to the array, so we improved it like this:

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 += 'r.push("' + line.replace(/"/g.'\ \ "') + '"); \n';
}Copy the code

The above code adds a new regular expression. If the JS code starts with if, for, else, switch, case, break, {,}, the line is added directly, not to the array. So the final result is:

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("");Copy the code

That way, everything compiles correctly.

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

The final improvement makes the function more powerful, and we can add complex logic directly to the template:

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
}));Copy the code

The final code, with some optimizations added, looks something like this:

var TemplateEngine = function(html, options) {
    var re = [^ % > / < % (] +)? %>/g, reExp = / (^ ()? (if|for|else|switch|case|break|{|}))(.*)? /g, code = 'var r=[]; \n', cursor = 0, match;
    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);
}Copy the code

reference

  • JavaScript Template Engine in Just 20 lines
  • JavaScript Micro-Templating