参考文章:只有20行Javascript代码!手把手教你写一个页面模板引擎 - 文章 - 伯乐在线
AbsurdJS
先来一个小case。
给出一个歌曲数组:
var songs = [
{name:"刚刚好",singer:"薛之谦",url:"http://music.163.com/xxx"},
{name:"最佳歌手",singer:"许嵩",url:"http://music.163.com/xxx"},
{name:"初学者",singer:"薛之谦",url:"http://music.163.com/xxx"},
{name:"绅士",singer:"薛之谦",url:"http://music.163.com/xxx"},
{name:"我们",singer:"陈伟霆",url:"http://music.163.com/xxx"},
{name:"画风",singer:"后弦",url:"http://music.163.com/xxx"},
{name:"We Are One",singer:"郁可唯",url:"http://music.163.com/xxx"},
]
需求:将数组内信息写入

这个很简单了,当时我想到的方法也是遍历嘛。
var songs = [
{name:"刚刚好",singer:"薛之谦",url:"http://music.163.com/xxx"},
{name:"最佳歌手",singer:"许嵩",url:"http://music.163.com/xxx"},
{name:"初学者",singer:"薛之谦",url:"http://music.163.com/xxx"},
{name:"绅士",singer:"薛之谦",url:"http://music.163.com/xxx"},
{name:"我们",singer:"陈伟霆",url:"http://music.163.com/xxx"},
{name:"画风",singer:"后弦",url:"http://music.163.com/xxx"},
{name:"We Are One",singer:"郁可唯",url:"http://music.163.com/xxx"},
]
for (var i=0,j=songs.length;i<j;i++) {
$(".hotSong").append(createHtml(i));
}
function createHtml(index) {
var htmls = "";
htmls += "<li><a href='" + songs[index]["url"] + "'>";
htmls += "<div class='song-name'>"+ songs[index]["name"] +"</div>";
htmls += "<div>-</div>";
htmls += "<div class='singer'>" + songs[index]["singer"] + "</div>";
htmls += "</a></li>";
return $(htmls);
}
但是,这种方法存在一些问题,我们来看一看。
- 如果用户传了这么个东西上来呢?:
{name:"初学者<script>alert('0')<\/script>",singer:"薛之谦",url:"http://music.163.com/xxx"},
运行一下....

那么第一个问题就是:
跨站脚本攻击(XSS Cross Site Scripting)
XSS 跨站脚本攻击 的防御解决方案
xss攻击原理与解决方法
那么我们换一种方法,通过构造DOM节点来push到页面上。
for (var i in songs) {
var node = document.querySelector(".hotSong");
node.appendChild(createHtml(i));
}
function createHtml(i) {
var fragment = document.createDocumentFragment();
var songNameDiv = document.createElement("div");
var songNameDivText = document.createTextNode(songs[i]["name"]);
songNameDiv.appendChild(songNameDivText);
var lineDiv = document.createElement("div");
lineDiv.appendChild(document.createTextNode("-"));
var singerDiv = document.createElement("div");
var singerDivText = document.createTextNode(songs[i]["singer"]);
singerDiv.appendChild(singerDivText);
var a = document.createElement("a");
a.href = songs[i]["href"];
a.appendChild(songNameDiv);
a.appendChild(lineDiv);
a.appendChild(singerDiv);
var li = document.createElement("li");
li.appendChild(a);
fragment.appendChild(li);
return fragment;
}
效果:

内嵌的script没有执行,因为它被识别为HTML了。
那么构造HTML的缺点是什么?
当然是烦琐复杂的代码,需要不断的构造节点,加入节点,不利于操作。
试试如果用jQuery改造能简单多少呢?
for (var i in songs) {
$(".hotSong").append(createHtml(i));
}
function createHtml(i) {
var $li = $("<li></li>");
var $a = $("<a></a>").attr("href",songs[i]["url"]);
var $songNameDiv = $("<div></div>").attr("class","song-name").text(songs[i]["name"]);
var $lineDiv = $("<div></div").text("-");
var $singerDiv = $("<div></div>").attr("class","singer").text(songs[i]["singer"]);
$a.append($singerDiv,$lineDiv,$songNameDiv);
$li.append($a);
return $li;
}

感觉少挺多哈。
我们可否实现这样一个功能的函数?

function stringFormat(string) {
var params = [].slice.call(arguments, 1); // 截取string后的代替参数
var regex = /\{(\d+)\}/g;
string = string.replace(regex, function () {
var index = arguments[1];
return params[index];
})
return string;
}
console.log(stringFormat("Hi , {0} , {1} , {2}","3","2","1")); // Hi , 3 , 2 , 1
console.log(stringFormat("Hi , {0}","Frank")); // Hi , Frank
console.log(stringFormat("Hi , {1}","Frank","Tomy")); // Hi , Tomy
console.log(stringFormat("Hi , {0} and {1}","Frank","Tomy")); // Hi , Frank and Tomy
以第一个console test为例,我们传入的参数是"Hi , {0} , {1} , {2}","3","2","1"
,用括号加以区别,("Hi , {0} , {1} , {2}") , ("3","2","1"))
,第一个参数"Hi , {0} , {1} , {2}"
中,大括号就是一个标记,目的就是将后续的替换值代替大括号所占位置,接下来逐步分析;
形参string保存了实参"Hi , {0} , {1} , {2}","3","2","1"
,在函数内部,我们可以在arguments类数组对象中找到它们,从实参的格式不难看出,只有第一个参数为原始字符串值,而后面的参数为替换值,"Hi , {0} , {1} , {2}"
为原始值,"3","2","1"
为依次要替换的值,所以需要取得除第一个参数外后面所有的参数,使用数组slice方法来实现。
var params = [].slice.call(arguments,1);
// Array(3) ["3", "2", "1"]
然后构造正则,识别原始值中的标记点{num}
。
var regex = /\{(\d+)\}/g;

测试一下:
var reg = /\{(\d+)\}/g
var text = "abcd{1}{2}cdff{adbc123}{-1}{0}";
console.log(text.match(reg)); // Array(3) ["{1}", "{2}", "{0}"]
这里是重点,先上一个MDN关于replace方法的链接:replace
因为最终需要替换字符串原始值,所以选择使用replace方法。
string.replace(regex,function() {
console.log(arguments);
var index = arguments[1];
return params[index];
})
首先我们明确一下,string指的是什么,是stringFormat("Hi , {0} , {1} , {2}" , "3","2","{4}")
参数里面的所有的值吗?当然不是,因为形参只有一个string,多余的实参会被arguments对象储存起来,但是函数内部的string指的是"Hi , {0} , {1} , {2}"
,逗号后的取不到。
在这个函数中,首先arguments对象发生改变,函数首先会对调用它的string
使用传入的regex
正则进行筛选,每一个被筛选中的值都会使arguments变化。我把每一次的arguments对象打印了出来看一看。

在replace中设置一个count,打印count得知replace运行了三次。
以第一次执行的argument对象为例:
arguments:Arguments(4) ["{0}", "0", 5, "Hi , {0} , {1} , {2}"]

- arguments[0]:匹配到的字串
- arguments[1]:
/\{(\d+)\}/g
小括号的意义就在于此,表示分组匹配的字符串。 - arguments[2]:取到的index值,不信数数
"Hi , {0} , {1} , {2}"
- arguments[3]:被匹配的原字符串。

第二次,第三次同理。
string = string.replace(regex,function() {
var index = arguments[1];
return params[index];
})
每一次,我们都会得到对象中的arguments[1],即替换标记,储存在index内。
然后用params对象的相同index位置的值替换它。
var params = [].slice.call(arguments,1);
// Array(3) ["3", "2", "1"]

