一、我理解的模板引擎
一张网页,要经历怎样的过程,才能抵达用户面前?前端开发最主要的作用就是:在最短的时间内,从后端获取到正确的数据,然后通过浏览器渲染,最后将效果展示给用户。
比如有这样一条数据:
let template = 'Hello world, my name is <% name %>, and I\'m <% age %> years old.';
从后端获取的数据如下:
let data = {
name: 'zhaoyiming',
age: 18
};
let res = ztpl(tpl, data);
假如我们现在使用的模板引擎叫做ztpl,那么以上代码运行之后,最后渲染的结果应该是:
// Hello world, my name is zhaoyiming, and I\'m 18 years old.
之前使用PHP开发项目的时候,最常用的模板引擎就是Smarty。这几年随着AngularJs、VueJs、ReactJs等框架的涌现,模板引擎对于前端来说已不再陌生。
说白了,模板引擎就是将指定模板内容(字符串)中的特定标记(子字符串)替换生成最终需要的业务数据,实现界面与数据的分离,提升代码重用度。
二、动手实现一个简易JavaScript模板引擎
1、基本原理:通过正则表达式替换变量
let template = 'Hello world, my name is <% name %>, and I\'m <% age %> years old.';
let data = {
name: 'zhaoyiming',
age: 18
};
let res = template.replace/<%[\s\t\r]+?([^%>]+)?[\s\t\r]+?%>/g, function ($0, $1) {
return data[$1];
});
console.log(res); // Hello world, my name is zhaoyiming, and I'm 18 years old.
但是如果把data改成如下格式:
let data = {
baseInfo: {
name: 'zhaoyiming',
age: 18
},
work: 'FE'
};
上面的方法就不能用了,会提示data[baseInfo.name] is undefined,只能将它转换成JS对象方法来处理:
function fn () {
return 'Hello world, my name is' + data.baseInfo.name + ', and I'm ' + data.baseInfo.age + ' years old';
}
没错,就是拼字符串,以前为了方便都是这么干,伪代码如下:
function strToDom(str) {
var oDiv = document.createElement("div");
oDiv.innerHTML = str;
return oDiv.childNodes[0];
}
function createProductList (data) {
var str = '<div><span>'+ data.name +'</span><span>'+ data.price +'</span></div>';
}
function renderProductList (productList, banners) {
if (!isArray(productList)) return;
var frag = document.createDocumentFragment();
for (var i = 0, len = productList.length; i < len; i += 1) {
var tmpNode = strToDom(createProductList(productList[i]));
frag.appendChild(tmpNode);
}
document.querySelector('#hot-product-list').appendChild(frag);
}
这种拼字符串,同样可以实现需求,但是html结构、css样式、js逻辑都耦合在一块,后期维护不方便,所以使用模板引擎是最好的。
但是有个问题,如果模板中有for、if等语句时,只靠replace是不行的,可以对比拼字符串的方式,代码总共分为两部分:字符串和变量,那模板引擎也是分为两部分:普通字符串代码和JS逻辑代码(for、if、else、break等等)。
2、使用正则区分普通字符串和JS逻辑代码
如下代码:
<% for(var i = 0; i < this.list.length; i++) {
var post = this.list[i]; %>
<tr>
<td><% post.uid %></td>
<td><% post.uname %></td>
</tr>
<% } %>
思路是这样:
(1)定义一个字符串str和数组arr;
(2)从前到后全局匹配模板;
(3)如果匹配到了<% %>,将匹配到的这段字符串中的逻辑代码添加给str,不是逻辑代码,str连接'arr.push(非逻辑代码)'。
(4)循环执行第三步,直到匹配到模板最后一个字符。
(5)返回最终的str。
4、通过构造函数来编译字符串
JavaScript给我们提供的类(构造函数),不仅可以实现面向对象编程,也可以编译传入的字符串参数,如下:
var data = {
name: 'zhaoyiming',
age: 18
};
var fn = new Function ('data', 'var arr = []; for(var prop in data){ arr.push(data[prop]); } return arr.join(" ")');
console.log(fn(data)); // zhaoyiming 18
这样的话,我们可以把第三步中返回的str和从后端获取到的data作为构造函数的参数,这样的话,就可以返回我们想要的数据,然后append到页面中。
根据以上思路,实现一个基本的模板引擎,核心代码如下:
let str = 'var r=[];\n';
const REG_OUT = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g;
function ztpl (id, data) {
const ELE = document.querySelector(id);
if (!ELE) throw new Error('无效dom对象id');
let html = ELE.value;
const REG = /<%([^%>]+)?%>/g;
let idx = 0;
let match = null;
while(match = REG.exec(html)) {
addStr(html.slice(idx, match.index))(match[1], true);
idx = match.index + match[0].length;
}
addStr(html.substr(idx, html.length - idx));
str += 'return r.join("");';
return new Function(str.replace(/[\r\t\n]/g, '')).apply(data);
}
function addStr (fragment, hasTplTab) {
if (hasTplTab) {
str += fragment.match(REG_OUT) ? (fragment + '\n') : ('r.push(' + fragment + ');\n');
} else {
str += fragment !== '' ? ('r.push("' + fragment.replace(/"/g, '\\"') + '");\n') : '';
}
return addStr;
}
已经将所有代码开源到了github:https://github.com/zymseo/ztpl,目前实现了一个简易的模板引擎,不断优化中。。。
最后再发个牢骚:前端每几个月都会有很大的变化,挨个学是真学不过来,理解代码为什么要这样写就行了。
参考资料:
https://github.com/BaiduFE/BaiduTemplate
https://www.awesomes.cn/repo/aui/arttemplate
https://blog.csdn.net/wxqee/article/details/76100732
https://github.com/jojoin/tppl