A brief analysis of the basic implementation method of JavaScript template engine

  • 2020-12-19 20:52:42
  • OfStack

Templates separate data from presentation, making presentation logic and effects easier to maintain. Using javascript's Function objects, step 1 builds a very simple template transformation engine

Introduction of the template
A template is usually a text embedded in some dynamic programming language code, and the data and template can be combined in some way to produce different results. Templates are often used to define the form of the display, making the data presentation richer and easier to maintain. For example, here is an example of a template:


<ul>
 <% for(var i in items){ %>
 <li class='<%= items[i].status %>'><%= items[i].text %></li>
 <% } %>
</ul>

If items data is as follows:


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]

In some way, this combination produces the following Html code:


<ul>
 <li class='done'>text1<li>
 <li class='pending'>text2<li>
 <li class='pending'>text3<li>
 <li class='processing'>text4<li>
</ul>

To achieve the same effect without using templates, present the above data as the result, as follows:


var temp = '<ul>';
for(var i in items){
 temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>";
}
temp += '</ul>';

You can see the following benefits of using templates:

Simplified html writing
Programming elements, such as loops and conditional branches, provide greater control over the presentation of data
Separation of data and presentation makes presentation logic and effects easier to maintain
A template engine
By analyzing templates, programs that combine data with templates to produce the final result are called template engines. There are many kinds of templates, and there are many kinds of corresponding template engines. One of the older templates, ERB, is used in many web frameworks, such as ASP.NET, Rails... The example above is the example of ERB. The two core concepts in ERB are evaluate and interpolate. On the surface evaluate means included in < % % > Part of the interpolate is included in < %= % > Part of. From the point of view of the template engine, the parts in evaluate are not directly output to the result, 1 is generally used for process control; The part in interpolate is directly output to the result.

From the perspective of template engine implementation, it relies on dynamic compilation or dynamic interpretation of programming language features to simplify implementation and improve performance. For example, ASP.NET uses dynamic compilation of.NET, which compiles templates into dynamic classes and uses reflection to dynamically execute code in the classes. This implementation is actually a bit more complicated because C# is a static programming language, but with javascript you can use Function to implement a simple template engine with very little code. In this article, we will implement a simple ERB template engine to demonstrate the power of javascript.

Template text transformation
For the above example, review the differences between using templates and not using templates:

Template writing:


<ul>
 <% for(var i in items){ %>
 <li class='<%= items[i].status %>'><%= items[i].text %></li>
 <% } %>
</ul>

Non-template writing:


var temp = '<ul>';
for(var i in items){
 temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>";
}
temp += '</ul>';

If you look closely, the two methods are actually 10 points "similar", and you can find a sense of 11. If you can turn the text of a template into code execution, you can achieve template transformation. There are two principles in the transformation process:

Encounter ordinary text as a string concatenation
Meet interpolate (i.e < %= % > ), which concatenates the contents of the string as variables
Meet evaluate (i.e < % % > ), just as code
Transform the above example according to the above principle, and add a total function:


var template = function(items){
 var temp = '';
 // Began to transform 
 temp += '<ul>';
 for(var i in items){
 temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>";
 }
 temp += '</ul>';
}

Finally, execute this function and pass in the data parameters:


var result = template(items);

javascript dynamic function
You can see that the above transformation logic is fairly simple, but the key point is that templates are mutable, which means that the generated program code must also be generated and executed at run time. The good news is that javascript has many dynamic features, one of which is a powerful feature called Function. We usually use the function keyword to declare functions in js, rarely Function. In js, function is a literal syntax, and the runtime of js converts a literal function to an Function object, so Function actually provides a more low-level and flexible mechanism.

The syntax for creating functions directly with the Function class is as follows:


var function_name = new Function(arg1, arg2, ..., argN, function_body)

Such as:


// Creating dynamic functions  
var sayHi = new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);");
// perform  
sayHi('Hello','World');

Function bodies and arguments can be created using strings! So cool! With this feature, you can convert template text into strings in the body of the function so that dynamic functions can be created for dynamic invocation.

Implementation approach
First, regular expressions are used to describe interpolate and evaluate. Parentheses are used to group capture:


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]
0

Combine these two regular expressions at 1 in order to match the entire template consecutively, but note that any string that matches interpolate matches evaluate, so interpolate needs to have a high priority:


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]
1

Design a function for converting templates with input parameters as template text strings and data objects


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]
2

