首页 > 代码库 > artTemplate模板引擎的源码拜读

artTemplate模板引擎的源码拜读

最初接触的模板引擎还是基于node的ejs,当时觉得很神奇原来还可以这么玩,后来随着学习的深入,使用过jade,doT等,当然还有一些比较火的诸如juicer、underscore还没有深入接触,直到今年上半年由于项目需要就想着要不试试腾讯的artTemplate,感觉牛逼也吹的挺响的。开始了解后,觉得它比我之前使用过的jade、doT都好用,调试神马的也方便很多,采用预编译的方式也让性能非常优越。

其实看了源码后简单的总结出来就是这么一句话:就是先获取html中对应的id下得innerHTML,利用开始标签和关闭标签进行字符串切分,其实是将模板划分成两部份内容,一部分是html部分,一部分是逻辑部分,通过区别一些特殊符号比如each、if等来将字符串拼接成函数式的字符串,将两部分各自经过处理后,再次拼接到一起,最后将拼接好的字符串采用new Function()的方式转化成所需要的函数。

总共700多行的代码,其实简化下来就200多行代码,

刚开始肯定是先定义方法:

 

 1 var template = function (filename, content) { 2     return typeof content === ‘string‘ 3     ?   compile(content, { 4             filename: filename 5         }) 6     :   renderFile(filename, content); 7 }; 8  9 10 var renderFile = template.renderFile = function (filename, data) {11     var fn = template.get(filename) || showDebugInfo({12         filename: filename,13         name: ‘Render Error‘,14         message: ‘Template not found‘15     });16     return data ? fn(data) : fn;17 };

 

定义好后开始缓存fn方法,通过id获取模板内容

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

 

其实是对渲染的方法进行缓存,接下来开始编译模板

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);    }    return render;};

 

把从模板中提取的代码分成两部分,一部分是html,一部分是逻辑部分,各自进行处理

function compiler (source, options) {    var debug = options.debug;    var openTag = options.openTag;    var closeTag = options.closeTag;    var parser = options.parser;    var compress = options.compress;    var escape = options.escape;    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]        if (code.length === 1) {            mainCode += html($0);        // code: [logic, html]        } else {            mainCode += logic($0);            if ($1) {                mainCode += html($1);            }        }    });    var code = headerCode + mainCode + footerCode;    try {        //将拼接好的字符串通过new Function的方法进行函数化        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) {             // 语法转换插件钩子            code = parser(code, options);        } else if (debug) {            // 记录行号            code = code.replace(/\n/g, function () {                line ++;                return "$line=" + line +  ";";            });        }        // 输出语句. 编码: <%=value%> 不编码:<%=#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                if (!utils[name] && !/^(include|print)$/.test(name)) {                    code = "$escape(" + code + ")";                }            // 不编码            } else {                code = "$string(" + code + ")";            }            code = replaces[1] + code + replaces[2];        }        // 提取模板中的变量名        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";    }};

 

在进行逻辑部分处理时,静态分析模板变量;采用正则表达式首先过滤掉系统关键字,其次过滤掉不合法变量,再去除掉收尾的都好,最后根据都好分割成数组形式,以此来拼接字符串。再通过上面的new Function将字符串生成一个function,此为渲染方法,提供数据,进行剩下的结合。

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;var SPLIT_RE = /[^\w$]+/g;//非数字字母下滑线和$符以外的其他字符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 = /^$|,+/;//匹配多个逗号,用于分割 类似 param1,param2,,param3=> ["param1","param2","param3"] ,/^$/是为了匹配防止空字符串被切割// 获取变量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‘) + "‘";}

 

还有一些模板的辅助方法、错误事件、模板调试器等等

template.helper = function (name, helper) {    helpers[name] = helper;};var helpers = template.helpers = utils.$helpers;/** * 模板错误事件(可由外部重写此方法) * @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}‘;    };};

 

最后return template;

// RequireJS && SeaJSif (typeof define === ‘function‘) {    define(function() {        return template;    });// NodeJS} else if (typeof exports !== ‘undefined‘) {    module.exports = template;} else {    this.template = template;}

 

从网上找了一个artTemplate的结构图,刚开始看可能看不明白,等看完源码再看这张图,会感觉清晰很多。技术分享

 

artTemplate模板引擎的源码拜读