资讯专栏INFORMATION COLUMN

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

gyl_coder / 2875人阅读

摘要:前言本篇接着上篇系列之实现一个模板引擎上。字符串中的每个字符均可由一个转义序列表示。在中,有四个字符被认为是行终结符,其他的折行字符都会被视为空白。

前言

本篇接着上篇 underscore 系列之实现一个模板引擎(上)。

鉴于本篇涉及的知识点太多,我们先来介绍下会用到的知识点。

反斜杠的作用
var txt = "We are the so-called "Vikings" from the north."
console.log(txt);

我们的本意是想打印带 "" 包裹的 Vikings 字符串,但是在 JavaScript 中,字符串使用单引号或者双引号来表示起始或者结束,这段代码会报 Unexpected identifier 错误。

如果我们就是想要在字符串中使用单引号或者双引号呢?

我们可以使用反斜杠用来在文本字符串中插入省略号、换行符、引号和其他特殊字符:

var txt = "We are the so-called "Vikings" from the north."
console.log(txt);

现在 JavaScript 就可以输出正确的文本字符串了。

这种由反斜杠后接字母或数字组合构成的字符组合就叫做“转义序列”。

值得注意的是,转义序列会被视为单个字符。

我们常见的转义序列还有 表示换行、 表示制表符、 表示回车等等。

转义序列

在 JavaScript 中,字符串值是一个由零或多个 Unicode 字符(字母、数字和其他字符)组成的序列。

字符串中的每个字符均可由一个转义序列表示。比如字母 a,也可以用转义序列 u0061 表示。

转义序列以反斜杠  开头,它的作用是告知 JavaScript 解释器下一个字符是特殊字符。 

转义序列的语法为 uhhhh,其中 hhhh 是四位十六进制数。

根据这个规则,我们可以算出常见字符的转义序列,以字母 m 为例:

// 1. 求出字符 `m` 对应的 unicode 值
var unicode = "m".charCodeAt(0) // 109
// 2. 转成十六进制
var result = unicode.toString(16); // "6d"

我们就可以使用 u006d 表示 m,不信你可以直接在浏览器命令行中直接输入字符串 "u006d",看下打印结果。

值得注意的是: 虽然也是一种转义序列,但是也可以使用上面的方式:

var unicode = "
".charCodeAt(0) // 10
var result = unicode.toString(16); // "a"

所以我们可以用 u000A 来表示换行符 ,比如在浏览器命令行中直接输入 "a b""a u000A b" 效果是一样的。

讲了这么多,我们来看看一些常用字符的转义序列以及含义:

Unicode 字符值 转义序列 含义
u0009 t 制表符
u000A n 换行
u000D r 回车
u0022 " 双引号
u0027 " 单引号
u005C 反斜杠
u2028 行分隔符
u2029 段落分隔符
Line Terminators

Line Terminators,中文译文行终结符。像空白字符一样,行终结符可用于改善源文本的可读性。

在 ES5 中,有四个字符被认为是行终结符,其他的折行字符都会被视为空白。

这四个字符如下所示:

字符编码值 名称
u000A 换行符
u000D 回车符
u2028 行分隔符
u2029 段落分隔符
Function

试想我们写这样一段代码,能否正确运行:

var log = new Function("var a = "1	23";console.log(a)");
log()

答案是可以,那下面这段呢:

var log = new Function("var a = "1
23";console.log(a)");
log()

答案是不可以,会报错 Uncaught SyntaxError: Invalid or unexpected token

这是为什么呢?

这是因为在 Function 构造函数的实现中,首先会将函数体代码字符串进行一次 ToString 操作,这时候字符串变成了:

var a = "1
23";console.log(a)

然后再检测代码字符串是否符合代码规范,在 JavaScript 中,字符串表达式中是不允许换行的,这就导致了报错。

为了避免这个问题,我们需要将代码修改为:

var log = new Function("var a = "1
23";console.log(a)");
log()

其实不止 ,其他三种 行终结符,如果你在字符串表达式中直接使用,都会导致报错!

之所以讲这个问题,是因为在模板引擎的实现中,就是使用了 Function 构造函数,如果我们在模板字符串中使用了 行终结符,便有可能会出现一样的错误,所以我们必须要对这四种 行终结符 进行特殊的处理。

特殊字符

