资讯专栏INFORMATION COLUMN

underscore 系列之实现一个模板引擎(上)

LeexMuller / 600人阅读

摘要:第一版我们来尝试实现第一版第一版为了验证是否有用文件文件完整的可以查看示例一在这里我们使用了,实际上在文章中使用的是构造函数。构造函数创建一个新的对象。

前言

underscore 提供了模板引擎的功能,举个例子:

var tpl = "hello: <%= name %>";

var compiled = _.template(tpl);
compiled({name: "Kevin"}); // "hello: Kevin"

感觉好像没有什么强大的地方,再来举个例子:

在 HTML 文件中:

    JavaScript 文件中:

    var container = document.getElementById("user_tmpl");
    
    var data = {
        users: [
            { "name": "Kevin", "url": "http://localhost" },
            { "name": "Daisy", "url": "http://localhost" },
            { "name": "Kelly", "url": "http://localhost" }
        ]
    }
    var precompile = _.template(document.getElementById("user_tmpl").innerHTML);
    var html = precompile(data);
    
    container.innerHTML = html;

    效果为:

    那么该如何实现这样一个 _.template 函数呢?

    实现思路

    underscore 的 template 函数参考了 jQuery 的作者 John Resig 在 2008 年发表的一篇文章 JavaScript Micro-Templating,我们先从这篇文章的思路出发,思考一下如何写一个简单的模板引擎。

    依然是以这段模板字符串为例:

    <%for ( var i = 0; i < users.length; i++ ) { %>
        
  • <%=users[i].name%>
  • <% } %>

    John Resig 的思路是将这段代码转换为这样一段程序:

    // 模拟数据
    var users = [{"name": "Kevin", "url": "http://localhost"}];
    
    var p = [];
    for (var i = 0; i < users.length; i++) {
        p.push("
  • "); p.push(users[i].name); p.push("
  • "); } // 最后 join 一下就可以得到最终拼接好的模板字符串 console.log(p.join("")) //
  • Kevin
  • 我们注意,模板其实是一段字符串,我们怎么根据一段字符串生成一段代码呢?很容易就想到用 eval,那我们就先用 eval 吧。

    然后我们会发现,为了转换成这样一段代码,我们需要将<%xxx%>转换为 xxx,其实就是去掉包裹的符号,还要将 <%=xxx%>转化成 p.push(xxx),这些都可以用正则实现,但是我们还需要写 p.push("

  • p.push("">");呐,这些该如何实现呢?

    那我们换个思路,依然是用正则,但是我们

    %> 替换成 p.push("

    <% 替换成 ");

    <%=xxx%> 替换成 ");p.push(xxx);p.push("

    我们来举个例子:

    <%for ( var i = 0; i < users.length; i++ ) { %>
        
  • <%=users[i].name%>
  • <% } %>

    按照这个替换规则会被替换为:

    ");for ( var i = 0; i < users.length; i++ ) { p.push("
        
  • ");p.push(users[i].name);p.push("
  • "); } p.push("

    这样肯定会报错,毕竟代码都没有写全,我们在首和尾加上部分代码,变成:

    // 添加的首部代码
    var p = []; p.push("
    
    ");for ( var i = 0; i < users.length; i++ ) { p.push("
        
  • ");p.push(users[i].name);p.push("
  • "); } p.push(" // 添加的尾部代码 ");

    我们整理下这段代码:

    var p = []; p.push("");
    for ( var i = 0; i < users.length; i++ ) { 
        p.push("
  • "); p.push(users[i].name); p.push("
  • "); } p.push("");

    恰好可以实现这个功能,不过还要注意一点,要将换行符替换成空格,防止解析成代码的时候报错,不过在这里为了方便理解原理,就只在代码里实现。

    第一版

    我们来尝试实现第一版:

    // 第一版
    function tmpl(str, data) {
        var str = document.getElementById(str).innerHTML;
    
        var string = "var p = []; p.push("" +
        str
        .replace(/[
    	
    ]/g, "")
        .replace(/<%=(.*?)%>/g, "");p.push($1);p.push("")
        .replace(/<%/g, "");")
        .replace(/%>/g,"p.push("")
        + "");"
    
        eval(string)
    
        return p.join("");
    };

    为了验证是否有用:

    HTML 文件:

    JavaScript 文件:

    var users = [
        { "name": "Byron", "url": "http://localhost" },
        { "name": "Casper", "url": "http://localhost" },
        { "name": "Frank", "url": "http://localhost" }
    ]
    tmpl("user_tmpl", users)

    完整的 Demo 可以查看 template 示例一

    Function

    在这里我们使用了 eval ,实际上 John Resig 在文章中使用的是 Function 构造函数。

    Function 构造函数创建一个新的 Function 对象。 在 JavaScript 中, 每个函数实际上都是一个 Function 对象。

    使用方法为:

    new Function ([arg1[, arg2[, ...argN]],] functionBody)

    arg1, arg2, ... argN 表示函数用到的参数,functionBody 表示一个含有包括函数定义的 JavaScript 语句的字符串。

    举个例子:

    var adder = new Function("a", "b", "return a + b");
    
    adder(2, 6); // 8

    那么 John Resig 到底是如何实现的呢?

    第二版

    使用 Function 构造函数:

    // 第二版
    function tmpl(str, data) {
        var str = document.getElementById(str).innerHTML;
    
        var fn = new Function("obj",
    
        "var p = []; p.push("" +
    
        str
        .replace(/[
    	
    ]/g, "")
        .replace(/<%=(.*?)%>/g, "");p.push($1);p.push("")
        .replace(/<%/g, "");")
        .replace(/%>/g,"p.push("")
        + "");return p.join("");");
    
        return fn(data);
    };

    使用方法依然跟第一版相同,具体 Demo 可以查看 template 示例二

    不过值得注意的是:其实 tmpl 函数没有必要传入 data 参数,也没有必要在最后 return 的时候,传入 data 参数,即使你把这两个参数都去掉,代码还是可以正常执行的。

    这是因为:

    使用Function构造器生成的函数,并不会在创建它们的上下文中创建闭包;它们一般在全局作用域中被创建。当运行这些函数的时候,它们只能访问自己的本地变量和全局变量,不能访问Function构造器被调用生成的上下文的作用域。这和使用带有函数表达式代码的 eval 不同。

    这里之所以依然传入了 data 参数,是为了下一版做准备。

    with

    现在有一个小问题,就是实际上我们传入的数据结构可能比较复杂,比如:

    var data = {
        status: 200,
        name: "kevin",
        friends: [...]
    }

    如果我们将这个数据结构传入 tmpl 函数中,在模板字符串中,如果要用到某个数据,总是需要使用 data.namedata.friends 的形式来获取,麻烦就麻烦在我想直接使用 name、friends 等变量,而不是繁琐的使用 data. 来获取。

    这又该如何实现的呢?答案是 with。

    with 语句可以扩展一个语句的作用域链(scope chain)。当需要多次访问一个对象的时候,可以使用 with 做简化。比如:

    var hostName = location.hostname;
    var url = location.href;
    
    // 使用 with
    with(location){
        var hostname = hostname;
        var url = href;
    }
    function Person(){
        this.name = "Kevin";
        this.age = "18";
    }
    
    var person = new Person();
    
    with(person) {
        console.log("my name is " + name + ", age is " + age + ".")
    }
    // my name is Kevin, age is 18.

    最后:不建议使用 with 语句,因为它可能是混淆错误和兼容性问题的根源,除此之外,也会造成性能低下

    第三版

    使用 with ,我们再写一版代码:

    // 第三版
    function tmpl(str, data) {
        var str = document.getElementById(str).innerHTML;
    
        var fn = new Function("obj",
    
        // 其实就是这里多添加了一句 with(obj){...}
        "var p = []; with(obj){p.push("" +
    
        str
        .replace(/[
    	
    ]/g, "")
        .replace(/<%=(.*?)%>/g, "");p.push($1);p.push("")
        .replace(/<%/g, "");")
        .replace(/%>/g,"p.push("")
        + "");}return p.join("");");
    
        return fn(data);
    };

    具体 Demo 可以查看 template 示例三

    第四版

    如果我们的模板不变,数据却发生了变化,如果使用我们的之前写的 tmpl 函数,每次都会 new Function,这其实是没有必要的,如果我们能在使用 tmpl 的时候,返回一个函数,然后使用该函数,传入不同的数据,只根据数据不同渲染不同的 html 字符串,就可以避免这种无谓的损失。

    // 第四版
    function tmpl(str, data) {
        var str = document.getElementById(str).innerHTML;
    
        var fn = new Function("obj",
    
        "var p = []; with(obj){p.push("" +
    
        str
        .replace(/[
    	
    ]/g, "")
        .replace(/<%=(.*?)%>/g, "");p.push($1);p.push("")
        .replace(/<%/g, "");")
        .replace(/%>/g,"p.push("")
        + "");}return p.join("");");
    
        var template = function(data) {
            return fn.call(this, data)
        }
        return template;
    };
    
    // 使用时
    var compiled = tmpl("user_tmpl");
    results.innerHTML = compiled(data);

    具体 Demo 可以查看 template 示例四

    下期预告

    至此,我们已经跟着 jQuery 的作者 John Resig 实现了一个简单的模板引擎,虽然 underscore 基于这个思路实现,但是功能强大,相对的,代码也更加复杂一下,下一篇,我们一起去分析 underscore 的 template 函数实现。

    underscore 系列

    underscore 系列目录地址:https://github.com/mqyqingfeng/Blog。

    underscore 系列预计写八篇左右,重点介绍 underscore 中的代码架构、链式调用、内部函数、模板引擎等内容,旨在帮助大家阅读源码,以及写出自己的 undercore。

    如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

  • 文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/92271.html

    相关文章

    • underscore 的源码该如何阅读?

      摘要:所以它与其他系列的文章并不冲突,完全可以在阅读完这个系列后,再跟着其他系列的文章接着学习。如何阅读我在写系列的时候,被问的最多的问题就是该怎么阅读源码我想简单聊一下自己的思路。感谢大家的阅读和支持,我是冴羽,下个系列再见啦 前言 别名:《underscore 系列 8 篇正式完结!》 介绍 underscore 系列是我写的第三个系列,前两个系列分别是 JavaScript 深入系列、...

      weknow619 评论0 收藏0
    • underscore 系列实现一个模板引擎(下)

      摘要:前言本篇接着上篇系列之实现一个模板引擎上。字符串中的每个字符均可由一个转义序列表示。在中,有四个字符被认为是行终结符,其他的折行字符都会被视为空白。 前言 本篇接着上篇 underscore 系列之实现一个模板引擎(上)。 鉴于本篇涉及的知识点太多,我们先来介绍下会用到的知识点。 反斜杠的作用 var txt = We are the so-called Vikings from th...

      gyl_coder 评论0 收藏0
    • 浅谈 Web 中前后端模板引擎的使用

      摘要:前端模板的出现使得前后端分离成为可能。总结本文简单介绍了模板引擎在前后端的使用,下文我们回到,重点分析下的使用方式以及源码原理。楼主对于模板引擎的认识比较浅显,有不正之处希望指出感谢 前言 这篇文章本来不打算写的,实话说楼主对前端模板的认识还处在非常初级的阶段,但是为了整个 源码解读系列 的完整性,在深入 Underscore _.template 方法源码后,觉得还是有必要记下此文,...

      chenjiang3 评论0 收藏0
    • underscore 系列内部函数 restArgs

      摘要:与最后,使用我们的写的函数重写下函数系列系列目录地址。系列预计写八篇左右,重点介绍中的代码架构链式调用内部函数模板引擎等内容,旨在帮助大家阅读源码,以及写出自己的。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。 partial 在《 JavaScript 专题之偏函数》中,我们写了一个 partial 函数,用来固定函数的部分参数,实现代码如下: // 这是文章中的第一版 fu...

      zzzmh 评论0 收藏0
    • underscore 系列字符实体与 _.escape

      摘要:前言提供了函数,用于转义字符串,替换和字符为字符实体。如果希望正确地显示预留字符,我们必须在源代码中使用字符实体。字符实体有两种形式。转义我们的应对方式就是将取得的值中的特殊字符转为字符实体。 前言 underscore 提供了 _.escape 函数,用于转义 HTML 字符串,替换 &, , , , 和 ` 字符为字符实体。 _.escape(Curly, Larry & Moe)...

      only_do 评论0 收藏0

    发表评论

    0条评论

    LeexMuller

    |高级讲师

    TA的文章

    阅读更多
    最新活动
    阅读需要支付1元查看
    <