Only 20 lines of JavaScript template engine

  javascript, Template engine

Original link:JavaScript template engine in just 20 lines

(Translator Spits out: All collecting but not praising is hooliganism)

Preface

I am still pre-processing for my JSAbsurdJSCarry out development work. It was originally a CSS preprocessor, but later it was expanded into a CSS/HTML preprocessor. Soon it will support JS to CSS/HTML conversion. It can generate HTML code just like a template engine, that is, it can fill the identification fragments in the template with data.

Therefore, I hope to write a template engine that can meet my current needs.AbsurdJSIt is mainly used as a NodeJS module, but it can also be used at the client. For this purpose, I cannot use the template engines that already exist on the market, because they almost all depend on NodeJS and are difficult to use in browsers. I need a smaller, pure JS template engine. I read this article written by John Resig.BlogIt seems that this is exactly what I need. I slightly modified the code and condensed it to 20 lines.

The operation principle of this code is very interesting, and I will show John’s wonderful idea step by step in this article.

1, extracting identification fragments

This is what we will get at the beginning:

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
 }));

A simple function, passed inTemplateAndDataAs a parameter, as you can imagine, we want to get the following results:

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

The first thing we have to do is to get the identification fragments in the template.<%...%>, and then populate them with data passed into the engine. I decided to use regular expressions to accomplish these functions. Regularity is not my strong point, so everyone will make do with it. If there is a better regularity, please bring it up to me.

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

We will match all<%Begin with%>At the end of the code block, at the end of the g(global) said we will match more than one. There are many ways to match regularities, but we only need an array that can load strings, which is exactly what it isexecWork done:

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

At the consoleconsole.log(match)You can see:

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

We got the correct matching result, but as you can see, only one identification fragment was matched.<%name%>So we need onewhileLoop to get all the marker fragments.

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

Run, found that all the identification fragments have been obtained by us.

2. Data Filling and Logic Processing

After obtaining the identification fragments, we will fill them with data. Use.replaceThe method is the simplest way:

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

OK, normal operation. But obviously this is not enough. Our current data structure is very simple, but in actual development we will face more complex data structures:

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

The reason for the error is that when we enter in the template<%profile.age%>When we got itdata["profile.age"]It’s undefined. Obviously.replaceThe method doesn’t work, we need some other method to insert real JS code into <% and% >, just like the following chestnut:

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

This seems impossible? John used itnew Function, that is, to create a function through a string method to complete this function. Take a chestnut:

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

fnIs a real function, it contains a parameter, its function body isconsole.log(arg + 1). The above codes are equivalent to the following codes:

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

vianew Function, we can create a function from a string, which is exactly what we need. Before creating such a function, we need to construct its function body. The function body should return a final spliced template. Following the template string above, imagine the result this function should return:

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

Obviously, we divided the template into text and JS code. As with the above code, we used a simple string concatenation method to get the final result, but this method could not achieve our requirements 100%, because after that we also had to deal with JS logic such as loops, like this:

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

If string concatenation is used, the result will look like this:

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

Of course, this will report a mistake. This is also the reason why I decided to write logic according to John’s article-I push all strings into an array and finally spliced them together:

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('');

The next logical step is to sort out each line of code to generate a function. We have extracted some information from the template and know the content and location of the identification fragment, so we can use a cursor to help us achieve the final 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 }
 }));

VariablecodeBegin by declaring an array as the body of the entire function. As I said, pointer variablescursorIndicates where we are in the template, and we need it to traverse all strings and skip fragments of filled data. In addition,addThe function inserts a string into thecodeVariable, as the process method of constructing function body. Here is a tricky place, we need to skip the identifier<%%>Otherwise, the JS script will be invalidated. If we run the above code directly, the result will be the following:

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

Er … this is not what we want.this.nameAndthis.profile.ageThey should not be quoted. Let’s improveaddFunctions:

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;
 }

The contents of the identification clip are controlled by a boolean value. Now we have a correct function body:

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

The next thing we need to do is to generate this function and run it. At the end of this template engine, we use the following code instead of directly returning a.tplObject:

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

We don’t even need to pass any parameters to the function becauseapplyThe method has already completed this step of work for us. It automatically sets the scope, which is whythis.nameCan run,thisPointed to our data.

3. Code Optimization

It has been basically completed. Last thing, we need to support more complex expressions, likeif/elseExpressions and loops, etc. Let’s try to run the following code using the same example:

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

The result will be an error, the error isUncaught SyntaxError: Unexpected token for. Watch carefully and passcodeVariables we can find the problem:

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

ContainsforLoop code should not be push into the array, but directly into the script. In order to solve this problem, in the code push tocodeVariable before we need one more step of judgment:

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';
 }

We added a new regularization. The effect of this regularization is that if a JS codeif, for, else, switch, case, break, |At the beginning, they will be added directly to the function body. If not, it will be push tocodeIn the variable. The following is the revised result:

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

The correct execution of course:

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

The following changes will give us more powerful functions. We may have more complicated logic to put into the template, like this:

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
 }));

After some minor optimizations, 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, 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);
 }

The optimized code is even less than 15 lines.

Postscript

This is the first time that I have translated the article completely. There are many mistakes and omissions in the sentences. Please forgive me. I will continue to work hard in the future and try to share more high-quality articles in translation.

Because of my special interest in the front-end framework, template engine and other tools, I very much hope to be able to learn the principles, so I found a relatively simple template engine to do research. google saw this article and found it very excellent. The explanation was vivid and in-depth step by step. The code can correctly get the results described in the article through my own tests.

The template engine has a variety of design ideas. This article is only one of them. Its performance and other parameters have yet to be tested and improved. It is only for learning and use.
Thank you all ~