除了这四种 行终结符 之外,我们还要对两个字符进行处理。

一个是

比如说我们的模板内容中使用了:

var log = new Function("var a = "123";console.log(a)");
log(); // 1

其实我们是想打印 "123",但是因为把 当成了特殊字符的标记进行处理,所以最终打印了 1。

同样的道理,如果我们在使用模板引擎的时候,使用了 字符串,也会导致错误的处理。

第二个是 "

如果我们在模板引擎中使用了 ",因为我们会拼接诸如 p.push(" ") 等字符串,因为 " 的原因,字符串会被错误拼接,也会导致错误。

所以总共我们需要对六种字符进行特殊处理,处理的方式,就是正则匹配出这些特殊字符,然后比如将 替换成 替换成 " 替换成 ",处理的代码为:

var escapes = {
    """: """,
    "": "",
    "
": "r",
    "
": "n",
    "u2028": "u2028",
    "u2029": "u2029"
};

var escapeRegExp = /|"|
|
|u2028|u2029/g;

var escapeChar = function(match) {
    return "" + escapes[match];
};

我们测试一下:

var str = "console.log("I am 
 Kevin");";
var newStr = str.replace(escapeRegExp, escapeChar);

eval(newStr)
// I am 
// Kevin
replace

我们来讲一讲字符串的 replace 函数:

语法为:

str.replace(regexp|substr, newSubStr|function)

replace 的第一个参数,可以传一个字符串,也可以传一个正则表达式。

第二个参数,可以传一个新字符串,也可以传一个函数。

我们重点看下传入函数的情况,简单举一个例子:

var str = "hello world";
var newStr = str.replace("world", function(match){
    return match + "!"
})
console.log(newStr); // hello world!

match 表示匹配到的字符串,但函数的参数其实不止有 match,我们看个更复杂的例子:

function replacer(match, p1, p2, p3, offset, string) {
    // match,表示匹配的子串 abc12345#$*%
    // p1,第 1 个括号匹配的字符串 abc
    // p2,第 2 个括号匹配的字符串 12345
    // p3,第 3 个括号匹配的字符串 #$*%
    // offset,匹配到的子字符串在原字符串中的偏移量 0
    // string,被匹配的原字符串 abc12345#$*%
    return [p1, p2, p3].join(" - ");
}
var newString = "abc12345#$*%".replace(/([^d]*)(d*)([^w]*)/, replacer); // abc - 12345 - #$*%

另外要注意的是,如果第一个参数是正则表达式,并且其为全局匹配模式, 那么这个方法将被多次调用,每次匹配都会被调用。

举个例子,如果我们要在一段字符串中匹配出 <%=xxx%> 中的值:

