JS相关

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

2017-01-23  本文已影响169人  ronniegong

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

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

简单版模板方法

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

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

假如用 <% 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

对基础知识的考察

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

进一步完善模板引擎

上述 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()方式动态创建函数对象

过程如下

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

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

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;
    }

对进阶知识的考察

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

参考文章

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

上一篇 下一篇

猜你喜欢

热点阅读