Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

60行代码实现简单模板引擎 #9

Open
yinguangyao opened this issue May 2, 2018 · 0 comments
Open

60行代码实现简单模板引擎 #9

yinguangyao opened this issue May 2, 2018 · 0 comments

Comments

@yinguangyao
Copy link
Owner

yinguangyao commented May 2, 2018

不久前看过一篇不错的文章,作者用了15行代码就实现了一个简单的模板引擎,我觉得很有趣,建议在读这篇文章之前先看一下这个,这里是传送门:只有20行的Javascript模板引擎
这个模板引擎实现的核心点是利用正则表达式来匹配到模板语法里面的变量和JS语句,再将这些匹配到的字段push到一个数组中,最后连接起来,用Function来解析字符串,最后将执行后的结果放到指定DOM节点的innerHTML里面。
但是这个模板引擎还是有很多不足,比如不支持取余运算,不支持自定义模板语法,也不支持if、for、switch之外的JS语句,缺少HTML实体编码。
恰好我这阵子也在看underscore源码,于是就参考了一下underscore中template方法的实现。
这个是我参考template后实现的模板,一共只有60行代码。

(function () {
    var root = this;
    // 将字符串中的HTML实体字符转义,可以有效减少xss风险
    var html2Entity = (function () {
        var escapeMap = {
            '&': '&',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#x27;',
            '`': '&#x60;'
        };
        var escaper = function (match) {
            return escapeMap[match];
        };
        return function (string) {
            var source = "(" + Object.keys(escapeMap).join("|") + ")";
            var regexp = RegExp(source), regexpAll = RegExp(source, "g");
            return regexp.test(string) ? string.replace(regexpAll, escaper) : string;
        }
    }())
    // 字符串中的转义字符
    var escapes = {
        '"': '"',
        "'": "'",
        "\\": "\\",
        '\n': 'n',
        '\r': 'r',
        '\u2028': 'u2028',
        '\u2029': 'u2029'
    }
    var escaper = /\\|'|"|\r|\n|\u2028|\u2029/g;
    var convertEscapes = function (match) {
        return "\\" + escapes[match];
    }
    var template = function (tpl, settings) {
        // 可以在外部修改template.templateSettings来自定义语法
        // 一定要保证evaluate在最后,不然会匹配到<%=%>和<%-%>
        var templateSettings = Object.assign({}, {
            interpolate: /<%=([\s\S]+?)%>/g,
            escape: /<%-([\s\S]+?)%>/g,
            evaluate: /<%([\s\S]+?)%>/g,
        }, template.templateSettings);
        settings = Object.assign({}, settings);
        // /<%=([\s\S]+?)%>|<%-([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
        // 其中$是为了匹配字符串的最后一个字符
        var matcher = RegExp(Object.keys(templateSettings).map(function (key) {
            return templateSettings[key].source
        }).join("|") + "|$", "g")
        var source = "", index = 0;
        // 字符串拼接,要拼接上没有匹配到的字符串和替换匹配到的字符串
        tpl.replace(matcher, function (match, interpolate, escape, evaluate, offset) {
            source += "__p += '" + tpl.slice(index, offset).replace(escaper, convertEscapes) + "'\n";
            index = offset + match.length;
            if (evaluate) {
                source += evaluate + "\n"
            } else if (interpolate) {
                source += "__p += (" + interpolate + ") == null ? '' : " + interpolate + ";\n"
            } else if (escape) {
                source += "__p += (" + escape + ") == null ? '' : " + html2Entity(escape) + ";\n"
            }
            return match;
        })
        source = "var __p = '';" + source + 'return __p;'
        // 使用with可以修改作用域
        if (!settings.variable) source = "with(obj||{}) {\n" + source + "\n}"
        var render = new Function(settings.variable || "obj", source);
        return render
    }
    // 将templateY导出到全局
    root.templateY = template
}.call(this))

转义

我们知道,在字符串中有一些特殊字符是需要转义的,比如"'", '"',不然就会和预期展示不一致,甚至是报错,所以我们一般会用反斜杠来表示转义,常见的转义字符有\n, \t, \r等等。
但是这里的convertEscapes里面我们为什么要多加一个反斜杠呢?
这是因为在执行new Function里面的语句时,也需要对字符进行一次转义,可以看一下下面这行代码:

var log = new Function("var a = '1\n23';console.log(a)");
log() // Uncaught SyntaxError: Invalid or unexpected token

这是因为Function函数在执行的时候,里面的内容被解析成了这样。

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

在JS里面是不允许字符串换行出现的,只能使用转义字符\n。

正则表达式

underscore中摒弃了用正则表达式匹配for/if/switch/{/}等语句的做法,而是使用了不同的模板语法(<%=%>和<%%>)来区分当前是变量还是JS语句,这样虽然需要用户自己区分语法,但是给开发者减少了很多不必要的麻烦,因为如果用正则来匹配,那么后面就无法使用类似{##}的语法了。
这里正则表达式的重点是+?,+?是惰性匹配,表示以最少的次数匹配到[\s\S],所以我们/<%=([\s\S]+?)%>/g是不会匹配到类似<%=name<%=age%>%>这种语法的,只会匹配到<%=name%>语法。

replace

这里我们用到了replace第二个参数是函数的情况。

var pattern = /([a-z]+)\s([a-z]+)/;
var str = "hello world";
str.replace(pattern, function(match, p1, p2, offset) {
    // p1 is "hello"
    // p2 is "world"
    return match;
})

在JS正则表达式中,使用()包起来的叫着捕获性分组,而使用(?:)的叫着非捕获性分组,在replace的第二个参数是函数时,每次匹配都会执行一次这个函数,这个函数第一个参数是pattern匹配到的字符串,在这个里面是"hello world"。
p1是第一个分组([a-z]+)匹配到的字符串,p2是第二个分组([a-z]+)匹配到的字符串,如果有更多的分组,那还会有更多参数p3, p4, p5等等,offset是最后一个参数,指的是在第几个索引处匹配到了,这里的offset是0,因为是从一开始就刚好匹配到了hello world。

字符串拼接

underscore中使用+=字符串拼接的方式代替了数组push的方式,这样是因为+=相比push的性能会更高。
我这里进行了一下测试,在新版chrome中,下面这段代码中,push的效率要远远好于+=,但是在v8中结果却是相反。

var arr = [], str = "";
var i = 0, j = 0
console.time();
for(;i<100000;i++) {
  arr.push(i);
}
arr.join("");
console.timeEnd()

console.time();
for(;j<100000;j++) {
  str+= j
}
console.timeEnd()

setting.variable

underscore这里使用with来改变了作用域,但是with会导致性能比较差,关于with的弊端可以参考一下这篇文章: Javascript中的with关键字
你还可以在variable设置里指定一个变量名,这样能显著提升模板的渲染速度。不过语法也和之前有一些不同,模板里面必须要用你指定的变量名来访问,而不能直接用answer这种形式,这种形式下没有使用with实现,所以性能会高很多。

_.template("Using 'with': <%= data.answer %>", {variable: 'data'})({answer: 'no'});

参考链接:

  1. js正则进阶
  2. JavaScript函数replace揭秘
  3. JavaScript正则表达式分组模式:捕获性分组与非捕获性分组及前瞻后顾
  4. underscore 系列之字符实体与 _.escape
  5. Javascript中的with关键字
  6. 高性能JavaScript模板引擎原理解析
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant