如何用javascript实现一个模板引擎

模板引擎简介

模板引擎是html渲染必不可少的工具,前端开发的同窗经历了从最原始的字符串拼接、用数组push/join,发展到封装简单的string.format函数,再到功能更增强大的模板引擎,能够在模板中内嵌处理逻辑。javascript

传统的页面开发语言技术asp.net,php,jsp都内置了模板引擎,javascrip常见的模板引擎有jquery的tmpl插件,underscore的template函数,ejs、jade、artTemplate等,以ejs为例,模板语法以下:php

<% if (user) { %>
    <h2><%= user.name %></h2>
<% } %>
复制代码

模板中能够直接访问绑定的数据源变量,而且能够支持任意的javascript代码片断,如if,for循环等css

原理

咱们从需求出发,想想把上面的模板字符串中的变量替换成数据源中的变量值,并支持嵌入的if等逻辑,须要怎样实现?能支持内嵌js代码执行,那确定是要使用eval来编译的,只要把模板当作两部分,一部分是在百分号之内的,使用eval执行;另外一部分是在百分号之外的,原样输出; 最后顺序拼接成一个完整的字符串就能够了。html

正则表达式分割字符串

首先要把模板,按照%分隔进行拆分红多个代码片断,并按顺序存入到数组中 分割模板字符串能够用String对象的split方法,不少人不知道split方法是能够支持正则表达式的,由于咱们的分隔符有两个<%和%>,因此要用正则表达式,能够同时匹配这两个字符,代码就只有一行:前端

var arr=str.split(/(\<%=?|%\>)/gm);
复制代码

拆分后的代码片断生成一个数组,咱们遍历一下这个数组,将代码片断分红3种类型java

  1. javascript代码
  2. html片断(原样输出的字符)
  3. 用<%=%>输出的变量值

只要按顺序拼接生成的字符串代码,最终编译执行字符串代码就能够了node

编译字符串的方法

1. eval

js最多见的编译字符串的方法固然是eval,能够直接在当前函数做用域将一个字符串编译成可执行代码,有一条代码规范叫作don't be eval,这是由于使用eval是用代码生成代码,会让代码更加难以看懂,而且会有一些性能损耗。jquery

2. new Function

与eval功能类似,不一样的是他会将传入的字符串代码编辑后生成一个function,并且能够生成函数的参数,更加方便调用,封装性隔离性更好。调用形式:new Function(args,functionBody),为了更好的可读性,咱们选择new Functiongit

3. setTimeout/setInterval

setTimeout/setInterval 的第一个参数,能够传入一个函数,这是最常规的用法,也能够传入一个字符串,使用字符串的时候自动调用eval执行,这个用法不太常见。github

做用域问题

模板中确定须要一些动态变量,就是所谓的数据源,模板编译后是生成独立函数的,做用域是隔离的,而数据源是在业务代码中,有独立的做用域,不能直接访问,那么怎么能把两个不一样的做用域链接起来呢,with,只有with能作到,将指定的对象插入到当前做用域链的顶端。

完整代码实现

function render(tpl,data){
    tpl=tpl.replace(/(\r|\n)/ig,"");
    var arr=tpl.split(/(\<%=?|%\>)/gm);//拆分模板
    var funcBody=["with(this){\r\nvar result=[];"];
    var item,codeType;
    codeType=0;
    for(var i=0;i<arr.length;i++){
        item=arr[i];
        //将代码片断分为3类
        if(item=="<%"){
           codeType=1;
           continue;
        }else if(item=="<%="){
           codeType=2;
           continue;
        }else if(item=="%>"){
            codeType=0;
            continue;
        } 
        //为3类代码片断生成最终可被eval的函数体
        if(codeType==0){ //字符
            funcBody.push("result.push(\"");
            funcBody.push(item);
            funcBody.push("\");\r\n");
        }else if(codeType==1){ //代码
            funcBody.push(item);
            funcBody.push("\r\n");
        }else if (codeType==2){ //代码输出
            funcBody.push("result.push(");
            funcBody.push(item);
            funcBody.push(");\r\n");
        }
    }
    funcBody.push("return result.join('')\r\n}");
    var template_func=new Function(["renderData"],funcBody.join(""));
    return template_func.apply(data,[data]);
 }
复制代码

测试一下

var html=render("<% list.forEach(function (item,idx){ %>"
+" <div><%=idx+1+'、'+item%></div>"
+" <%})%>",
{list:["javascript","css","node.js"]})
document.write(html)
复制代码

完美运行

咱们来看一下最终编译后生成的函数是什么样子的,以下图:

因为生成的函数是用apply调用的,template_inner.apply(data,[data]),因此函数内部的this指向传入的数据源(data变量),所以能够在模板中直接使用传入的数据源对象

模板引擎与ES6模板字符串对比

ES6新增了模板字符串功能,不一样于普通字符串的单引号和双引号,模板字符用`符号定义,在模板字符中直接能够经过${变量名}访问当前做用域内的变量并直接输出该变量的值,而且在js文件中定义大段的html字符串时,通常是把html片断粘帖进来,包含不少换行符,而模板字符串能够直接兼容换行符,使用起来很是方便。上文中的模板,用ES6的模板字符串来实现,代码也很是精简,以下:

var user={name:"windy"};
var str=`<h2>${user.name}</h2>`
复制代码

ES6模板字符串与普通的模板引擎相比,最大区别在于开发流程的不一样,业务逻辑是在js中实现的,模板只实现纯净的变量替换功能,代码与逻辑分离,比较规范易用,可维护也较好,而普通的模板引擎不只实现了变量替换,还能够内嵌js逻辑代码,更加灵活和强大。

最后,推荐一下我的的开源项目,内置了模板引擎的node.js web开发框架,webcontext:

github.com/windyfancy/…

相关文章
相关标签/搜索