最初接觸的模板引擎還是基於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 && SeaJS if (typeof define === 'function') { define(function() { return template; }); // NodeJS } else if (typeof exports !== 'undefined') { module.exports = template; } else { this.template = template; }
從網上找了一個artTemplate的結構圖,剛開始看可能看不明白,等看完源碼再看這張圖,會感覺清晰很多。
