The template separates data and presentation, making the presentation logic and effects easier to maintain. Using JavaScript’s Function object, build an extremely simple template conversion engine step by step
Template Introduction
Template usually refers to text embedded with some kind of dynamic programming language code. Data and templates can be combined in some form to produce different results. Templates are usually used to define the display form, which can make 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 there is the following items data:
items:[ { text: 'text1' ,status:'done' }, { text: 'text2' ,status:'pending' }, { text: 'text3' ,status:'pending' }, { text: 'text4' ,status:'processing' } ]
By combining it in some way, the following Html code can be generated:
<ul> <li class='done'>text1<li> <li class='pending'>text2<li> <li class='pending'>text3<li> <li class='processing'>text4<li> </ul>
If you want to achieve the same effect without using a template, that is, to display the above data as a result, you need to do the following:
var temp = '<ul>'; for(var i in items){ temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>"; } temp += '</ul>';
It can be seen that using templates has the following benefits:
Simplified html writing
Have more control over the presentation of data through programming elements (such as loops and conditional branches)
Separates data and display, making the display logic and effects easier to maintain
Template Engine
A program that combines data and templates to output the final result by analyzing templates is called a template engine. There are many types of templates, and there are many corresponding template engines. An older template is called ERB, which is used in many web frameworks, such as ASP.NET, Rails... The above example is an example of ERB. There are two core concepts in ERB: evaluate and interpolate. On the surface, evaluate refers to the part contained in <% %>, and interpolate refers to the part contained in <%= %>. From the perspective of the template engine, the part in evaluate will not be directly output to the result and is generally used for process control; while the part in interpolate will be directly output to the result.
From the perspective of the implementation of the template engine, it needs to rely on the dynamic compilation or dynamic interpretation features of the programming language to simplify the implementation and improve performance. For example: ASP.NET uses .NET's dynamic compilation to compile templates into dynamic classes, and uses reflection to dynamically execute the code in the class. This implementation is actually more complicated because C# is a static programming language, but using JavaScript you can use Function to implement a simple template engine with very little code. This article will implement a simple ERB template engine to show the power of JavaScript.
Template text conversion
For the above example, review the difference 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>';
Looking carefully, the two methods are actually very "similar" and can be found in a certain sense of one-to-one correspondence. If the text of the template can be turned into code for execution, then template conversion can be achieved. There are two principles in the conversion process:
When encountering ordinary text, it is directly concatenated into strings
When encountering interpolate (i.e. <%= %>), the content is treated as a variable and spliced into the string
When encountering evaluate (i.e. <% %>), it is directly treated as code
Transform the above example according to the above principles and add a general function:
var template = function(items){ var temp = ''; //开始变换 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
It can be seen that the above conversion logic is actually very simple, but the key problem is that the template changes, which means that the generated program code must also be generated and executed at runtime. Fortunately, JavaScript has many dynamic features, one of which is Function. We usually use the function keyword to declare functions in js, and Function is rarely used. In js, function is a literal syntax. The runtime of js will convert the literal function into a Function object, so Function actually provides a more low-level and flexible mechanism.
The syntax for directly creating a function using the Function class is as follows:
var function_name = new Function(arg1, arg2, ..., argN, function_body)
For example:
//创建动态函数 var sayHi = new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);"); //执行 sayHi('Hello','World');
Both function body and parameters can be created through strings! So cool! With this feature, the template text can be converted into a string of function body, so that dynamic functions can be created and called dynamically.
Implementation ideas
First use regular expressions to describe interpolate and evaluate, and parentheses are used to group capture:
var interpolate_reg = /<%=([\s\S]+?)%>/g; var evaluate_reg = /<%([\s\S]+?)%>/g;
In order to continuously match the entire template, these two regular expressions are merged together, but note that all strings that can match interpolate can match evaluate, so interpolate needs to have a higher priority:
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g
Design a function for converting templates, the input parameters are template text strings and data objects
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ ... }
使用replace方法,进行正则的匹配和“替换”,实际上我们的目的不是要替换interpolate或evaluate,而是在匹配的过程中构建出“方法体”:
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ var index = 0;//记录当前扫描到哪里了 var function_body = "var temp = '';"; function_body += "temp += '"; text.replace(matcher,function(match,interpolate,evaluate,offset){ //找到第一个匹配后,将前面部分作为普通字符串拼接的表达式 function_body += text.slice(index,offset); //如果是<% ... %>直接作为代码片段,evaluate就是捕获的分组 if(evaluate){ function_body += "';" + evaluate + "temp += '"; } //如果是<%= ... %>拼接字符串,interpolate就是捕获的分组 if(interpolate){ function_body += "' + " + interpolate + " + '"; } //递增index,跳过evaluate或者interpolate index = offset + match.length; //这里的return没有什么意义,因为关键不是替换text,而是构建function_body return match; }); //最后的代码应该是返回temp function_body += "';return temp;"; }
至此,function_body虽然是个字符串,但里面的内容实际上是一段函数代码,可以用这个变量来动态创建一个函数对象,并通过data参数调用:
var render = new Function('obj', function_body); return render(data);
这样render就是一个方法,可以调用,方法内部的代码由模板的内容构造,但是大致的框架应该是这样的:
function render(obj){ var temp = ''; temp += ... ... return temp; }
注意到,方法的形参是obj,所以模板内部引用的变量应该是obj:
<script id='template' type='javascript/template'> <ul> <% for(var i in obj){ %> <li class="<%= obj[i].status %>"><%= obj[i].text %></li> <% } %> </ul> </script>
看似到这里就OK了,但是有个必须解决的问题。模板文本中可能包含\r \n \u2028 \u2029等字符,这些字符如果出现在代码中,会出错,比如下面的代码是错误的:
temp += ' <ul> ' + ... ;
我们希望看到的应该是这样的代码:
temp += '\n \t\t<ul>\n' + ...;
这样需要把\n前面的转义成\即可,最终变成字面的\\n。
另外,还有一个问题是,上面的代码无法将最后一个evaluate或者interpolate后面的部分拼接进来,解决这个问题的办法也很简单,只需要在正则式中添加一个行尾的匹配即可:
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g;
相对完整的代码
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g //模板文本中的特殊字符转义处理 var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\t': 't', '\u2028': 'u2028', '\u2029': 'u2029' }; //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ var index = 0;//记录当前扫描到哪里了 var function_body = "var temp = '';"; function_body += "temp += '"; text.replace(matcher,function(match,interpolate,evaluate,offset){ //找到第一个匹配后,将前面部分作为普通字符串拼接的表达式 //添加了处理转义字符 function_body += text.slice(index,offset) .replace(escaper, function(match) { return '\\' + escapes[match]; }); //如果是<% ... %>直接作为代码片段,evaluate就是捕获的分组 if(evaluate){ function_body += "';" + evaluate + "temp += '"; } //如果是<%= ... %>拼接字符串,interpolate就是捕获的分组 if(interpolate){ function_body += "' + " + interpolate + " + '"; } //递增index,跳过evaluate或者interpolate index = offset + match.length; //这里的return没有什么意义,因为关键不是替换text,而是构建function_body return match; }); //最后的代码应该是返回temp function_body += "';return temp;"; var render = new Function('obj', function_body); return render(data); }
调用代码可以是这样:
<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));
可见,我们只用了很少的代码就实现了一个简易的模板。
遗留的问题
还有几个细节的问题需要注意: