美文网首页JS相关
【转向JavaScript系列】深入理解JavaScript模板

【转向JavaScript系列】深入理解JavaScript模板

作者: ronniegong | 来源:发表于2017-01-23 17:43 被阅读169次

很久之前看过几篇模板引擎相关文章,当时没什么感觉。最近参与了一些面试,再看一篇模板引擎文章时,突然觉得其中涉及的很多知识点,是很好的笔试/面试话题,对于基础的考察还是不错的。

整理一篇文章记录下,本文内容参考逐步实现简单的 JS 模板引擎

简单版模板方法

最基础的模板引擎,是定义一个正则表达式,在模板文本中,按照正则表达式匹配的格式,定义占位变量。最后解析正则表达式,找出匹配的变量名,替换成变量的实际值。

模板引擎中,包含三个组成部分

  • template,模板内容
  • pattern,正则表达式
  • data,数据对象

假如用 <% variable %> 来表示变量,则可用正则 /<%\s(\w?)\s*%>/ 来匹配变量。

var name = 'Han Meimei';
var gender = '男';
var tpl = '<% name %>,欢迎来到这里,祝你早日找到<% gender %>盆友!';
var html = tpl.replace(/<%\s*(\w*?)\s*%>/g, function (match, variable) {
    if (variable === 'name') {
        return name;
    }
    else if (variable === 'gender') {
        return gender;
    }
});

//html 的值就为 Han Meimei,欢迎来到这里,祝你早日找到男盆友!。

上述代码实现了一个文本替换。首选定义了 /<%\s(\w?)\s*%>/g 正则表达式来定义变量模式,然后在模板文本中,使用<% name %>和<% gender %>定义了两个变量,在代码的初始部分,分别为两个变量赋值。

上述代码还不能称之为模板引擎,对引擎来讲,数据对象和模板内容应当是通用的,匹配模式是固定的,将上述代码进行封装

function render(tpl, data) {
    return tpl.replace(/<%\s*(\w*?)\s*%>/g, function (match, variable) {
        if (data.hasOwnProperty(variable)) {
            return data[variable];
        }
    });
}

var tpl = '<% name %>,欢迎来到这里,祝你早日找到<% gender %>盆友!';
var data = {
    name: 'Han Meimei',
    gender: '男'
};
render(tpl, data);
// output: Han Meimei,欢迎来到这里,祝你早日找到男盆友!

var tpl = '姓名:<% name %>,年龄:<% age %>,电话:<% phone %>';
var data = {
    name: 'Li Lei',
    age: 28,
    phone: '123456789'
};
render(tpl, data);
// output: 姓名:Li Lei,年龄:28,电话:123456789

对基础知识的考察

上述代码已经实现了一个最简单的模板引擎。在这一过程中,由浅入深,有几个笔试/面试过程中值得考察的点了。

  • 了解模板引擎基本组成,可以写出一个最简单的正则表达式来匹配模板内容中的占位变量。这一条是最基本要求,提到模板引擎如果一脸茫然不知如何分解问题,说明解决问题能力太差或者经验实在是太少。匹配一个固定占位变量模式的正则表达式还是很简单的,如果完全无法写出,说明完全不掌握正则表达式。如果做不到这一步,可以考虑结束笔试/面试了。
  • 掌握string.replace方法的使用,了解string.replace(regPattern,func)的执行过程。string.replace方法是一个常用的方法,如果不了解这一方法发使用,对一个有经验的开发者来说是不应该的。不过我个人认为,这里如在面试中,可以给一些提示,不至于因为这里一票否决。可以完成前述两步,达到初级水平。
  • 有一定的代码组织能力,可以将上述过程封装为一个方法,完成一个最基本的模板引擎方法。这一步需要一定的代码经验,可以完成这一步,稍高于初级水平了。

进一步完善模板引擎

上述 render 函数可以实现变量替换,但当模板中带有逻辑语句便不再适用。字符串的 replace 方法只能替换模板字符串中的变量标识符为实际的变量值,但没法处理控制逻辑。

现在问题演化为,当模板内容中带有逻辑语句时,如何实现模板引擎?

var tpl = 'Hi <%= name %>,你好!<% if (date >= 1 && date < 6) { %>今天是工作日'
    + ',好沮丧啊!<% } else { %>今天是周末,好开心啊!<% } %> 再见!';
var data = {
    name: 'Li Lei',
    date: '3'
};
function render(tpl, data) {
  ???
}

继续分析,相比之前的模板内容,现在的模板包含了固定文本、代表变量的标识文本以及代表处理逻辑的标识文本。

目标是根据输入数据,将模板内容转换为‘Hi Li Lei,你好!今天是工作日,好沮丧啊! 再见!’

在这一过程中,对三种类型文本有如下处理逻辑

  • 当遇到固定文本时,直接输出;
  • 当遇到变量标识时,替换成具体变量值输出;
  • 当遇到代码逻辑标识时,执行代码逻辑

已知可以通过文本替换方式,将代表变量的标识进行替换。现在问题变为,如何执行处理逻辑的文本?

