artTemplate源码

主要实现:通过拼接字符串的方式构建编译函数,辅助函数通过在编译函数字符串体内以var methodName=function(){}方式传入,因此编译函数字符串体内就可以使用methodName方式加以调用;用户数据通过向编译函数传参注入,赋值给$data后,就可以使用$data.value的方式使用;if、each语句预先通过parser方法将其拼接为js形式的if、each语法。

1.构建编译函数

compile.js

/**
 * 编译模板
 * 2012-6-6 @TooBug: define 方法名改为 compile,与 Node Express 保持一致
 * @name    template.compile
 * @param   {String}    模板字符串
 * @param   {Object}    编译选项
 *
 *      - openTag       {String}      // 逻辑语法开始标签 "{{" 或 "<%"
 *      - closeTag      {String}      // 逻辑语法开始标签 "}}" 或 "%>"
 *      - filename      {String}	  // 用于报错时提醒用户模板字符串的模板名,并作为cacheStore的属性存储编译函数
 *      - escape        {Boolean}     // html字符串转义,编码: <%=value%> 不编码:<%=#value%>
 *      - compress      {Boolean}	  // 是否压缩多余空白和注释
 *      - debug         {Boolean}     // 是否开启调试模式编译模板字符串
 *      - cache         {Boolean}     // 是否缓存模板字符串编译结果
 *      - parser        {Function}    // 语法转换插件钩子,"<%"、"%>"间内部值预处理,默认defaults.parser
 *
 * @return  {Function}  渲染方法
 */
// 通过compiler以字符串形式拼接编译函数体,最终转化成函数输出
var compile = template.compile = function (source, options) {
    // 合并默认配置
    options = options || {};
    for (var name in defaults) {
        if (options[name] === undefined) {
            options[name] = defaults[name];
        }
    }

    var filename = options.filename;

    try {
        var Render = compiler(source, options);
    } catch (e) {
        e.filename = filename || 'anonymous';
        e.name = 'Syntax Error';

        return showDebugInfo(e);
    }
    
    // 对编译结果进行一次包装
    function render (data) {
        try {
            return new Render(data, filename) + '';
        } catch (e) {
            // 运行时出错后自动开启调试模式重新编译
            if (!options.debug) {
                options.debug = true;
                return compile(source, options)(data);
            }
            return showDebugInfo(e)();
        }
    }
    
    render.prototype = Render.prototype;
    render.toString = function () {
        return Render.toString();
    };

    if (filename && options.cache) {
    	// 缓存模板字符串解析函数
        cacheStore[filename] = render;
    }

    return render;
};

// 数组迭代
var forEach = utils.$each;

// 静态分析模板变量
var KEYWORDS =
    // 关键字
    'break,case,catch,continue,debugger,default,delete,do,else,false'
    + ',finally,for,function,if,in,instanceof,new,null,return,switch,this'
    + ',throw,true,try,typeof,var,void,while,with'

    // 保留字
    + ',abstract,boolean,byte,char,class,const,double,enum,export,extends'
    + ',final,float,goto,implements,import,int,interface,long,native'
    + ',package,private,protected,public,short,static,super,synchronized'
    + ',throws,transient,volatile'

    // ECMA 5 - use strict
    + ',arguments,let,yield'

    + ',undefined';

// 滤除多行注释、单行注释、单双引号包裹字符串、点号+空格后的字符串
var REMOVE_RE = /\/\*[\w\W]*?\*\/|\/\/[^\n]*\n|\/\/[^\n]*$|"(?:[^"\\]|\\[\w\W])*"|'(?:[^'\\]|\\[\w\W])*'|\s*\.\s*[$\w\.]+/g;
// 滤除变量,如{{if admin}}中的admin
var SPLIT_RE = /[^\w$]+/g;
// 滤除js关键字
var KEYWORDS_RE = new RegExp(["\\b" + KEYWORDS.replace(/,/g, '\\b|\\b') + "\\b"].join('|'), 'g');
// 滤除数字
var NUMBER_RE = /^\d[^,]*|,\d[^,]*/g;
// 滤除起始、结尾的多个逗号
var BOUNDARY_RE = /^,+|,+$/g;
// 以$或,分割
var SPLIT2_RE = /^$|,+/;