var str = "
  • <%=baidu%>
  • " str.replace(/<%=(.+?)%>/g, function(match, p1, offset, string){ console.log(match); console.log(p1); console.log(offset); console.log(string); })

    传入的函数会被执行两次,第一次的打印结果为:

    <%=www.baidu.com%>
    www.baidu.com
    13
    
  • <%=baidu%>
  • 第二次的打印结果为:

    <%=baidu%>
    "baidu"
    33
    
  • <%=baidu%>
  • 正则表达式的创建

    当我们要建立一个正则表达式的时候,我们可以直接创建:

    var reg = /ab+c/i;

    也可以使用构造函数的方式:

    new RegExp("ab+c", "i");

    值得一提的是:每个正则表达式对象都有一个 source 属性,返回当前正则表达式对象的模式文本的字符串:

    var regex = /fooBar/ig;
    console.log(regex.source); // "fooBar",不包含 /.../ 和 "ig"。
    正则表达式的特殊字符

    正则表达式中有一些特殊字符,比如 d 就表示了匹配一个数字,等价于 [0-9]。

    在上节,我们使用 /<%=(.+?)%>/g 来匹配 <%=xxx%>,然而在 underscore 的实现中,用的却是 /<%=([sS]+?)%>/g

    我们知道 s 表示匹配一个空白符,包括空格、制表符、换页符、换行符和其他 Unicode 空格,S
    匹配一个非空白符,[sS]就表示匹配所有的内容,可是为什么我们不直接使用 . 呢?

    我们可能以为 . 匹配任意单个字符,实际上,并不是如此, .匹配除行终结符之外的任何单个字符,不信我们做个试验:

    var str = "<%=hello world%>"
    
    str.replace(/<%=(.+?)%>/g, function(match){
        console.log(match); // <%=hello world%>
    })

    但是如果我们在 hello world 之间加上一个行终结符,比如说 "u2029":

    var str = "<%=hello u2029 world%>"
    
    str.replace(/<%=(.+?)%>/g, function(match){
        console.log(match);
    })

    因为匹配不到,所以也不会执行 console.log 函数。

    但是改成 /<%=([sS]+?)%>/g 就可以正常匹配:

    var str = "<%=hello u2029 world%>"
    
    str.replace(/<%=([sS]+?)%>/g, function(match){
        console.log(match); // <%=hello 
 world%>
    })
    惰性匹配

    仔细看 /<%=([sS]+?)%>/g 这个正则表达式,我们知道 x+ 表示匹配 x 1 次或多次。x?表示匹配 x 0 次或 1 次,但是 +? 是个什么鬼?

    实际上,如果在数量词 *、+、? 或 {}, 任意一个后面紧跟该符号(?),会使数量词变为非贪婪( non-greedy) ,即匹配次数最小化。反之,默认情况下,是贪婪的(greedy),即匹配次数最大化。

    举个例子:

    console.log("aaabc".replace(/a+/g, "d")); // dbc
    
    console.log("aaabc".replace(/a+?/g, "d")); // ffffdbc

    在这里我们应该使用非惰性匹配,举个例子:

    var str = "
  • <%=baidu%>
  • " str.replace(/<%=(.+?)%>/g, function(match){ console.log(match); }) // <%=www.baidu.com%> // <%=baidu%>

    如果我们使用惰性匹配:

    var str = "
  • <%=baidu%>
  • " str.replace(/<%=(.+)%>/g, function(match){ console.log(match); }) // <%=www.baidu.com%>"><%=baidu%>
    template

    讲完需要的知识点,我们开始讲 underscore 模板引擎的实现。

    与我们上篇使用数组的 push ,最后再 join 的方法不同,underscore 使用的是字符串拼接的方式。

    比如下面这样一段模板字符串:

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

    我们先将 <%=xxx%> 替换成 "+ xxx +",再将 <%xxx%> 替换成 "; xxx __p+=":

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

    这段代码肯定会运行错误的,所以我们再添加些头尾代码,然后组成一个完整的代码字符串:

    var __p="";
    with(obj){
    __p+="
    
    ";for ( var i = 0; i < users.length; i++ ) { __p+="
        
  • "+ users[i].name +"
  • "; } __p+=" "; }; return __p;

    整理下代码就是:

    var __p="";
    with(obj){
        __p+="";
        for ( var i = 0; i < users.length; i++ ) { 
            __p+="
  • "+ users[i].name +"
  • "; } __p+=""; }; return __p

    然后我们将 __p 这段代码字符串传入 Function 构造函数中:

    var render = new Function(data, __p)

    我们执行这个 render 函数,传入需要的 data 数据,就可以返回一段 HTML 字符串:

    render(data)
    第五版 - 特殊字符的处理

    我们接着上篇的第四版进行书写,不过加入对特殊字符的转义以及使用字符串拼接的方式:

    // 第五版
    var settings = {
        // 求值
        evaluate: /<%([sS]+?)%>/g,
        // 插入
        interpolate: /<%=([sS]+?)%>/g,
    };
    
    var escapes = {
        """: """,
        "": "",
        "
    ": "r",
        "
    ": "n",
        "u2028": "u2028",
        "u2029": "u2029"
    };
    
    var escapeRegExp = /|"|
    |
    |u2028|u2029/g;
    
    var template = function(text) {
    
        var source = "var __p="";
    ";
        source = source + "with(obj){
    "
        source = source + "__p+="";
    
        var main = text
        .replace(escapeRegExp, function(match) {
            return "" + escapes[match];
        })
        .replace(settings.interpolate, function(match, interpolate){
            return ""+
    " + interpolate + "+
    ""
        })
        .replace(settings.evaluate, function(match, evaluate){
            return "";
     " + evaluate + "
    __p+=""
        })
    
        source = source + main + "";
     }; 
     return __p;";
    
        console.log(source)
    
        var render = new Function("obj",  source);
    
        return render;
    };

    完整的使用代码可以参考 template 示例五。

    第六版 - 特殊值的处理

    不过有一点需要注意的是:

    如果数据中 users[i].url 不存在怎么办?此时取值的结果为 undefined,我们知道:

    "1" + undefined // "1undefined"

    就相当于拼接了 undefined 字符串,这肯定不是我们想要的。我们可以在代码中加入一点判断:

    .replace(settings.interpolate, function(match, interpolate){
        return ""+
    " + (interpolate == null ? "" : interpolate) + "+
    ""
    })

    但是吧,我就是不喜欢写两遍 interpolate …… 嗯?那就这样吧:

    var source = "var __t, __p="";
    ";
    
    ...
    
    .replace(settings.interpolate, function(match, interpolate){
        return ""+
    ((__t=(" + interpolate + "))==null?"":__t)+
    ""
    })

    其实就相当于:

    var __t;
    
    var result = (__t = interpolate) == null ? "" : __t;

    完整的使用代码可以参考 template 示例六。

    第七版

    现在我们使用的方式是将模板字符串进行多次替换,然而在 underscore 的实现中,只进行了一次替换,我们来看看 underscore 是怎么实现的:

    var template = function(text) {
        var matcher = RegExp([
            (settings.interpolate).source,
            (settings.evaluate).source
        ].join("|") + "|$", "g");
    
        var index = 0;
        var source = "__p+="";
    
        text.replace(matcher, function(match, interpolate, evaluate, offset) {
            source += text.slice(index, offset).replace(escapeRegExp, function(match) {
                return "" + escapes[match];
            });
    
            index = offset + match.length;
    
            if (interpolate) {
                source += ""+
    ((__t=(" + interpolate + "))==null?"":__t)+
    "";
            } else if (evaluate) {
                source += "";
    " + evaluate + "
    __p+="";
            }
    
            return match;
        });
    
        source += "";
    ";
    
        source = "with(obj||{}){
    " + source + "}
    "
    
        source = "var __t, __p="";" +
            source + "return __p;
    ";
    
        var render = new Function("obj", source);
    
        return render;
    };

    其实原理也很简单,就是在执行多次匹配函数的时候,不断复制字符串,处理字符串,拼接字符串,最后拼接首尾代码,得到最终的代码字符串。

    不过值得一提的是:在这段代码里,matcher 的表达式最后为:/<%=([sS]+?)%>|<%([sS]+?)%>|$/g

    问题是为什么还要加个 |$ 呢?我们来看下 $:

    var str = "abc";
    str.replace(/$/g, function(match, offset){
        console.log(typeof match) // 空字符串
        console.log(offset) // 3
        return match
    })

    我们之所以匹配 $,是为了获取最后一个字符串的位置,这样当我们 text.slice(index, offset)的时候,就可以截取到最后一个字符。

    完整的使用代码可以参考 template 示例七。

    最终版

    其实代码写到这里,就已经跟 underscore 的实现很接近了,只是 underscore 加入了更多细节的处理,比如:

    对数据的转义功能

    可传入配置项

    对错误的处理

    添加 source 属性,以方便查看代码字符串

    添加了方便调试的 print 函数

    ...

    但是这些内容都还算简单,就不一版一版写了,最后的版本在 template 示例八,如果对其中有疑问,欢迎留言讨论。

    underscore 系列

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

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

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

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

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

    相关文章

    • underscore 的源码该如何阅读?

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

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

      摘要:第一版我们来尝试实现第一版第一版为了验证是否有用文件文件完整的可以查看示例一在这里我们使用了,实际上在文章中使用的是构造函数。构造函数创建一个新的对象。 前言 underscore 提供了模板引擎的功能,举个例子: var tpl = hello: ; var compiled = _.template(tpl); compiled({name: Kevin}); // hello:...

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

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

      chenjiang3 评论0 收藏0
    • underscore 系列防冲突与 Utility Functions

      摘要:你可以轻松为你的函数库添加防冲突功能。系列系列目录地址。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。 防冲突 underscore 使用 _ 作为函数的挂载对象,如果页面中已经存在了 _ 对象,underscore 就会覆盖该对象,举个例子: var _ = {value: 1 } // 引入 underscore 后 console.log(_.value); // un...

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

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

      zzzmh 评论0 收藏0

    发表评论

    0条评论

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