由 JavaScript 模板引擎想到的

旧文一篇,2014年发布于前端乱炖中,是当时学习的整理,现在拿过来备份凑个篇数;

  当下有很多优秀的JavaScript模板引擎,相信各位或多或少对它都有了解。

  因为之前并不知道这东西的存在,最近才在了解一些模板引擎,所以对一些模板引擎的原理以及实现的理解并不是很深。当我简单看过几个模板后,发现一个”秘密”:模板的语法以及用法和ASP.NET中的一些语法及控件用法有惊人的相似 – 其实和PHP语法、JavaWeb中某些模板也是相似的吧,个人对PHP以及JavaWeb不是很熟悉,就拿ASP.NET来说了。

  具体有哪些相似等下再说,下面先说说最近在项目中遇到的相关问题以及解决方式。

模板问题与解决方案

  1、项目中很多地方用ajax从后端取数据,将这些数据显示到界面上,最传统的方式就是拼接字符串,这个是没什么问题的。(注:这里只讨论列表显示;下面的代码为了叙述方便,一些DOM操作就用JQuery来写了)。

示例代码1:  

//假设通过ajax从后台请求来的数据是这样的
var list = [{"name":"super"},{"name":"sun"}];
var result = [];
for(var i=0,len=list.length; i<len; i++){
  result.push("<li><a href=’javascript:;’ >"+list[i].name+"</a>");
}
$("#ul_list").html("").html(result.join(""));

2、因为产品经理的原因,这些列表中的项目改动频繁,之前拼接字符串的方式简直弱爆了,改起来麻烦死了;之前扩展过一个JS方法,是模仿C#中的string.format()方法,相关代码如下:

//summary: 格式化字符串方法
//params: args……
//return: 格式化之后的字符串
String.prototype.format = function(){
  var pattern = /\{(\d+)\}/gm;
  var args = arguments;
  var format = this.replace(pattern, function(){
    return args[arguments[1]];
    //arguments[0]-匹配项; arguments[1]-第一个捕获组,如果有多个捕获组,则应该对应多个参数,从1开始; 之后一项(倒数第二项)为匹配的位置; 最后一项为原始字符串
  });
  return format;
}

用法就很简单,需要替换的参数,用 ”{0}” 这种占位符代替,示例代码1可以修改为以下结构,

示例代码2:

var list = [{"name":"super"},{"name":"sun"}];
var result = [];
var template = "<li><a href='javascript:;'>{0}</a><li>";
for(var i=0,len=list.length; i<len; i++){
  result.push(template.format(list[i].name));
}
$("#ul_list").html("").html(result.join(""));

3、示例代码2比代码1已经好太多了,能够一眼看清数据需要填入的结构。现在需求又变了!先从HTML标签中改出最新版,然后将需要动态修改的结构复制到JS代码中,重新改参数。为什么不直接用HTML标签中改好的结构?

示例代码3:

HTML代码:

<body>
    ……
    <!-- 列表 -->
    <div class="employee_list">
            <ul id="ul_id">
            </ul>
    </div>
    <!-- /列表 -->
    ……
    <!-- 模板 -->
    <ul id="employee_template" style="display:none;">
        <li><a href="javascript:;"> {0} </a><span class="age">{1}</span></li>
        <!-- 0:姓名 --><!-- 1:年龄 -->
    </ul>
    <!-- /模板 -->
</body>

JavaScript 代码: 

var list = [{"name":"super","age":"23"},{"name":"sun","age":"24"}];
var result = [];
var template = $("#employee_template").html();
for(var i=0,len=list.length; i<len; i++){
  var item = list[i];
  result.push(template.format(item.name, item.age));
}
$("#ul_list").html("").html(result.join(""));

  代码3比代码2更直观了吧,在页面上修改完成后,只需要把动态展示的标签拿到一个隐藏的模板中,改成占位符;JS代码这边只需要一个参数顺序的文档就可以了。

  之前的问题貌似是解决了,但随之而来的是新的问题

Q1:如果需要动态请求图片的路径,那模板中就要有如下代码 <img alt='{0}' src='{1}' />,虽然模板是隐藏的,但在浏览器中F12查看结果窗口,会发现有错误,原因就是这个隐藏的img标签向服务器请求了路径为”{1}”的图片。

  像这种情况怎么解决,img 标签的 src 属性应该是浏览器的默认行为,如何阻止 src 行为呢?牺牲一次请求来实现这个功能是否值得呢?还请大家给出自己的建议。

Q2:为了解决Q1,是否可以将上面的模板放在注释中,然后把注释的内容当做模板呢?

代码如下:

<body>
    ……
    <!-- 模板 0:提示文字,1:路径 -->
    <div id="template" style="display:none;">
        <!-- <img alt="{0}" src="{1}"> -->
    </div>
    <!-- /模板 -->
    ……
</body>

这种方式虽然解决了结果窗口报错的问题,但貌似又带来了新的问题,取注释内容是有兼容性问题的,取上面HTML代码模板中的注释:

window.onload = function(){
  var template = document.getElementById("template").firstChild;
  alert("name="+template.nodeName+"\n type="+template.nodeType+
      "\n"+template.nodeValue);
}

IE8及其以下版本的浏览器中,可以取到注释内容 <img alt="{0}" src="{1}"> ,但是在IE9及其以上、Chrome和FF中,取到的是 “name=#text type=3″,这是因为<div id="template" style="display:none;">之后的换行符和注释前边的空格/Tab被识别为Text类型的节点。

  通过兼容的方式还是能够取到注释内容的(或者通过for循环,挨个节点判断类型也可以取到注释内容)。个人感觉这种方式不太靠谱,取注释内容,听起来就不那么高大上。还请大家分析下这种方式的可行性。

Q3:目前我在项目中使用的还是牺牲请求的方式。如果用当下流行的JavaScript模板引擎,应该能够解决Q1中的问题吧,因为模板代码是写在<script>标签中的,当添加到<body>标签中时,数据的填充已经完成。我的理解对么?

Q4:如果只是这一个简单的需求,用模板引擎的性能(暂且就把引擎文件加载到页面中的时间和模板内容的解析时间都算作是性能)会不会比上面的方式低?毕竟模板引擎怎么也得1K多吧。希望大家能解答我的疑问。

ASP.NET 与 JavaScript模板引擎

  ASP.NET的三种开发模式:Web Pages、MVC和Web Forms,个人比较擅长的还是Web Forms,事件驱动模型开发(不擅长MVC是因为做的项目少,并且没有遇到比较有经验的Web开发人员–不要说我要求高、光找客观因素,工作这两年多…哎,说多了都是泪啊)。

  很多JavaScript 模板引擎语法都和 ASP.NET 中的 Razor 语法、Web Forms语法类似(还有JavaWeb中的Velocity模板语法、PHP的原生语法)类似。

  个人觉得ASP.NET、PHP就是在前台代码中嵌入后台代码,只是数据还好些,要是嵌套了判断什么的,感觉破坏了整个页面的结构,我不喜欢这种方式。

Razor(在<body>标签中):

<ul>
    @for (int i = 0; i < 5; i++) {
        <li>@i</li>
    }
</ul>

Web Forms(在<body>标签中):

<ul>
    <% for (int i = 0; i < 5; i++) { %>
        <li><% =i %></li>
    <% } %>
</ul>

artTemplate 模板引擎代码(在<script>标签中)

<ul>
    <%for(i = 0; i < list.length; i ++) {%>
        <li><%=i%></li>
    <%}%>
</ul>

上面四段代码语法基本是一模一样,前面两段代码是由服务器解析的,后面两段JS模板引擎代码是由JS模板引擎解析的。

  依个人愚见,往简单了说,JS模板引擎就是通过各种正则来匹配模板中的固定语法进而转义出HTML代码模板,最后填充数据生成HTML代码添加到页面中。往复杂了说,虽然想到的不是很多,但总觉得实现这个功能要考虑的东西很多,需要花费大量的精力。

  artTemplate 模板引擎代码中,<% %> 恰好是ASP.NET下识别后台动态代码的标识,这意味着在aspx页面中,不能够使用该模板引擎,为了解决该冲突,可以把该模板引擎的代码拿到.js脚本文件中。

  为什么不把JS模板代码写到<body>标签中?

  我觉得原因有两点:

  1、可能会和ASP.NET、PHP的后端代码有冲突;

  2、应该和Q1中的问题有关吧。

  大家有什么其他的见解可以发出来讨论哈。

扩展

  想到一种思路来实现在<body>标签中插入JS模板,”灵感”来源于ASP.NET中的一个服务器控件-Repeater,先看一段 Repeater 的使用代码:

