之前在阅读《现代JavaScript库开发》一书时,作者之一的自我介绍中有提及template.js库,抱着好奇心于是去看了下,由于笔者入行时间较晚,但是一眼还是看出,这貌似就是当时php那种模板的语法,想着来都来了,那就看看咋写的。
<ul>
<%for(var i = 0; i < list.length; i++) {%>
<li><%:=list[i].name%></li>
<%}%>
</ul>
const data = {
list: [
{name: "yan"},
{name: "haijing"}
]
};
template(tpl,data)
懂事的已经能够猜到,这玩意儿就是传入模板+数据,返回渲染好的html,然后挂载到指定页面容器中(想了想,现在vue、react前端框架貌似也是这种思路,解析模板+数据组装,最后返回html)
之前以为会类似vue类框架一样,依赖某些解析模板的库,根据模板的规则,解析出来那样,但仔细一看,只有区区三百多行,并没有依赖什么额外的库,更加激起了欲望
// 借鉴一下template
;(function(root,factory) {
var template = factory(root)
// 进行一下模块化检测
if(typeof define === 'function' && define.amd) {
// AMD
define('template',function() {
return template
})
}else if(typeof exports === 'object') {
// Node.js
module.exports = template
}else{
var _template = root.template
// 添加一个防止重复安装的函数
root.noConflict = function () {
if(root.template === template) {
root.template = _template
}
return template
}
root.template = template
}
})
(window,function(root) {
...// 核心处理逻辑
});
首先是一个很常规的通用js模块写法(对几种环境的检测,核心处理函数)
通常我们会从主入口下手,好比vue,则是从new Vue()这一步开始,看看它到底干了什么,这里引入插件后,会暴露一个template变量,使用方法是直接调用它,那么我们只需要找到这个函数即可
function template(tpl, data) {
if (typeof tpl !== 'string') {
return '';
}
var fn = compile(tpl);
if (!isObject(data)) {
return fn;
}
return fn(data);
}
可以看到,这个主函数很简单,大体分为两步:
1、编译模板,并且返回一个函数
2、给返回的函数传入渲染数据
由上可以看出,该库核心点就是模板语法处理+数据渲染,最后返回渲染好的html
function compile(tpl, opt) {
...
try {
// 看样子用compiler又包了一层
var Render = compiler(tpl, opt);
} catch(e) {
...
}
// 返回的渲染函数
function render(data) {
...
}
render.toString = function () {
return Render.toString();
};
return render;
}
继续查看compiler,由于篇幅过程,只保留核心点
function compiler(tpl, opt) {
var mainCode = parse(tpl, opt);
var headerCode = '\n' +
' var html = (function (__data__, __modifierMap__) {\n' +
...
var footerCode = '\n' +
...
' return html;\n';
var code = headerCode + mainCode + footerCode;
code = code.replace(/[\r]/g, ' '); // ie 7 8 会报错,不知道为什么
try {
var Render = new Function('__data__', '__modifierMap__', code);
...
return Render;
} catch(e) {
...
}
}
看到这里一下子有点懵逼(额,和预想的不太一样呢?),仔细一看,有几个代码的字符串以及new Function这种,看到new Function 大致就能猜到,应该是动态生成了一个函数执行的思路,但是还是没搞懂为啥是这样
还好在README.md中看到相关链接中有一则只有20行Javascript代码!手把手教你写一个页面模板引擎,于是就去看了下才恍然大悟,看来对于开源库,良好的README.md 还是非常重要的
链接在那儿了,就不啰嗦了,简单介绍下思路
假设,现在你有如下模板和数据
let template = 'Hello, my name is <%name%>. I\'m <%age%> years old.';
let data = { name: '这小子',age: '25' }
常规思路:根据特定标记,把变量给分割出来,然后把数据放进去,完成渲染,这一步大部分分也能完成,但是对于复杂的就不太理想了(再加点 if else 啥的),于是有了如下思路
1、把字符串分割为纯字符串、代码
2、构造一个函数,去执行上面生成的代码
3、最后返回 纯字符串+函数执行后生成的字符串
// 模板字符串
let template = 'Hello, my name is <%this.name%>. I\'m <%this.age%> years old.';
// 构造一个渲染函数
let TemplateEngine = function (tpl,data) {
let re = /<%([^%>]+)?%>/g,
match,
code = 'var r=[];\n',
cursor = 0;
let 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)
cursor = match.index + match[0].length
}
add(tpl.substr(cursor, tpl.length - cursor))
code += 'return r.join("");'
return new Function(code.replace(/[\r\t\n]/g, '')).apply(data)
}
1、利用正则把特定的 纯字符串+带变量的找出来
2、用一个数组来存放,最后转成字符串
3、构造一个函数执行,并且指定执行环境
上述模板解析后的code为如下
var r=[];
r.push("Hello, my name is ");
r.push("this.name");
r.push(". I'm ");
r.push("this.profile.age");
return r.join("");
// 也就是把这些传入 new Function 中并且执行,就会得到我们想要的东西了
到这里你可能会问,那复杂的模板怎么搞呢,什么 for if 啥的,其实不用担心,既然用了new Function,就是用来解决这些问题的,也就是会把js代码转化到函数内部去执行,其他的字符串自己拼接就可以了,其他的细节可以去看原文就不多说了
再回到template.js中,作者处理更为细致些,同时也额外提供一些api,可以自定义注入一些配置项,总之学习一些优秀的库,可以学到更规范更合理的开发规则和思路