I don't know if you have heard of a Javascript-based Web page Preprocessor called AbsurdJS. I just want to write a pre-processor of CSS, and then extend it to CSS and HTML, which can be used to convert Javascript code into CSS and HTML code. Of course, because HTML code can be generated, you can also use it as a template engine to fill data in markup language. So I thought about whether I could write some simple code to improve the template engine and work with other existing logic. AbsurdJS itself is mainly released in the form of NodeJS modules, but it will also release the Client Version. With this in mind, I cannot directly use the existing engines, because most of them are running on NodeJS, rather than on browsers. What I need is something small and purely written in Javascript that can run directly on a browser. When I accidentally found John Resig's blog one day, I was pleasantly surprised to find that this is not exactly what I am searching! I made some slight changes, and the number of lines of code is about 20. The logic is very interesting. In this article, I will reproduce the process of writing this engine step by step. If you can read it all the way, you will understand how sharp John is!
At first, I thought like this:
var TemplateEngine = function(tpl, data) { // magic here ...}var template = 'Hello, my name is <%name%>. I\'m <%age%> years old.
';console.log(TemplateEngine(template, { name: "Krasimir", age: 29}));
For a simple function, the input is our template and data object. It is easy to think of the output, as shown below:
Hello, my name is clarimir. I'm 29 years old.
The first step is to find the template parameters and replace them with the specific data sent to the engine. I decided to use a regular expression to complete this step. However, I am not the best at this, so if you are not good at writing, you are welcome to try it at any time.
var re = /<%([^%>]+)?%>/g;
This regular expression captures all fragments starting with <% and ending with %>. The g (global) parameter at the end indicates that not only one is matched, but all the matching fragments are matched. There are many ways to use regular expressions in Javascript. What we need is to output an array containing all strings based on regular expressions, which is exactly what exec does.
var re = /<%([^%>]+)?%>/g;var match = re.exec(tpl);
If we use console. log to print the variable match, we will see:
[ "<%name%>", " name ", index: 21, input: "Hello, my name is <%name%>. I\'m <%age%> years old.
"]
However, we can see that the returned array only contains the first matching item. We need to wrap the above logic with a while loop to get all the matching items.
var re = /<%([^%>]+)?%>/g;while(match = re.exec(tpl)) { console.log(match);}
If you run the preceding code again, you will see that both <% name %> and <% age %> are printed.
The following is an interesting part. After identifying the matching items in the template, we need to replace them with the actual data passed to the function. The simplest way is to use the replace function. We can 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 can run it, but it's not good enough. Here we use data ["property"] to transmit data using a simple object. However, in actual situations, we may need more complex nested objects. So we slightly modified the data object:
{ name: "Krasimir Tsonev", profile: { age: 29 }}
However, you cannot run the script directly because <% profile. age %>, the code will be replaced with data ['profile. the result is undefined. In this way, we can't simply use the replace function, but use other methods. If you can use Javascript code directly between <% and %>, you can directly evaluate the passed data, as shown below:
The Code is as follows:
Var template ='
Hello, my name is <% this. name %>. I \'m <% this. profile. age %> years old.
';
You may wonder how this works? Here John uses the new Function syntax to create a Function based on the string. Let's take a look at an example:
var fn = new Function("arg", "console.log(arg + 1);");fn(2); // outputs 3
Fn is a real function. It accepts a parameter, and the function body 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 parameters and function bodies. This is not exactly what we want! But don't worry. Before constructing a function, let's take a look at what the function body looks like. According to previous ideas, the template engine should eventually return a compiled template. If the previous template string is used as an example, the returned content should be similar:
return"Hello, my name is " + this.name + ". I\'m " + this.profile.age + " years old.
";
Of course, in the actual template engine, we will split the Template into small sections of text and meaningful Javascript code. You may have seen that I use simple String concatenation to achieve the desired effect, but this is not 100% that meets our requirements. Since users are likely to pass more complex Javascript code, we need another loop here, as shown below:
var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<%this.skills[index]%>' +'<%}%>';
If String concatenation is used, the Code should look like the following:
return'My skills:' + for(var index in this.skills) { +'' + this.skills[index] +'' +}
Of course, this Code cannot be run directly, and an error occurs when it runs. So I used the logic written in John's article to put all the strings in an array and splice them at the end of the program.
var r = [];r.push('My skills:'); for(var index in this.skills) {r.push('');r.push(this.skills[index]);r.push('');}return r.join('');
The next step is to collect different code lines in the template for generating functions. Through the method described above, we can know the placeholders in the template (the Translator's note: or the matching items of the regular expression) and their locations. Therefore, relying on a helper 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 = 'Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.
';console.log(TemplateEngine(template, { name: "Krasimir Tsonev", profile: { age: 29 }}));
The variable code in the above code saves the function body. The section at the beginning defines an array. The cursor tells us which position in the template is currently parsed. We need to use it to traverse the entire template string. In addition, there is a function add, which is responsible for adding the parsed code lines to the variable code. Note that the double quotation mark characters in the code must be escaped (escape ). Otherwise, an error occurs in the generated function code. If we run the above Code, we will see the following content in the console:
var r=[];r.push("Hello, my name is ");r.push("this.name");r.push(". I'm ");r.push("this.profile.age");return r.join("");
And so on. this. name and this. profile. age should not be enclosed in quotation marks.
var add = function(line, js) { js? code += 'r.push(' + line + ');\n' : code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';}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 content of the placeholder and a Boolean value are passed as parameters to the add function for differentiation. In this way, we can generate the desired function body.
var r=[];r.push("Hello, my name is ");r.push(this.name);r.push(". I'm ");r.push(this.profile.age);return r.join("");
The rest is to create a function and execute it. Therefore, at the end of the template engine, replace the statement that originally returned the template string with the following content:
The Code is as follows:
Return new Function (code. replace (/[\ r \ t \ n]/g, ''). apply (data );
We do not even need to explicitly PASS Parameters to this function. We use the apply method to call it. It automatically sets the context of function execution. This is why we can use this. name in the function. Here this points to the data object.
The template engine is almost complete. However, we need to support more complex statements, such as condition judgment and loops. Let's continue with the above example.
var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<%this.skills[index]%>' +'<%}%>';console.log(TemplateEngine(template, { skills: ["js", "html", "css"]}));
Here an exception will occur, Uncaught SyntaxError: Unexpected token. If we debug and print out the code variable, we can find the problem.
var r=[];r.push("My skills:");r.push(for(var index in this.skills) {);r.push("");r.push(this.skills[index]);r.push("");r.push(});r.push("");return r.join("");
The line with a for loop should not be directly put into the array, but should be directly run as part of the script. Therefore, we need to make another judgment before adding the content to the code variable.
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';}
Here we add a new regular expression. It determines whether the Code contains keywords such as if, for, and else. If yes, add it to the script code. Otherwise, add it to the array. The running result is as follows:
var r=[];r.push("My skills:");for(var index in this.skills) {r.push("");r.push(this.skills[index]);r.push("");}r.push("");return r.join("");
Of course, the compiled results are also correct.
The Code is as follows:
My skills: jshtmlcss
The last improvement makes our template engine more powerful. We can directly use complex logic in the template, for example:
var template = 'My skills:' + '<%if(this.showSkills) {%>' + '<%for(var index in this.skills) {%>' + '<%this.skills[index]%>' + '<%}%>' +'<%} else {%>' + 'none
' +'<%}%>';console.log(TemplateEngine(template, { skills: ["js", "html", "css"], showSkills: true}));
In addition to the improvements mentioned above, I also optimized the Code itself. 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);}
The code is less than I expected. There are only 15 lines in the zone!