Mustache.js前端模板引擎源码解读

页面导航:首页 > 网络编程 > JavaScript > Mustache.js前端模板引擎源码解读

Mustache.js前端模板引擎源码解读

来源: 作者: 时间:2016-02-04 09:15 【

mustache是一个很轻的前端模板引擎,因为之前接手的项目用了这个模板引擎,自己就也继续用了一会觉得还不错,最近项目相对没那么忙,于是就抽了点时间看了一下这个的源码。源码很
mustache是一个很轻的前端模板引擎,因为之前接手的项目用了这个模板引擎,自己就也继续用了一会觉得还不错,最近项目相对没那么忙,于是就抽了点时间看了一下这个的。源码很少,也就只有六百多行,所以比较容易。做前端的话,还是要多看优秀源码,这个模板引擎的知名度还算挺高,所以其源码也肯定有值得一读的地方。
 
  本人前端小菜,写这篇博文纯属自己记录一下以便做备忘,同时也想分享一下,希望对园友有帮助。若解读中有不当之处,还望指出。
 
  如果没用过这个模板引擎,建议 去 https://github.com/janl/mustache.js/ 试着用一下,上手很容易。
 
  摘取部分官方demo代码(当然还有其他基本的list遍历输出): 
 
 
数据:
{
  "name": {
    "first": "Michael",
    "last": "Jackson"
  },
  "age": "RIP"
}
 
模板写法:
* {{name.first}} {{name.last}}
* {{age}}
 
渲染效果:
* Michael Jackson
* RIP
 
  OK,那就开始来解读它的源码吧:
 
  首先先看下源码中的前面多行代码:
 
 
var Object_toString = Object.prototype.toString;
    var isArray = Array.isArray || function (object) {
            return Object_toString.call(object) === '[object Array]';
        };
 
    function isFunction(object) {
        return typeof object === 'function';
    }
 
    function escapeRegExp(string) {
        return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
    }
 
    // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
    // See https://github.com/janl/mustache.js/issues/189
    var RegExp_test = RegExp.prototype.test;
    function testRegExp(re, string) {
        return RegExp_test.call(re, string);
    }
 
    var nonSpaceRe = /\S/;
    function isWhitespace(string) {
        return !testRegExp(nonSpaceRe, string);
    }
 
    var entityMap = {
        "&": "&",
        "<": "&lt;",
        ">": "&gt;",
        '"': '&quot;',
        "'": '&#39;',
        "/": '&#x2F;'
    };
 
    function escapeHtml(string) {
        return String(string).replace(/[&<>"'\/]/g, function (s) {
            return entityMap[s];
        });
    }
 
    var whiteRe = /\s*/;    //匹配0个或以上空格
    var spaceRe = /\s+/;    //匹配一个或以上空格
    var equalsRe = /\s*=/;  //匹配0个或者以上空格再加等于号
    var curlyRe = /\s*\}/;  //匹配0个或者以上空格再加}符号
 
  这些都比较简单,都是一些为后面主函数准备的工具函数,包括
 
  · toString和test函数的简易封装
 
  · 判断对象类型的方法
 
  · 字符过滤正则表达式关键符号的方法
 
  · 判断字符为空的方法
 
  · 转义字符映射表 和 通过映射表将html转码成非html的方法
 
  · 一些简单的正则。
 
  一般来说mustache在js中的使用方法都是如下:
 
var template = $('#template').html();
  Mustache.parse(template);   // optional, speeds up future uses
  var rendered = Mustache.render(template, {name: "Luke"});
  $('#target').html(rendered);
   所以,我们接下来就看下parse的实现代码,我们在源码里搜索parse,于是找到这一段
 
mustache.parse = function (template, tags) {
        return defaultWriter.parse(template, tags);
    };
  再通过找defaultWriter的原型Writer类后,很容易就可以找到该方法的核心所在,就是parseTemplate方法,这是一个解析器,不过在看这个方法之前,还得先看一个类:Scanner,顾名思义,就是扫描器,源码如下
 
 
/**
     * 简单的字符串扫描器,用于扫描获取模板中的模板标签
     */
    function Scanner(string) {
        this.string = string;   //模板总字符串
        this.tail = string;     //模板剩余待扫描字符串
        this.pos = 0;   //扫描索引,即表示当前扫描到第几个字符串
    }
 
    /**
     * 如果模板被扫描完则返回true,否则返回false
     */
    Scanner.prototype.eos = function () {
        return this.tail === "";
    };
 
    /**
     * 扫描的下一批的字符串是否匹配re正则,如果不匹配或者match的index不为0;
     * 即例如:在"abc{{"中扫描{{结果能获取到匹配,但是index为4,所以返回"";如果在"{{abc"中扫描{{能获取到匹配,此时index为0,即返回{{,同时更新扫描索引
     */
    Scanner.prototype.scan = function (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 (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;
    };
 
  扫描器,就是用来扫描字符串,在mustache用于扫描模板代码中的模板标签。扫描器中就三个方法:
 
  eos:判断当前扫描剩余字符串是否为空,也就是用于判断是否扫描完了
 
  scan:仅扫描当前扫描索引的下一堆匹配正则的字符串,同时更新扫描索引,注释里我也举了个例子
 
  scanUntil:扫描到匹配正则为止,同时更新扫描索引
 
  看完扫描器,我们再回归一下,去看一下解析器parseTemplate方法,模板的标记标签默认为"{{}}",虽然也可以自己改成其他,不过为了统一,所以下文解读的时候都默认为{{}}:
 
 
    function parseTemplate(template, tags) {
        if (!template)
            return [];
 
        var sections = [];     // 用于临时保存解析后的模板标签对象
        var tokens = [];       // 保存所有解析后的对象
        var spaces = [];       // 保存空格对象在tokens里的索引
        var hasTag = false;    
        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(tags) {
            if (typeof tags === 'string')
                tags = tags.split(spaceRe, 2);
 
            if (!isArray(tags) || tags.length !== 2)
                throw new Error('Invalid tags: ' + tags);
 
            openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
            closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
            closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
        }
 
        compileTags(tags || mustache.tags);
 
        var scanner = new Scanner(template);
 
        var start, type, value, chr, token, openSection;
        while (!scanner.eos()) {
            start = scanner.pos;
 
            // Match any text between tags.
            // 开始扫描模板,扫描至{{时停止扫描,并且将此前扫描过的字符保存为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}}与其对应
 
                // 检查模板标签是否闭合,{{#}}是否与{{
Tags:

文章评论


<