// 获取变量
function getVariable (code) {
    return code
    .replace(REMOVE_RE, '')
    .replace(SPLIT_RE, ',')
    .replace(KEYWORDS_RE, '')
    .replace(NUMBER_RE, '')
    .replace(BOUNDARY_RE, '')
    .split(SPLIT2_RE);
};

// 字符串转义
function stringify (code) {
    return "'" + code
    // 单引号与反斜杠转义
    .replace(/('|\\)/g, '\\$1')
    // 换行符转义(windows + linux)
    .replace(/\r/g, '\\r')
    .replace(/\n/g, '\\n') + "'";
}

// 通过模板字符串和options配置,拼接编译函数体,template.compile方法中转化成函数
function compiler (source, options) {
    var debug = options.debug;// 是否开启调试模式编译模板字符串
    var openTag = options.openTag;// 逻辑语法开始标签 "{{"
    var closeTag = options.closeTag;// 逻辑语法闭合标签 "}}"
    var parser = options.parser;// 语法转换插件钩子,默认的钩子为拼接if、each、echo等语句
    var compress = options.compress;// 是否压缩多余空白和注释
    var escape = options.escape;// html字符串转义,编码: <%=value%> 不编码:<%=#value%>
    
    var line = 1;

    // uniq记录定义编译函数体内已定义的方法名或属性名,防止重复定义
    var uniq = {$data:1,$filename:1,$utils:1,$helpers:1,$out:1,$line:1};

    var isNewEngine = ''.trim;// '__proto__' in {}
    var replaces = isNewEngine
    ? ["$out='';", "$out+=", ";", "$out"]
    : ["$out=[];", "$out.push(", ");", "$out.join('')"];

    var concat = isNewEngine
        ? "$out+=text;return $out;"
        : "$out.push(text);";
          
    var print = "function(){"
    +      "var text=''.concat.apply('',arguments);"
    +       concat
    +  "}";

    var include = "function(filename,data){"
    +      "data=data||$data;"
    +      "var text=$utils.$include(filename,data,$filename);"
    +       concat
    +   "}";

    var headerCode = "'use strict';"
    + "var $utils=this,$helpers=$utils.$helpers,"
    + (debug ? "$line=0," : "");
    
    var mainCode = replaces[0];

    var footerCode = "return new String(" + replaces[3] + ");"
    
    // html与逻辑语法分离
    forEach(source.split(openTag), function (code) {
        code = code.split(closeTag);
        
        var $0 = code[0];
        var $1 = code[1];
        
        // code: [html] 以openTag起始,无closeTag闭合,处理成html字符串形式
        if (code.length === 1) {
            mainCode += html($0);
         
        // code: [logic, html] 以openTag起始,有closeTag闭合,处理成logic+html字符串形式
        } else {
            mainCode += logic($0);
            
            if ($1) {
                mainCode += html($1);
            }
        }
    });
    
    var code = headerCode + mainCode + footerCode;
    
    // 调试语句,试用try-catch方法捕获错误,报错
    if (debug) {
        code = "try{" + code + "}catch(e){"
        +       "throw {"
        +           "filename:$filename,"
        +           "name:'Render Error',"
        +           "message:e.message,"
        +           "line:$line,"
        +           "source:" + stringify(source)
        +           ".split(/\\n/)[$line-1].replace(/^\\s+/,'')"
        +       "};"
        + "}";
    }
    
    try {
    	// code用于拼接字符串构建函数
        var Render = new Function("$data", "$filename", code);
        Render.prototype = utils;
        return Render;
    } catch (e) {
        e.temp = "function anonymous($data,$filename) {" + code + "}";
        throw e;
    }

    // 处理 HTML 语句
    function html (code) {
        // 记录行号,调试模式下输出处理失败的行号
        line += code.split(/\n/).length - 1;

        // 压缩多余空白与注释
        if (compress) {
            code = code
            .replace(/\s+/g, ' ')
            .replace(/<!--[\w\W]*?-->/g, '');
        }
        
        if (code) {
            code = replaces[1] + stringify(code) + replaces[2] + "\n";
        }

        return code;
    }
    
    // 处理逻辑语句
    function logic (code) {

        var thisLine = line;
       
        if (parser) {
            // 语法转换插件钩子,默认的钩子为拼接if、each、echo等语句
            code = parser(code, options); 
        } else if (debug) {
            // 记录行号
            code = code.replace(/\n/g, function () {
                line ++;
                return "$line=" + line +  ";";
            });
        }
        
        // 输出语句. 编码: <%=value%> 不编码:<%=#value%>
        // <%=#value%> 等同 v2.0.3 之前的 <%==value%>
        if (code.indexOf('=') === 0) {
            var escapeSyntax = escape && !/^=[=#]/.test(code);

            code = code.replace(/^=[=#]?|[\s;]*$/g, '');

            // 对内容编码
            if (escapeSyntax) {
                var name = code.replace(/\s*\([^\)]+\)/, '');

                // 排除 utils.* | include | print,当name值为utils中内部方法或print、include
                // headerCode中,this关键字指向$utils,$escape可直接调用,对html中'、"、<、>、&进行转义
                if (!utils[name] && !/^(include|print)$/.test(name)) {
                    code = "$escape(" + code + ")";
                }

            // 不编码
            } else {
                code = "$string(" + code + ")";
            }
            
            code = replaces[1] + code + replaces[2];
        }
        
        if (debug) {
            code = "$line=" + thisLine + ";" + code;
        }
        
        // 提取模板中的方法名,在headerCode中注入该方法的内容,拼接的函数体内就可以通过方法名调用
        forEach(getVariable(code), function (name) {
            
            // name 值可能为空,在安卓低版本浏览器下
            if (!name || uniq[name]) {
                return;
            }

            var value;

            // 声明模板变量
            // 赋值优先级:
            // [include, print] > utils > helpers > data
            if (name === 'print') {
                value = print;
            } else if (name === 'include') {
                value = include;
            } else if (utils[name]) {
                value = "$utils." + name;
            } else if (helpers[name]) {
                value = "$helpers." + name;
            } else {
                value = "$data." + name;
            }
            
            headerCode += name + "=" + value + ",";
            uniq[name] = true;
        });
        
        return code + "\n";
    }
    
};

2.if、each、print、echo、include语句及过滤函数预处理

syntax.js

// 语法转换插件钩子,"<%"、"%>"间内部值预处理,拼接if、each、print、include、echo语句等,参见compiler模块

defaults.openTag = '{{';
defaults.closeTag = '}}';

// {{value | filterA:'abcd' | filterB}}形式,调用$helpers下方法对value进行过滤处理
var filtered = function (js, filter) {
    var parts = filter.split(':');
    var name = parts.shift();
    var args = parts.join(':') || '';

    if (args) {
        args = ', ' + args;
    }

    return '$helpers.' + name + '(' + js + args + ')';
}

// 语法转换插件钩子,"<%"、"%>"间内部值预处理,拼接if、each、print、include、echo语句等,参见compiler模块
defaults.parser = function (code, options) {
    // var match = code.match(/([\w\$]*)(\b.*)/); // \b单词边界符
    // var key = match[1];
    // var args = match[2];
    // var split = args.split(' ');
    // split.shift();
    code = code.replace(/^\s/, '');// 滤除起始的空格

    var split = code.split(' ');
    var key = split.shift();
    var args = split.join(' ');

    switch (key) {
        // 拼接if语句
        case 'if':
            code = 'if(' + args + '){';
            break;
        case 'else':
            if (split.shift() === 'if') {
                split = ' if(' + split.join(' ') + ')';
            } else {
                split = '';
            }

            code = '}else' + split + '{';
            break;
        case '/if':
            code = '}';
            break;

        // 拼接each语句
        case 'each':
            var object = split[0] || '$data';
            var as     = split[1] || 'as';
            var value  = split[2] || '$value';
            var index  = split[3] || '$index';
            
            var param   = value + ',' + index;
            
            if (as !== 'as') {
                object = '[]';
            }
            
            code =  '$each(' + object + ',function(' + param + '){';
            break;
        case '/each':
            code = '});';
            break;

        // 拼接print语句
        case 'echo':
            code = 'print(' + args + ');';
            break;

        // 拼接print、include语句
        case 'print':
        case 'include':
            code = key + '(' + split.join(',') + ');';
            break;

        default:
            // 过滤器(辅助方法),value为待过滤的变量,filterA为helpers下方法名,'abcd'为filterA参数
            // {{value | filterA:'abcd' | filterB}}
            // >>> $helpers.filterB($helpers.filterA(value, 'abcd'))
            // TODO: {{ddd||aaa}} 不包含空格
            if (/^\s*\|\s*[\w\$]/.test(args)) {

                var escape = true;

                // {{#value | link}}
                if (code.indexOf('#') === 0) {
                    code = code.substr(1);
                    escape = false;
                }

                var i = 0;
                var array = code.split('|');
                var len = array.length;
                var val = array[i++];

                for (; i < len; i ++) {
                    val = filtered(val, array[i]);
                }

                code = (escape ? '=' : '=#') + val;

            // 即将弃用 {{helperName value}}
            } else if (template.helpers[key]) {
                code = '=#' + key + '(' + split.join(',') + ');';
            
            // 内容直接输出 {{value}}
            } else {
                code = '=' + code;
            }
            break;
    }
    
    return code;
};

3.辅助函数

utils.js

var toString = function (value, type) {

    if (typeof value !== 'string') {

        type = typeof value;
        if (type === 'number') {
            value += '';
        } else if (type === 'function') {
            value = toString(value.call(value));
        } else {
            value = '';
        }
    }

    return value;
};

var escapeMap = {
    "<": "&#60;",
    ">": "&#62;",
    '"': "&#34;",
    "'": "&#39;",
    "&": "&#38;"
};

var escapeFn = function (s) {
    return escapeMap[s];
};

var escapeHTML = function (content) {
    return toString(content)
    .replace(/&(?![\w#]+;)|[<>"']/g, escapeFn);
};

var isArray = Array.isArray || function (obj) {
    return ({}).toString.call(obj) === '[object Array]';
};

var each = function (data, callback) {
    var i, len;        
    if (isArray(data)) {
        for (i = 0, len = data.length; i < len; i++) {
            callback.call(data, data[i], i, data);
        }
    } else {
        for (i in data) {
            callback.call(data, data[i], i);
        }
    }
};

var utils = template.utils = {
	$helpers: {},
    $include: renderFile,
    $string: toString,
    $escape: escapeHTML,
    $each: each 
};

helper.js,可由用户添加过滤函数等

/**
 * 添加模板辅助方法
 * @name    template.helper
 * @param   {String}    名称
 * @param   {Function}  方法
 */
template.helper = function (name, helper) {
    helpers[name] = helper;
};

var helpers = template.helpers = utils.$helpers;

4.编译接口

template.js

/**
 * 模板引擎
 * @name    template
 * @param   {String}            模板名
 * @param   {Object, String}    数据。如果为字符串,则作为模板字符串进行编译,缓存并返回编译函数
 *                              	如果为对象,则作为传给编译函数的数据,最终返回编译结果
 * @return  {String, Function}  渲染好的HTML字符串或者渲染方法
 */
var template = function (filename, content) {
    return typeof content === 'string'
    ?   compile(content, {
            filename: filename
        })
    :   renderFile(filename, content);
};

template.version = '3.0.0';

renderFile.js

/**
 * 渲染模板(根据模板名)
 * @name    template.render
 * @param   {String}    模板名,页面元素id
 * @param   {Object}    数据,data传入为空时,返回结果为编译函数
 * @return  {String}    渲染好的字符串
 */
var renderFile = template.renderFile = function (filename, data) {
    var fn = template.get(filename) || showDebugInfo({
        filename: filename,
        name: 'Render Error',
        message: 'Template not found'
    });
    return data ? fn(data) : fn;
};

get.js

/**
 * 获取编译缓存(可由外部重写此方法)
 * @param   {String}    模板名
 * @param   {Function}  编译好的函数
 */
template.get = function (filename) {

    var cache;
    
    if (cacheStore[filename]) {
        // 获取使用内存缓存的编译函数
        cache = cacheStore[filename];
    } else if (typeof document === 'object') {
        // 通过模板名获取模板字符串,编译,并返回编译函数
        var elem = document.getElementById(filename);
        
        if (elem) {
            var source = (elem.value || elem.innerHTML)
            .replace(/^\s*|\s*$/g, '');
            cache = compile(source, {
                filename: filename
            });
        }
    }

    return cache;
};

render.js

/**
 * 渲染模板
 * @name    template.render
 * @param   {String}    模板字符串
 * @param   {Object}    数据
 * @return  {String}    编译函数
 */
template.render = function (source, options) {
    return compile(source, options);
};

5.错误提示

onerror.js

/**
 * 模板错误事件(可由外部重写此方法),触发console.error提示错误信息
 * @name    template.onerror
 * @event
 */
template.onerror = function (e) {
    var message = 'Template Error\n\n';
    for (var name in e) {
        message += '<' + name + '>\n' + e[name] + '\n\n';
    }
    
    if (typeof console === 'object') {
        console.error(message);
    }
};

// 模板调试器
var showDebugInfo = function (e) {

    template.onerror(e);
    
    return function () {
        return '{Template Error}';
    };
};

6.配置

compile.js

/**
 * 设置全局配置
 * @name    template.config
 * @param   {String}    名称
 * @param   {Any}       值
 */
template.config = function (name, value) {
    defaults[name] = value;
};

var defaults = template.defaults = {
    openTag: '<%',    // 逻辑语法开始标签
    closeTag: '%>',   // 逻辑语法结束标签
    escape: true,     // 是否编码输出变量的 HTML 字符
    cache: true,      // 是否开启缓存(依赖 options 的 filename 字段)
    compress: false,  // 是否压缩输出
    parser: null      // 自定义语法格式器 @see: template-syntax.js
};

7.外层包裹,适用于amd/cmd/commonjs环境,同seajs

intro.js

/*!
 * artTemplate - Template Engine
 * https://github.com/aui/artTemplate
 * Released under the MIT, BSD, and GPL Licenses
 */
 
!(function () {

outro.js

// RequireJS && SeaJS
if (typeof define === 'function') {
    define(function() {
        return template;
    });

// NodeJS
} else if (typeof exports !== 'undefined') {
    module.exports = template;
} else {
    this.template = template;
}

})();

附:拼接js文件实现使用grunt

Gruntfile.js配置

module.exports = function (grunt) {

    var sources_native = [
        'src/intro.js',
        'src/template.js',
        'src/config.js',
        'src/cache.js',
        'src/render.js',
        'src/renderFile.js',
        'src/get.js',
        'src/utils.js',
        'src/helper.js',
        'src/onerror.js',
        'src/compile.js',
                    //<<<< 'src/syntax.js',
        'src/outro.js'
    ];

    var sources_simple = Array.apply(null, sources_native);
    sources_simple.splice(sources_native.length - 1, 0, 'src/syntax.js');


    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        meta: {
            banner: '/*!<%= pkg.name %> - Template Engine | <%= pkg.homepage %>*/\n'
        },
        concat: {
            options: {
                separator: ''
            },

            'native': {
                src: sources_native,
                dest: 'dist/template-native-debug.js'
            },

            simple: {
                src: sources_simple,
                dest: 'dist/template-debug.js'
            }
        },
        uglify: {
            options: {
                banner: '<%= meta.banner %>'
            },
            'native': {
                src: '<%= concat.native.dest %>',
                dest: 'dist/template-native.js'
            },
            simple: {
                src: '<%= concat.simple.dest %>',
                dest: 'dist/template.js'
            }
        },
        qunit: {
            files: ['test/**/*.html']
        },
        jshint: {
            files: [
              'dist/template-native.js',
              'dist/template.js'
            ],
            options: {
                curly: true,
                eqeqeq: true,
                immed: true,
                latedef: true,
                newcap: true,
                noarg: true,
                sub: true,
                undef: true,
                boss: true,
                eqnull: true,
                browser: true
            },
            globals: {
                console: true,
                define: true,
                global: true,
                module: true
            }
        },
        watch: {
            files: '<config:lint.files>',
            tasks: 'lint qunit'
        }
    });

    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-jshint');
    //grunt.loadNpmTasks('grunt-contrib-qunit');
    //grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-concat');


    grunt.registerTask('default', ['concat', /*'jshint',*/ 'uglify']);

};

package.json

{
  "name": "art-template",
  "description": "JavaScript Template Engine",
  "homepage": "http://aui.github.com/artTemplate/",
  "keywords": [
    "util",
    "functional",
    "template"
  ],
  "author": "tangbin <[email protected]>",
  "repository": {
    "type": "git",
    "url": "git://github.com/aui/artTemplate.git"
  },
  "main": "./node/template.js",
  "version": "3.0.3",
  "devDependencies": {
    "express": "~4.4.3",
    "grunt-cli": "*",
    "grunt": "*",
    "grunt-contrib-jshint": "*",
    "grunt-contrib-concat": "*",
    "grunt-contrib-uglify": "*"
  },
  "license": "BSD"
}

猜你喜欢

转载自schifred.iteye.com/blog/2346308