<asp:Repeater ID="Repeater2" runat="server">
    <HeaderTemplate><!-- 头部模板,不参与循环 -->
        <ul>
    </HeaderTemplate>
    <ItemTemplate><!-- 循环的模板 -->
        <li>
            <a href="#"><%#Eval("Title")%></a> 
        </li>
    </ItemTemplate>
    <AlternatingItemTemplate><!-- 循环的间隔模板 -->
        <li style="background-color:#eee;">
            <a href="#"><%#Eval("Title")%></a> 
        </li>
    </AlternatingItemTemplate>
    <FooterTemplate><!-- 尾部模板,不参与循环 -->
        </ul>
    </FooterTemplate>
</asp:Repeater>

后台绑定数据代码:

List<NewsData> list = new NewsDA().SelectAll();
Repeater2.DataSource  = list;
Repeater2.DataBind();

自定义JS模板 – HTML代码:

<div id="real_list">
  <div id="template1">
    <superTemplate>
      <superHead>
        <ul>
      </superHead>
      <superItem>
        <li>
          <a href="#">{name} {age}岁</a>
        </li>
        </supeItem>
        <superAlterItem>
          <li style="background-color:#eee;">
            <a href="#">{name} {age}岁</a>
          </li>
        </superAlterItem>
        <superFoot>
          <ul>
        </superFoot>
    </superTemplate>
  </div>
</div>

知道我想要干什么了吗?通过自定义标签来设置模板,然后在JS中取到内容,用正则进行各种操作各种拼接,最后替换模板即可。

  在IE8及其以下,是不能直接通过 id、tagName 来取自定义标签的,所以外边套一层HTML能够识别的 div 标签,通过 div 标签来取模板的内容就兼容了。在生成 HTML 代码后,再将外部套用的 div 移除掉(template1)。上面的代码中,real_list 是页面中的内容。

自定义JS模板 – JS代码:

var template = document.getElementById("template1").innerHTML; //IE7及其以上都是可以的,IE6没有测

还没完,可以接着往下扩展,Repeater 的 DataSource 属性和 DataBind() 方法都是可以借鉴的,在模板引擎中也封装一个DataSource属性和DataBind()方法。大体用法应该如下:

//通过ajax从后台取来的数据
var list = [{"name":"super", "age":"23"}, {"name":"sun", "age":"24"}];

//方式1:模仿Repeater
var template = new SuperTempate(); 
template.htmlId = "template1";
template.dataSource = list;
template.dataBind();

//方式2:采用更简洁的方式
var template = new SuperTemplate("template1"); //构造传值
template.bind(list);            //直接绑定

上面都是在围绕JSON数组展开讨论,如果有一个基本类型数组呢?var names = ["super", "sun"];, 其实解决起来还是很简单的,在绑定<a href="#">{name} {age}岁</a> 的模板中可以加入对数字下标的实现<a href="#">姓名:{0}</a>,支持字符串和下标两种。

  分析到这儿,这种思路下的模板引擎该怎么写,差不多就一目了然了吧。Repeater控件还有ItemCreated() 、ItemDataBind() 以及 ItemDataBind() 事件,都可以借鉴过来。

  这种模板引擎的优缺点分析:

  优点:能够直观的在标签中看到结构,对于使用支持HTML标签变色编辑器的开发人员来说,绝对要比在<script></script>标签中写代码要更方便直观。

  缺点:在模板未调用之前,代码已经加载到了页面中,在请求时间上影响应该是不大。只是模板名称有风险,要是以后HTML标签新增了<superHead></superHead>这样的标签并且有自己的意义,那之前使用该模板的代码就挂了。

  这东西只是在假设阶段,我就已经分析得这么happy了,(⊙o⊙)!

  上面已经提到了,这只是一个思路,如果大家有更好的思路或者发现了我这个思路实施起来可能会存在的问题,都可以提出来讨论。

填坑

  突然就想到了上面那个Q1问题的解决方案:<img _src="{0}" alt="{1}" />。对,就是用自定义属性,先把占位符替换成数据,然后再把自定义属性换成标准属性。

结尾

  在写这篇文章之前,本来没有那么多东西要写;一边写一边整理思路,期间找了不少资料,结果就产生了很多想法;希望大家也多多抽出时间来整理,收获还是很多的。

  我也是很久没整理了,最近一直在忙公司项目,项目年前要上线!!!加班加点赶赶项目回家过个好年啊,在这儿先预祝大家新年快乐了,哈哈。

如果这篇文章对你有用,可以点击下面的按钮告诉我

0

发表回复