资讯专栏INFORMATION COLUMN

Mustache.js源码分析

mating / 1565人阅读

摘要:是一个弱逻辑的模板引擎,语法十分简单,使用很方便。源码只有行,且代码结构清晰。解析器解析器是整个源码中最重要的方法,用于解析模板,将标签与模板标签分离。同时比较后还需将的最后一个删除,才能进行下一轮比较。

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.js5个模块(暂且称它们为:Utils, Scanner, Parser, Writer,Context)间的关系图如下:

代码层面,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 === "
")
                    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的数据结构:

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

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最主要的是一个解析器和一个渲染器,以非常简洁的方式实现了一个强大的模板引擎。

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

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

相关文章

  • js模版引擎介绍

    摘要:使用方法编译模板并根据所给的数据立即渲染出结果仅编译模版暂不渲染,它会返回一个可重用的编译后的函数根据给定的数据,对之前编译好的模板进行数据渲染参考资料模板引擎概述 js模版引擎介绍 JavaScript 模板是将 HTML 结构从包含它们的内容中分离的方法。模板系统通常会引入一些新语法,但通常是非常简单的,一个要注意的有趣的点是,替换标记通常是由双花括号({ {……} })表示,这也...

    duan199226 评论0 收藏0
  • [译] 用 Webpack 武装自己

    摘要:现在来做一个的入口让我们在文件里进行的配置。如果想要显示它们,我们可以在运行的时候使用你还可以使用,在改变代码的时候自动进行打包。新建文件,里面是一段,告诉使用进行预处理。 本文译自:Webpack your bags这篇文章由入门到深入的介绍了webpack的功能和使用技巧,真心值得一看。 由于我英语水平有限,而且很少翻译文章,所以文中的一些语句在翻译时做了类似语义的转换,望谅解。...

    Tychio 评论0 收藏0
  • Mustache学习笔记

    摘要:一个返回值渲染后的例那年那夏我是,年龄结果我是那年那夏,年龄的思想的核心是标签和。从上面的代码中可以看到定义模板时,使用了这样的标记,这就是的标签,只不过它用替代了,以免跟标签的混淆。 Mustache学习笔记 Mustache 是一款基于javascript 实现的模板引擎,类似于 Microsoft’s jQuery template plugin,但更简单易用,在前后端分离的技术...

    qylost 评论0 收藏0
  • mustache:web模板引擎

    摘要:简介是一个轻逻辑模板解析引擎,它的优势在于可以应用在等多种编程语言中。这里主要是看中的应用。 mustache简介 Mustache 是一个 logic-less (轻逻辑)模板解析引擎,它的优势在于可以应用在 Javascript、PHP、Python、Perl 等多种编程语言中。这里主要是看JavaScript中的应用。Javascript 语言的模板引擎,目前流行有 Mustac...

    klivitamJ 评论0 收藏0

发表评论

0条评论

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