Mustache.js源码分析

栏目: 编程语言 · 发布时间: 5年前

内容简介:一般来说,通过使用

mustache.js 是一个弱逻辑的模板引擎,语法十分简单,使用很方便。源码( v2.2.1 )只有 600+ 行,且代码结构清晰。

一般来说, mustache.js 使用方法如下:

var template = 'Hello, {{name}}';
var rendered = Mustache.render(template, {
    name: 'World'
});
document.getElementById('container').innerHTML = rendered;

通过使用 Chrome 对上述 Mustache.renderdebug ,我们顺藤摸瓜梳理了 mustache.js 5个模块(暂且称它们为: Utils , Scanner , Parser , Writer , Context )间的关系图如下:

Mustache.js源码分析

代码层面, Mustache.render() 方法是 mustache.js 向外暴露的方法之一,

mustache.render = function render(template, view, partials) {
    // 容错处理
    if (typeof template !== 'string') {
        throw new TypeError('Invalid template! Template should be a "string" ' +
            'but "' + typeStr(template) + '" was given as the first ' +
            'argument for mustache#render(template, view, partials)');
    }
    // 调用Writer.render
    return defaultWriter.render(template, view, partials);
};

在其内部,它首先调用了 Writer.render() 方法,

Writer.prototype.render = function render(template, view, partials) {
    // 调用Writer构造器的parse方法
    var tokens = this.parse(template);
    // 渲染逻辑,后文会分析
    var context = (view instanceof Context) ? view : new Context(view);
    return this.renderTokens(tokens, context, partials, template);
};

Writer.render() 方法首先调用了 Writer.parse() 方法,

Writer.prototype.parse = function parse(template, tags) {
    var cache = this.cache;
    var tokens = cache[template];
    if (tokens == null)
        // 调用parseTemplate方法
        tokens = cache[template] = parseTemplate(template, tags);
    return tokens;
};

Writer.parse() 方法调用了 parseTemplate 方法,

所以,归根结底, Mustache.render() 方法首先调用 parseTemplate 方法对 html 字符串进行解析,

然后,将一个对象渲染到解析出来的模板中去。

所以,我们得研究源码核心所在—— parseTemplate 方法。在此之前,我们的先看一些前置方法:工具方法和扫描器。

工具方法( Utils

// 判断某个值是否为数组
var objectToString = Object.prototype.toString;
var isArray = Array.isArray || function isArrayPolyfill(object) {
    return objectToString.call(object) === '[object Array]';
};
// 判断某个值是否为函数
function isFunction(object) {
    return typeof object === 'function';
}
// 更精确的返回数组类型的typeof值为'array',而非默认的'object'
function typeStr(obj) {
    return isArray(obj) ? 'array' : typeof obj;
}
// 转义正则表达式里的特殊字符
function escapeRegExp(string) {
    return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
}
// 判断对象是否有某属性
function hasProperty(obj, propName) {
    return obj != null && typeof obj === 'object' && (propName in obj);
}
// 正则验证,防止 Linux 和Windows下不同spidermonkey版本导致的bug
var regExpTest = RegExp.prototype.test;

function testRegExp(re, string) {
    return regExpTest.call(re, string);
}
// 是否是空格
var nonSpaceRe = /\S/;

function isWhitespace(string) {
    return !testRegExp(nonSpaceRe, string);
}
// 将特殊字符转为转义字符
var entityMap = {
    '&': '&',
    '<': '<',
    '>': '>',
    '"': '"',
    "'": ' ',
    '/': '/',
    '`': '`',
    '=': '='
};

function escapeHtml(string) {
    return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) {
        return entityMap[s];
    });
}
var whiteRe = /\s*/; // 匹配0个以上的空格
var spaceRe = /\s+/; // 匹配1个以上的空格
var equalsRe = /\s*=/; // 匹配0个以上的空格加等号
var curlyRe = /\s*\}/; // 匹配0个以上的空格加}
var tagRe = /#|\^|\/|>|\{|&|=|!/; // 匹配#,^,/,>,{,&,=,!