Using the replace method for regular matching and "replacement", the purpose is not actually to replace interpolate or evaluate, but to build the "method body" during the matching process:


var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g
//text:  The template text string passed in 
//data:  The data object 
var template = function(text,data){
 var index = 0;// Where is the current scan recorded 
 var function_body = "var temp = '';";
 function_body += "temp += '";
 text.replace(matcher,function(match,interpolate,evaluate,offset){
 // Find the first 1 An expression that concatenates the preceding part as a normal string after a match 
 function_body += text.slice(index,offset);
 
 // If it is <% ... %> Directly as code snippets, evaluate That's the grouping of the captures 
 if(evaluate){
  function_body += "';" + evaluate + "temp += '";
 }
 // If it is <%= ... %> Concatenate strings, interpolate That's the grouping of the captures 
 if(interpolate){
  function_body += "' + " + interpolate + " + '";
 }
 // increasing index , skip evaluate or interpolate
 index = offset + match.length;
 // Here, return It doesn't make any sense, because it's not about substitution, right text But to build function_body
 return match;
 });
 // The final code should be return temp
 function_body += "';return temp;";
}

So far, function_body is a string, but its contents are actually 1 section of function code. You can use this variable to dynamically create 1 function object and call it with the data parameter:


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]
4

Thus render is a method that can be called. The code inside the method is constructed from the contents of the template, but the general framework should look like this:


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]
5

Note that the formal parameter of the method is obj, so the variable referenced within the template should be obj:


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]
6

This seems to be the end of OK, but there is a problem that must be solved. Template text may contain characters such as \r \n \u2028 \u2029 that would error if they appeared in the code, such as the following code:


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]
7

What we'd like to see is code like this:


temp += '\n \t\t<ul>\n' + ...;

In this way, you need to escape \\ from \n to \\ \ and finally turn it into a literal \\n.

In addition, there is a problem that the above code cannot join the last evaluate or interpolate, and the solution to this problem is simply to add a match at the end of a line to the regular formula:


items:[
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
]
9

Relatively complete code


var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g


// Special character escape processing in template text 
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
var escapes = {
  "'":   "'",
  '\\':   '\\',
  '\r':   'r',
  '\n':   'n',
  '\t':   't',
  '\u2028': 'u2028',
  '\u2029': 'u2029'
 };

//text:  The template text string passed in 
//data:  The data object 
var template = function(text,data){
 var index = 0;// Where is the current scan recorded 
 var function_body = "var temp = '';";
 function_body += "temp += '";
 text.replace(matcher,function(match,interpolate,evaluate,offset){
 // Find the first 1 An expression that concatenates the preceding part as a normal string after a match 
 // Added to handle escape characters 
 function_body += text.slice(index,offset)
  .replace(escaper, function(match) { return '\\' + escapes[match]; });

 // If it is <% ... %> Directly as code snippets, evaluate That's the grouping of the captures 
 if(evaluate){
  function_body += "';" + evaluate + "temp += '";
 }
 // If it is <%= ... %> Concatenate strings, interpolate That's the grouping of the captures 
 if(interpolate){
  function_body += "' + " + interpolate + " + '";
 }
 // increasing index , skip evaluate or interpolate
 index = offset + match.length;
 // Here, return It doesn't make any sense, because it's not about substitution, right text But to build function_body
 return match;
 });
 // The final code should be return temp
 function_body += "';return temp;";
 var render = new Function('obj', function_body);
 return render(data);
}

The calling code could look like this:


<script id='template' type='javascript/template'>
 <ul>
 <% for(var i in obj){ %>
  <li class="<%= obj[i].status %>"><%= obj[i].text %></li>
 <% } %>
 </ul>
</script>

...

var text = document.getElementById('template').innerHTML;
var items = [
 { text: 'text1' ,status:'done' },
 { text: 'text2' ,status:'pending' },
 { text: 'text3' ,status:'pending' },
 { text: 'text4' ,status:'processing' }
];
console.log(template(text,items));

As you can see, we implemented a simple template with very little code.

Legacy problems
There are a few details to note:

because < % or % > Are the boundary characters of the template, if the template needs output < % or % > , then you need to design escape methods If the data object contains null, you obviously don't want to end up with 'null', so you need to consider null in your function_body code It may be inconvenient to use obj's formal reference data every time in the template, you can add with(obj||{}){... }, so that obj attributes can be used directly in the template You can design render to be returned, rather than the converted result, so that the generated functions can be cached externally to improve performance

Related articles: