曾经阅读过《只有20行JAVASCRIPT代码, 手把手教你写一个页面模版引擎》这篇文章, 对其中实现模版的想法实在膜拜, 于是有了这篇读后感, 谈谈自己对模版引擎的理解, 以及用自己的语言探讨他的实现方式, 加深理解
文章地址: http://krasimirtsonev.com/blog/article/Javascript-template-engine-in-just-20-line
前端编写过程中, 经常性的会出现需要在js中组合, 或生成html插入到页面中
类似:
$.get('/data.json', function (json) {
var dataList = json.data,
index, length, html = ''; for (index = 0, length = dataList.length; index < length; index++) {
html += '<div>' + dataList[index'].name + '</div>';
} document.getElementById('content').innerHTML = html;
});
如果需要生成的html比较简单, 没有什么逻辑判断的时候, 并不觉得有什么问题, 但...
$.get('/data.json', function (json) {
var dataList = json.data;
index, length, data, html = ''; for (index = 0, length = dataList.length; index < length; index++) {
data = dataList[index]; html += '<div'
if (data.isDelete) {
html += ' class="red" ';
}
html += '>'; if (data.sex == 1) {
html += '男';
} else {
html += '女';
} html += ' age: ' + (data.age - 1); html += '</div>';
}
});
这种时候, 就有很多坑了, 经常性的标签没有正确闭合, 而且不输出html的情况下, 无法预测html的结构, 实在坑爹... 这时候, 模版引擎的优势就体现出来了
在《深入理解PHP》一书中, 提到, PHP的模版引擎, 就是把你发明的语言, 通过你实现的编译器, 翻译成PHP语言, 再给PHP执行.
同样的, JS模版引擎也是类似的
把你发明的语言, 通过你实现的编译器, 翻译成HTML, 再插入到DOM中, 就是JS模版引擎的工作的, 实际上, 模版引擎应当只负责翻译的工作, 是否插入到DOM中, 或者是怎么插入, 就由自己选择了
实现模版引擎, 关键以及重点其实就是在于 正则表达式 的运用, 通过正则表达式, 获取模版中的关键字, 通过各种方法去替换, 或循环, 或逻辑判断, 最后组合返回HTML
在文章中, 使用<% %>作为替换的标志, 类似与<?php ?>之间的代码会被php执行一样, 规定<%%>之间的代码, 才会被执行, 因此得出的正则
var reg = /<%([^%>]+)%>/g;
通过该正则循环获取, 并替换
while (match = reg.exec(html)) {
//code
}
为了添加逻辑判断, 在<%%>中, 如果出现if, else, case, break, switch等关键字, 就不输出, 直接当做js执行, 所以添加了另外一句正则
var reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
对正则的编写是整个模版引擎的核心, 其余便是一些字符串替换以及如何巧妙得拼接字符串, 该文章采用了new Function的方式, 再使用一个数组r, 往数组r中push字符串, 再最后join输出, 实在是巧妙
最后的15行代码:
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 != '' && line.replace(/\s/g, "") != '' ? '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);
}
在实际使用过程中, 发现了一些小问题
如果在使用逻辑判断switch中, 模版有换行的时候, new Function中会插入了多个
r.push("");
switch(str) {
r.push("");
case 1:
r.push(""); break:
r.push("");
}
所以 add 方法在 js == false的情况, 应当还要判断line是否为空字符串
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;
} //改为 var add = function(line, js) {
js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
(code += line != '' && line.replace(/\s/g, "") != "" ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
return add;
}
最后结果
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 != '' && line.replace(/\s/g, "") != '' ? '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);
}
实际使用时, 个人喜欢在html中插入一个type="text/tpl"的script作为模版标签
<script type="text/tpl" id="tpl">
<% for (var i = 0, length = this.length; i < length; i++) { %>
<div <% if (this[i].isDeleted) { %>class="red"<% } %>>
<% if (this[i].sex == 0) { %>
男
<% } else { %>
女
<% } %>
</div>
</script>
获取该script的innerHTML后, 通过模版引擎编译, 再插入到需要插入的DOM中