试一个长一点的:
stringFormat("{0}今天去游乐场,{1}陪他去,路上碰到了{2},之后{3}一起结伴回家","小明","小红","小刚","小明小红小刚")
大体思路:
- 标记点设定(需要方便正则查找),如
{0}
- 保存替换值,如params数组。
- replace方法,正则取出index值,如
{1} => parmas[1]
注意点:
标记点和替换参数必须对应上
stringFormat("{0}今天去游乐场,{1}陪他去,路上碰到了{2},之后{3}一起结伴回家","小明","小红","小明小红小刚")
这里我没有传入{3}
的对应值,就成了这样。
小明今天去游乐场,小红陪他去,路上碰到了小明小红小刚,之后undefined一起结伴回家
替换参数括号
stringFormat("{0}今天去游乐场,{1}陪他去,路上碰到了{2},之后{3}一起结伴回家",("小明","小红","小刚","小明小红小刚"))
小明小红小刚今天去游乐场,undefined陪他去,路上碰到了undefined,之后undefined一起结伴回家
对替换参数加大括号,导致params数组中只有一个值,那么也会出现错误。
但是目前来说,还不能称之为一个模版引擎,只能做一对一的替换。
“升级“之后,就叫做模版引擎了。
var TemplateEngine = function(tpl,data) {
var regex = /<%([^%>]+)?%>/g;
while (match = regex.exec(tpl)) {
console.log(match);
tpl = tpl.replace(match[0],data[match[1]])
}
return tpl;
}
var template = "<p>Hello, My name is <%name%>. I'm <%age%> years old.</p>";
var data = {
name: "DH",
age: 21
}
var string = TemplateEngine(template,data);
console.log(string);
// <p>Hello, My name is DH. I'm 21 years old.</p>
template
是原始模版文本,data
对象是替换数据源,template()
函数就是主要的功能实现函数了。
有两个方法需要先了解,第一个就是replace,这个不多说,另外一个是正则的exec()方法。
regex.exec()
MDN——RegExp.prototype.exec()
同样的,在原始模版文本上,我们依然可以找到标记点,<%name%>
和<%age%>
,这是我们的正则表达式所决定的,原始模版文本将会作为TemplateEngine()
函数的参数之一传入。
/<%([^%>]+)?%>/g
:
-
/<%%>/g
:以<%
开始,%>
结束。 -
[^%>]
:这个字符集合中,字符不能为%
或>
(不能是这两个字符中的任何一个) -
[^%>]+
:这个集合可重复0次或多次。 -
([^%>]+)
:对其分组
...
总之,正则是为了获取到原始文本中的标记点的,相辅相成。
while (match = regex.exec(tpl)) {
console.log(match);
tpl = tpl.replace(match[0],data[match[1]])
}
regex.exec每一次遍历原始文本,每匹配到一个字串,都会返回一个数组,返回数组也能让while进行下去,当不再有字串符合,将返回undefined,那么while条件不成立,跳出循环。
每一次匹配到的如下:

之所以在正则中对其分组,也是为了获取<%string%>
中的关键字string
。