答案是通过new Function()方式动态创建函数对象

过程如下

  • 定义正则表达式 regExp = /<%(=?)\s(.?)\s*%>/g; 变量为<%= variable %>,逻辑语句为<% code %>
  • 利用数组,将解析的文本暂存起来。在匹配过程中,每一个片段作为一个语句,保存为一个数组元素。固定文本,直接输出;变量标识,替换为具体变量后输出;代码逻辑标识,保存至数组待执行。
  • 将数组合并为一个字符串,使用new Function(codeStr)生成函数对象并执行

同时,在这一过程中,有几个优化的点

  • 将模板的编译和模板语句的执行分离开,使得编译模板后生成的函数对象可缓存,而不需要每次执行时重新编译模板
  • 变量替换时,支持对象的属性和数组这类复杂对象。即支持obj.attr和array[index]这类变量替换
  • 对 "、\r、\n 等一些特殊字符进行转义

最终实现一个较为完善的版本

var regExp = /<%(=?)\s*(.*?)\s*%>/g;

    /**
     * Template 构造函数
     *
     * @constructor
     * @param {string} template 模板文本
     */
    function Template(template) {
        if (!(this instanceof Template)) {
            return new Template(template);
        }

        this.template = template || '';
    }

    /**
     * 编译模板
     *
     * @return {Function} 编译后的渲染函数
     */
    Template.prototype.compile = function () {
        var match;
        var cursor = 0;
        var codes = [];
        // 因为构造函数体内容拼字符串是使用`"`,所以模板中输出的双引号需要转义
        // 同样,模板中的`\n、\r`也需要转义,因为正常代码是需要`;`作为一行的,但`\n、\r`会使代码在一行中折断
        var tpl = this.template
            .replace(/\"/g, '\\\"')
            .replace(/\n/g, '\\\n')
            .replace(/\r/g, '\\\r');

        codes.push(getValue.toString() + ';');
        codes.push('var r = "";');
        // 默认情况下,模板中访问数据`data`的属性时,需要`data.xxx`。为方便,使用`with`语法可以使变量访问的
        // 作用域限制在`data`下,所以直接使用`xxx`就可以访问(当然,平常开发中不推荐使用`with`)
        codes.push('with (data || {}) {');
        while (match = regExp.exec(tpl)) {
            // 固定文本
            codes.push('r += "' + tpl.slice(cursor, match.index) + '";');
            // 变量
            if (match[1]) {
                codes.push('r += getValue("' + match[2] + '");');
            }
            // 代码逻辑
            else {
                codes.push(match[2]);
            }
            cursor = match.index + match[0].length;
        }
        codes.push('r += "' + tpl.slice(cursor) + '";');
        codes.push('}');
        codes.push('return r;');

        return new Function('data', codes.join(''));
    };

    /**
     * Template 构造函数
     *
     * @param {Object} data 渲染时用到的数据
     * @return {string} 渲染后的文本
     */
    Template.prototype.render = function (data) {
        if (!this.renderer) {
            this.renderer = this.compile();
        }

        return this.renderer(data);
    };

    /**
     * 获取字段的值,支持对象取值和数组取值
     *
     * - 对象支持如下取值方式:a.b.c 或 a[b][c]
     * - 数组支持如下取值方式:a[0][1]
     *
     * @param {string} name 字段名,可以包含`.`和`[]`
     * @return {Mixed} 字段的值
     */
    function getValue(name) {
        if (!name) {
            return '';
        }
        var fields = name.split(/\.|\[(\d+)\]/);
        // 正则匹配出的结果有包含空值,过滤一下
        for (var i = 0; i < fields.length;) {
            if (!fields[i]) {
                fields.splice(i, 1);
            }
            else {
                i++;
            }
        }
        var value = data;
        for (var i = 0, len = fields.length; i < len; i++) {
            var field = fields[i];
            if (typeof value === 'object') {
                value = value[field];
            }
            else {
                return '';
            }
        }
        // null或undefined值转换成空字符串
        return value == null ? '' : value;
    }

对进阶知识的考察

这一版的模板引擎,对知识的要求高了不少,有以下一些考察的点

  • 掌握通过new Function()方式动态创建函数对象
  • 对正则表达式掌握多一些,可以写出/<%(=?)\s(.?)\s*%>/g 来分别匹配变量和语句
  • 掌握regExp.exec方法,并且了解string.match和regExp.exec方法的差异。掌握上述三点,认为可以达到中级的水平
  • 可以完整写出一个基本可用的实现,可以正确解析上述模板
  • 在给出提示,如几个优化方向的前提下,可以继续优化实现。可以做到这两点的,可以认为能达到中级以上水平。
  • 视过程具体表现,及对其他模板引擎实现有无了解,可以判断是否达到高级水平

参考文章

如果觉得有帮助,可以扫描二维码对我打赏,谢谢

相关文章

网友评论

本文标题:【转向JavaScript系列】深入理解JavaScript模板

本文链接:https://www.haomeiwen.com/subject/jhmabttx.html