扫描器( Scanner

// Scanner构造器,用于扫描模板
function Scanner(string) {
    this.string = string; // 模板总字符串
    this.tail = string; // 模板剩余待扫描字符串
    this.pos = 0; // 扫描索引,即表示当前扫描到第几个字符串
}
// 如果模板扫描完成,返回true
Scanner.prototype.eos = function eos() {
    return this.tail === '';
};
// 扫描的下一批的字符串是否匹配re正则,如果不匹配或者match的index不为0
Scanner.prototype.scan = function scan(re) {
    var match = this.tail.match(re);
    if (!match || match.index !== 0)
        return '';
    var string = match[0];
    this.tail = this.tail.substring(string.length);
    this.pos += string.length;
    return string;
};
// 扫描到符合re正则匹配的字符串为止,将匹配之前的字符串返回,扫描索引设为扫描到的位置
Scanner.prototype.scanUntil = function scanUntil(re) {
    var index = this.tail.search(re),
        match;
    switch (index) {
        case -1:
            match = this.tail;
            this.tail = '';
            break;
        case 0:
            match = '';
            break;
        default:
            match = this.tail.substring(0, index);
            this.tail = this.tail.substring(index);
    }
    this.pos += match.length;
    return match;
};

总的来说,扫描器,就是用来扫描字符串的。扫描器中只有三个方法:

eos
scan
scanUntil

现在进入 parseTemplate 方法。

解析器( Parser

解析器是整个源码中最重要的方法,用于解析模板,将 html 标签与模板标签分离。

整个解析原理为:遍历字符串,通过正则以及扫描器,将普通 html 和模板标签扫描并且分离,并保存为数组 tokens

function parseTemplate(template, tags) {
    if (!template)
        return [];
    var sections = []; // 用于临时保存解析后的模板标签对象
    var tokens = []; // 保存所有解析后的对象
    var spaces = []; // 包括空格对象在tokens里的索引
    var hasTag = false; // 当前行是否有{{tag}}
    var nonSpace = false; // 当前行是否有非空格字符
    // 去除保存在tokens里的空格对象
    function stripSpace() {
        if (hasTag && !nonSpace) {
            while (spaces.length)
                delete tokens[spaces.pop()];
        } else {
            spaces = [];
        }
        hasTag = false;
        nonSpace = false;
    }
    var openingTagRe, closingTagRe, closingCurlyRe;
    // 将tag转换为正则,默认tag为{{和}},所以转成匹配{{的正则,和匹配}}的正则,以及匹配}}}的正则
    // 因为mustache的解析中如果是{{{}}}里的内容则被解析为html代码
    function compileTags(tagsToCompile) {
        if (typeof tagsToCompile === 'string')
            tagsToCompile = tagsToCompile.split(spaceRe, 2);
        if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
            throw new Error('Invalid tags: ' + tagsToCompile);
        openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
        closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
        closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
    }
    compileTags(tags || mustache.tags);
    var scanner = new Scanner(template);
    var start, type, value, chr, token, openSection;
    while (!scanner.eos()) {
        start = scanner.pos;
        // 开始扫描模板,扫描至{{时停止扫描,并且将此前扫描过的字符保存为value
        value = scanner.scanUntil(openingTagRe);
        if (value) {
            // 遍历{{之前的字符
            for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
                chr = value.charAt(i);
                // 如果当前字符为空格,这用spaces数组记录保存至tokens里的索引
                if (isWhitespace(chr)) {
                    spaces.push(tokens.length);
                } else {
                    nonSpace = true;
                }
                tokens.push(['text', chr, start, start + 1]);
                start += 1;
                // 如果遇到换行符,则将前一行的空格清除
                if (chr === '\n')
                    stripSpace();
            }
        }
        // 判断下一个字符串中是否有{{,同时更新扫描索引到{{的后一位
        if (!scanner.scan(openingTagRe))
            break;
        hasTag = true;
        // 扫描标签类型,是{{#}}还是{{=}}或其他
        type = scanner.scan(tagRe) || 'name';
        scanner.scan(whiteRe);
        // 根据标签类型获取标签里的值,同时通过扫描器,刷新扫描索引
        if (type === '=') {
            value = scanner.scanUntil(equalsRe);
            // 使扫描索引更新为\s*=后
            scanner.scan(equalsRe);
            // 使扫描索引更新为}}后,下面同理
            scanner.scanUntil(closingTagRe);
        } else if (type === '{') {
            value = scanner.scanUntil(closingCurlyRe);
            scanner.scan(curlyRe);
            scanner.scanUntil(closingTagRe);
            type = '&';
        } else {
            value = scanner.scanUntil(closingTagRe);
        }
        // 匹配模板闭合标签即}},如果没有匹配到则抛出异常,
        // 同时更新扫描索引至}}后一位,至此时即完成了一个模板标签{{#tag}}的扫描
        if (!scanner.scan(closingTagRe))
            throw new Error('Unclosed tag at ' + scanner.pos);
        // 将模板标签也保存至tokens数组中
        token = [type, value, start, scanner.pos];
        tokens.push(token);
        // 如果type为#或者^,也将tokens保存至sections
        if (type === '#' || type === '^') {
            sections.push(token);
        } else if (type === '/') {
            // 如果type为/则说明当前扫描到的模板标签为{{/tag}},
            // 则判断是否有{{#tag}}与其对应
            openSection = sections.pop();
            // 检查模板标签是否闭合,{{#}}是否与{{/}}对应,即临时保存在sections最后的{{#tag}}
            if (!openSection)
                throw new Error('Unopened section "' + value + '" at ' + start);
            // 是否跟当前扫描到的{{/tag}}的tagName相同
            if (openSection[1] !== value)
                throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
            // 具体原理:扫描第一个tag,sections为[{{#tag}}],
            // 扫描第二个后sections为[{{#tag}}, {{#tag2}}],
            // 以此类推扫描多个开始tag后,sections为[{{#tag}}, {{#tag2}} ... {{#tag}}]
            // 所以接下来如果扫描到{{/tag}}则需跟sections的最后一个相对应才能算标签闭合。
            // 同时比较后还需将sections的最后一个删除,才能进行下一轮比较。
        } else if (type === 'name' || type === '{' || type === '&') {
            // 如果标签类型为name、{或&,不用清空上一行的空格
            nonSpace = true;
        } else if (type === '=') {
            // 编译标签,为下一次循环做准备
            compileTags(value);
        }
    }
    // 确保sections中没有开始标签
    openSection = sections.pop();
    if (openSection)
        throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
    return nestTokens(squashTokens(tokens));
}

我们来看经过解析器解析之后得到的 tokens 的数据结构:

Mustache.js源码分析

每一个子项都类似下面这种结构

Mustache.js源码分析

token[0]token 的类型,可能的值有 #^/&nametext ,分别表示{}时,调用 renderSection 方法

Writer.prototype.renderSection = function renderSection(token, context, partials, originalTemplate) {
    var self = this;
    var buffer = '';
    // 获取{{#xx}}中xx在传进来的对象里的值
    var value = context.lookup(token[1]);

    function subRender(template) {
        return self.render(template, context, partials);
    }
    if (!value) return;
    if (isArray(value)) {
        // 如果为数组,说明要复写html,通过递归,获取数组里的渲染结果
        for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
            buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
        }
    } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
        // 如果value为对象或字符串或数字,则不用循环,根据value进入下一次递归
        buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
    } else if (isFunction(value)) {
        if (typeof originalTemplate !== 'string')
            throw new Error('Cannot use higher-order sections without the original template');
        // 如果value是方法,则执行该方法,并且将返回值保存
        value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
        if (value != null)
            buffer += value;
    } else {
        // 如果不是上面所有情况,直接进入下次递归
        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
    }
    return buffer;
};

当模板标签类型为时,说明要当 value 不存在( nullundefined0'' )或者为空数组的时候才触发渲染。

看看 renderInverted 方法的实现

Writer.prototype.renderInverted = function renderInverted(token, context, partials, originalTemplate) {
    var value = context.lookup(token[1]);
    // 值为null,undefined,0,''或空数组
    // 直接进入下次递归
    if (!value || (isArray(value) && value.length === 0)) {
        return this.renderTokens(token[4], context, partials, originalTemplate);
    }
};

结语

到这为止, mustache.js 的源码解析完了,可以看出来, mustache.js 最主要的是一个解析器和一个渲染器,以非常简洁的方式实现了一个强大的模板引擎。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Art and Science of CSS

The Art and Science of CSS

Jonathan Snooks、Steve Smith、Jina Bolton、Cameron Adams、David Johnson / SitePoint / March 9, 2007 / $39.95

Want to take your CSS designs to the next level? will show you how to create dozens of CSS-based Website components. You'll discover how to: # Format calendars, menus and table of contents usin......一起来看看 《The Art and Science of CSS》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换