由于各种原因,被逼使用前台模板。看了一下其他JS模板库的实现,发现其原理并不难,遂决定重造轮子。
做一个前台模板,有如下几个问题需要考量:
- 模板是放置于哪里?是内嵌于HTML页面还是像JS文件那样独立出来?如果是内嵌可以减少请求数但无法让模板重用于另一个HTML页面,反之亦然。
- 如果是内嵌于HTML页面,如何存放它?目前有两种方式,script标签与textarea。
- 模板界定符的风格,是ASP的<%与%>,还是Django的{{与}},还是其他方式。
下面是我一些不成熟的见解:
- 应该存在两种模板,普通模板(内嵌于HTML)与局部模板(存放于独立的文件中)。普通模板是为某个页面订制的,局部模板可以是订制的,但多是为了实现多页面的重用,它可以与普通模板一起组成完整的模板。用rails的术语来说,这是一个partial。
- 普通模板的容器为一个丧失解析脚本能力的script标签,因为我们可以不需要在script标签里面内嵌script标签。但是与textara作为容器,就很可能碰到模板存在textarea的情况,这时就存在一个错位套嵌的问题。
- 模板风格,让它可以定制就行了。默认是ASP风格,好让它在一些主流的IDE中自动排版。
我把我的模板引擎称之为ejs(embedded javascript snippet,嵌入式javascript代码片断)。任何javascript模板只最终目的就是生成一个可以传入后台参数的函数。
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta content="IE=8" http-equiv="X-UA-Compatible"/>
<meta name="keywords" content="javascript模板 by 司徒正美" />
<meta name="description" content="javascript模板 by 司徒正美" />
<title>javascript模板 by 司徒正美</title>
</head>
<body>
<h1>javascript模板 by 司徒正美</h1>
<script id="tmpl" type="text/html">
<h2><%= name %></h2>
<%# 这是注释!!!!!!!!! %>
<ul>
<% for(var i=0; i< supplies.length; i++){ %>
<li><%= supplies[i] %></li>
<% } %>
</ul>
<% var color = "color:red;" %>
<p style="text-indent:2em;<%= color %>"><%= address %></p>
</script>
<script id="tmpl" type="template" ></script >
<script>
window.onload = function(){
dom.ejs({
selector:"tmpl",
json: {
name:"司徒正美",
supplies:["第一个LI元素","第二个LI元素","第三个LI元素","第四个LI元素"],
address:"异次元"}
});
}
</script>
</body>
</html>
模板系统:
//司徒正美 javascript template - http://www.cnblogs.com/rubylouvre/ - MIT Licensed
(function () {
if(!String.prototype.trim){
String.prototype.trim = function(str) {
return this.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
}
}
var dom = {
quote: function (str) {
str = str.replace(/[\x00-\x1f\\]/g, function (chr) {
var special = metaObject[chr];
return special ? special : '\\u' + ('0000' + chr.charCodeAt(0).toString(16)).slice(-4)
});
return '"' + str.replace(/"/g, '\\"') + '"';
}
},
metaObject = {
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'\\': '\\\\'
},
parser = document.createElement("div"),
startOfHTML = "\t__views.push(",
endOfHTML = ");\n",
outerScan = function(str,buff,left,right){
var index = str.indexOf(left);
if(index !== -1){
buff.push(startOfHTML, dom.quote(str.slice(0,index)), endOfHTML);
innerScan(str.slice(index+2),buff,left,right);
}else{
buff.push(startOfHTML, dom.quote(str), endOfHTML);
}
},
innerScan = function(str,buff,left,right){
var index = str.indexOf(right);
if(index !== -1){
var text = str.slice(0,index);
switch (text.charAt(0)) {
case "#"://处理注释
break;
case "="://处理后台返回的变量(输出到页面的)
buff.push(startOfHTML, text.slice(1), endOfHTML)
break;
default:
buff.push(text, "\n")
}
outerScan( str.slice(index+2),buff,left,right);
}else{
throw "找不到右界定符 " + str
}
}
//onsite,可选,Boolean,是否就地替换掉模板容器,默认true,如果为false,则返回一个文档碎片,交由用户自己插入到需要的地方
dom.ejs = function (obj) {
var onsite = obj.onsite === void 0 ,
left = obj.left || "<%",
right = obj.right || "%>",
selector = obj.selector,
buff = ["var __views = [];\n"],
fragment = document.createDocumentFragment(),
el = document.getElementById(selector),
ejs = dom.ejs;
if (!el) throw "找不到目标元素";
if(!ejs[selector]){
outerScan(el.text.trim(),buff,left,right);
ejs[selector] = new Function("json", "with(json){"+buff.join("") + '};return __views.join("");')
}
parser.innerHTML = ejs[selector](obj.json || {});
while (parser.firstChild) {
fragment.appendChild(parser.firstChild)
}
return onsite ? el.parentNode.replaceChild(fragment, el) : fragment;
};
window.dom = dom;
})();
PS:发现javascript模板没有想象中的糟糕,尤其是大流量的页面在无法使用UI库的情况下,这是个不错的选择,例子如qq zone与ニコニコ動画。