So I wondered if I could write some simple code to perfect the template engine and work with other existing logic. The Absurdjs itself is primarily published as a Nodejs module, but it also publishes a client version. With that in mind, I can't use the existing engine directly, because most of them run on Nodejs, not in the browser. What I need is a small, purely JavaScript-written thing that can run directly on the browser. When I stumbled upon John Resig's blog One day, I was pleasantly surprised to find that it was not exactly what I was looking for! I made some changes slightly, the number of lines of code is about 20 lines. The logic is very interesting. In this article I will reproduce the process of writing this engine step-by-step, and if you can see it all the way, you will understand how sharp John's idea is!
At first my idea was this:
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, input is our template as well as data objects, output estimates you can easily think of, as follows:
<p>hello, my name is Krasimir. I ' M years old.</p>
The first step is to look for the template parameters inside and replace them with the specific data that is passed to the engine. I decided to use a regular expression to complete this step. But I am not good at this, so write a bad words welcome to spray at any time.
var re =/<% ([^%>]+)?%>/g;
This regular expression captures all fragments that begin with <% and end with%>. The parameter G (global) at the end indicates that it matches not just one but all matched fragments. There are many ways to use regular expressions in JavaScript, and what we need is to output an array based on the regular expression, containing all the strings, which is what exec does.
var re =/<% ([^%>]+)?%>/g;
var match = re.exec (TPL);
If we print the variable match with Console.log, we will see:
[
"<%name%>",
"name",
index:21,
input:
"<p>hello, my name is <%name%>. I\ ' m <%age%> years old.</p> '
]
We can see, however, that the returned array contains only the first occurrence. We need to wrap this logic in a while loop to get all the matches.
var re =/<% ([^%>]+)?%>/g;
while (match = Re.exec (TPL)) {
console.log (match);
}
If you run through the above code, you will see that <%name%> and <%age%> are printed.
Now, here's the interesting part. After identifying the matches in the template, we will replace them with the actual data passed to the function. The easiest way to do this is to use the Replace function. We can write 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;
OK, so we can run, but it's not good enough. Here we use a simple object to pass data in the form of data["property", but in reality we probably need more complex nesting objects. So we modified the data object a little bit:
{
name: ' Krasimir Tsonev ', profile
: {age:29}
}
However, it is not possible to write directly, because the use of <%profile.age%> in the template, the code will be replaced with data[' profile.age ', the result is undefined. So we can't simply use the Replace function, but to use another method. It's best if you can use JavaScript code directly between <% and%>, so you can evaluate the incoming data directly, as follows:
Copy Code code as follows:
var template = ' <p>hello, my name is <%this.name%>. I\ ' m <%this.profile.age%> years old.</p> ';
You might wonder, how did this happen? Here John uses the syntax of the new function to create a function from a 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, 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 base the string constructor, including its arguments and function bodies. That's not what we want! But don't worry, before we start the constructor, let's look at what the function body looks like. According to the previous idea, the template engine should eventually return a compiled template. Or use the previous template string as an example, the returned content should resemble the following:
Return
' <p>hello, my name is ' +
THIS.name +
'. I\ ' m ' +
This.profile.age +
"Years old.</p>";
Of course, in the actual template engine, we cut the template into small pieces of text and meaningful JavaScript code. You might see me using a simple string concatenation to achieve the desired effect, but this is not the 100% approach that meets our requirements. Since the user is likely to deliver more complex JavaScript code, we need a loop here, as follows:
var template =
' my skills: ' +
' <%for (var index in this.skills) {%> ' +
' <a href= ' "><% This.skills[index]%></a> ' +
' <%}%> ';
If you use string concatenation, the code should look like this:
Return
' Me skills: ' + for
(var index in this.skills) {+
' <a href= ' "> ' +
This.skills[index] +
' </a> ' +
}
Of course, this code can not run directly, run will be wrong. So I used the logic written in John's article to put all the strings in an array and stitch them up at the end of the program.
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 step is to collect the different lines of code within the template for generating functions. With the methods described earlier, we can see what placeholders are in the template (or the matches for regular expressions) and where they are located. So, depending on an auxiliary 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 of 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}
}));
The variables in the code above are stored in the function body. The beginning section defines an array. The cursor cursor tells us which location in the template is currently parsed. We need to rely on it to traverse the entire template string. There is also a function add, which is responsible for adding the parsed lines to the variable code. One place to pay special attention is the need to escape (escape) The double quote characters that the code contains. Otherwise, the generated function code will be faulted. If we run the above code, we'll see the following in the console:
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 ("");
Wait, seems not very right Ah, this.name and this.profile.age should not have quotes Ah, and then to change.
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's actually valid JS
cursor = match.index + match[0].length;
}
The contents of the placeholder are passed to the Add function as arguments along with a Boolean value to be used as a distinction. So we can generate the function body we want.
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 only thing left to do is to create the function and execute it. Therefore, at the end of the template engine, replace the statement that originally returned the template string with the following:
Copy Code code as follows:
return new Function (Code.replace (/[\r\t\n]/g, ')). Apply (data);
We don't even need to explicitly pass parameters to this function. We use the Apply method to invoke it. It automatically sets the context in which the function executes. That's why we can use this.name inside a function. Here this points to the data object.
The template engine is nearing completion, but there is a point where we need to support more complex statements, such as conditional judgments and loops. We went on to write the example above.
var template =
' my skills: ' +
' <%for (var index in this.skills) {%> ' +
' <a href= ' # ' ><% This.skills[index]%></a> ' +
' <%}%> ';
Console.log (Templateengine (template, {
skills: ["JS", "HTML", "CSS"]
});
An exception is generated here, uncaught syntaxerror:unexpected token for. If we debug and print the code variable, we can see where the problem is.
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 ("");
The line with a for loop should not be placed directly into the array, but should run directly as part of the script. So we need to make more judgments 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 will determine whether the code contains if, for, else, and so on keywords. Add it to your script code if you want to, or add it to the array. The results of the operation are as follows:
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 ("");
Of course, the results of the compilation are also true.
Copy Code code as follows:
My skills:<a href= "#" >js</a><a href= "#" >html</a><a href= "#" >css</a>
The final improvement will make our template engine more powerful. We can use complex logic directly in the template, for example:
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
});
In addition to the improvements mentioned above, I have also made some optimizations for the code itself, and 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, only 15 lines!