tpl = tpl.replace(match[0],data[match[1]])
接下来就使用replace进行替换了,match就是每一次匹配打印的数组中的匹配字串中的分组内容。将会按照这个内容会到data
数组中查找替换字串。
var data = {
name: "DH",
age: 21
}
over。
但是到这里还是有一些问题。
不信把data改成如下的形式试试。
{
name: "Krasimir Tsonev",
profile: { age: 29 }
}
作者后续给出了更佳的解决方案。(结合文章)
当时自己也看不懂,一个一个console看参数最后才理清,留一些备注,后续仅作为个人理解代码纪录吧。
var TemplateEngine = function (tpl, data) {
var re = /<%([^%>]+)?%>/g, // 标记点正则
code = 'var r=[];\n', // 使用code数组保存所有的字符串
cursor = 0; // 正则表达式的匹配项,以及它们的位置
var add = function (line, js) { // add函数用于(识别并)接收一个字符串 || JS执行代码(由while提供参数js),拼接为完整JS代码。
if (js) {
code += 'r.push(' + line + ');\n'; // JS走此处逻辑
} else {
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n'; // HTML走此处逻辑
// 需要把 code 包含的双引号字符进行转义(escape)。否则生成的函数代码会出错
}
// "<p>Hello, my name is "|| "this.name"
// r.push("<p>Hello, my name is ") || r.push("this.name");
}
while (match = re.exec(tpl)) { // 检测JS标记点
add(tpl.slice(cursor, match.index)); // 截至到JS标记点(cursor)之前所有的非JS(即HTML)字串作为参数传入add
add(match[1],true);
// add("this.name") || add("this.profile.age") , true表示此处为JS,不需要引号
cursor = match.index + match[0].length; // 更新cursor,准备下一段HTML传入
// cursor = 标记点字串开始位置 + 整个标记点的字串长度; cursor = 21 + 13 <%this.name%>.length
}
add(tpl.substr(cursor, tpl.length - cursor)); // 将最后的HTML Code取出。
code += 'return r.join("");'; // 末尾工序,拼接 return r.join("")
console.log(code);
// var r=[];
// r.push("<p>Hello, my name is ");
// r.push(this.name);
// r.push(". I'm ");
// r.push(this.profile.age);
// r.push(" years old.</p>");
// return r.join("");
return new Function(code.replace(/[\t\r\n]/g,'')).apply(data);
// 然后将完整的字串(code)传入函数类中,并apply了data,使得this正确指向相关数据,最后得出完整HTML Code
}
// 简单版,不支持复杂语句(if,for...)
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 }
}));
// 最终: <p>Hello, my name is Krasimir Tsonev. I'm 29 years old.</p>
最后一个改进可以使我们的模板引擎更为强大。我们可以直接在模板中使用复杂逻辑。
var TemplateEngine = function (tpl, data) {
var re = /<%([^%>]+)?%>/g; // 标记点正则
var reExp = /(^()?(if|for|else|switch|case|break|{|}))(.*)?/g; // 判断if,for,else等关键字正则
var code = 'var r=[];\n'; // 使用code数组保存所有的字符串
var cursor = 0; // 正则表达式的匹配项,以及它们的位置
var add = function (line, js) { // add函数用于(识别并)接收一个字符串 || JS执行代码(由while提供参数js),拼接为完整JS代码。
console.log(line);
if (js) {
if (line.match(reExp)) { // 是否为if,for等语句,是则直接加入其中,直接执行,不需要包含在push内部
code += line + "\n";
} else {
code += 'r.push(' + line + ');\n'; // 普通的JS语句,如this.name等
}
} else {
code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n'; // HTML语句逻辑走此处
// 需要把 code 包含的双引号字符进行转义(escape)。否则生成的函数代码会出错
}
}
while (match = re.exec(tpl)) { // 检测JS标记点
add(tpl.slice(cursor, match.index)); // 截至到JS标记点(cursor)之前所有的非JS(即HTML)字串作为参数传入add
add(match[1],true);
cursor = match.index + match[0].length; // 更新cursor,准备下一段HTML传入
// cursor = 标记点字串开始位置 + 整个标记点的字串长度;
}
add(tpl.substr(cursor, tpl.length - cursor)); // 将最后的HTML Code取出。
code += 'return r.join("");'; // 末尾工序,拼接 return r.join("")
console.log(code); // 以下就是进入new Function的JS语句,注意if
/* 打印code
var r = [];
r.push("My skills:");
if (this.showSkills) {
r.push("");
for (var index in this.skills) {
r.push("<a href=\"#\">");
r.push(this.skills[index]);
r.push("</a>");
}
r.push("");
} else {
r.push("<p>none</p>");
}
r.push("");
return r.join("");
*/
return new Function(code.replace(/[\t\r\n]/g,'')).apply(data);
// 完整的字串传入函数类中,并apply了data,使得this正确指向相关数据,最后得出完整HTML Code
}
// test,加入了更复杂的if语句
var template = "";
template += 'My skills:';
template += '<%if(this.showSkills) {%>';
template += '<%for(var index in this.skills) {%>';
template += '<a href="#"><%this.skills[index]%></a>';
template += '<%}%>';
template += '<%} else {%>';
template += '<p>none</p>';
template += '<%}%>';
console.log(TemplateEngine(template, {
skills: ["js", "html", "css"],
showSkills: true
}));
// 最终结果(true):My skills:<a href="#">js</a><a href="#">html</a><a href="#">css</a>
// 最终结果(false):My skills:<p>none</p>
网友评论