js模版引擎開發實戰以及對eval函數的改進
簡介
前段時間,想著自己寫一個簡單的模版引擎,便於自己平時開發demo時使用,同時也算是之前學習的知識的一種總結吧!
首先我們先了解一下模版引擎的工作原理吧!
1. 模版引擎其實就是將指定標簽的內容根據固定規則,解析為可執行語句字符串;
2. 執行可執行解析後的語句字符串,即生成我們想要的頁面結構。
具體實現方法:
1. 最終效果
1 /* 解析前
2 <ul>
3 {{for(var i = 0; i < data.todos.length; ++i)}}
4 {{if(data.todos[i].todo_type)}}
5 <li>{{data.todos[i].todo_name}}</li>
6 {{/if}}
7 {{/for}}
8 </ul>
9 */
10
11 /* 解析後
12 var str = "";
13 str += "<ul>";
14 for (var i = 0; i < data.todos.length; ++i) {
15 if (data.todos[i].todo_type) {
16 str += "<li>";
17 str += data.todos[i].todo_name;
18 str += "</li>";
19 }
20 }
21 str += "</ul>";
22 */
23
24 /* 執行後
25 <ul><li>eat</li><li>sleep</li><li>play</li></ul>
26 */
2. 整體分析
1. 定義屬於自己的模版引擎格式
2. 創建一個全局對象,它包括存放編譯後字符串的屬性,編譯和執行的函數方法,以及一些工具函數
3. 具體實現
1. 自定義模版引擎格式
1. 賦值 {{data}}
2. 判斷 {{if(...) { }} {{ } else if(...) { }} {{ } else { }} {{ } }}
3. 對象 {{for(key in object) { }} {{ } }}
4. 數組 {{for(var i = 0); i < arrays.length; ++i) { }} {{ } }}
2. 定義全局對象
全局對象中包括五個函數和一個字符串:其中complileTpl用於解析字符串,executeTpl用於運行解析生成的代碼, jsStr用於存放解析生成的字符串,其他都是中間處理函數。
var template = {
// 存放解析後的js字符串
jsStr: "var str = ‘‘;",
/**
* 將模版中的字符串解析為可執行的js語句
* @param {string} tpl 模版字符串
*/
complileTpl: function(tpl) {
},
/**
* 執行解析後的js語句
* @param {DOM對象} root 掛載對象
* @param {json} data 解析的數據對象
*/
executeTpl: function(root, data) {
},
/**
* 不包含指令行的處理函數
* @param {string} str 需要處理的字符串
*/
_handleLabel: function(str) {
},
/**
* 包含指令行的處理函數
* @param {string} str 需要處理的字符串
*/
_handleDirective: function(str) {
},
/**
* 處理字符串前後空白
* @param {string} str 需要處理的字符串
*/
_handlePadding: function(str) {
}
}
3. 解析函數詳解
由於我是在mac上開發的,mac上‘\n‘表示換行。
首先根據換行符,將標簽中的字符串,分隔為數組。然後分別根據每一行中是否包含指令,進行不同的處理。
如果不包含指令,創建一個將該字符串添加到存儲字符串的變量jsStr中。
如果包含指令,由於我設置了格式要求,只有賦值操作可以和html標簽在同一行,其他的指令都要獨占一樣,所以,當為賦值情況下,將指令左右的標簽元素作為字符串操作,添加到變量jsStr中,如過是其他指令,直接去掉{{}},添加到變量jsStr即可。
/**
* 將模版中的字符串解析為可執行的js語句
* @param {string} tpl 模版字符串
*/
complileTpl: function(tpl) {
// 模版字符串按行分隔
var tplArrs = tpl.split(‘\n‘);
for (var index = 0; index < tplArrs.length; ++index) {
var item = this._handlePadding(tplArrs[index]);
// 處理不包含指令的行
if (item.indexOf(‘{{‘) == -1) {
this._handleLabel(item);
} else {
this._handleDirective(item);
}
}
},
/**
* 不包含指令行的處理函數
* @param {string} str 需要處理的字符串
*/
_handleLabel: function(str) {
// 去除空行或者空白行
if (str) {
this.jsStr += "str += ‘" + str + "‘;";
}
},
/**
* 包含指令行的處理函數
* @param {string} str 需要處理的字符串
*/
_handleDirective: function(str) {
// 處理指令前的字符串
var index = str.indexOf(‘{{‘);
var lastIndex = str.lastIndexOf(‘}}‘);
if (index == 0 && lastIndex == str.length - 2) {
this.jsStr += str.slice(index + 2, lastIndex);
} else if (index != 0 && lastIndex != str.length - 2) {
this.jsStr += "str += ‘" + str.slice(0, index) + "‘;";
this.jsStr += "str += " + str.slice(index + 2, lastIndex) + ";";
this.jsStr += "str += ‘" + str.slice(lastIndex + 2, str.length) + "‘;";
} else {
throw new Error(‘格式錯誤‘);
}
},
/**
* 處理字符串前後空白
* @param {string} str 需要處理的字符串
*/
_handlePadding: function(str) {
return str.replace(/^\s*||\s*$/g, ‘‘);
}
4. 執行編譯後的字符串語句
使用eval運行編譯後的字符串語句。
/**
* 執行解析後的js語句
* @param {DOM對象} root 掛載對象
* @param {json} data 解析的數據對象
*/
executeTpl: function(root, data) {
var html = eval(this.jsStr);
console.log(html);
root.innerHTML = html;
},
5. 使用方法
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Document</title> 6 <script src="utils/template.js"></script> 7 </head> 8 <body> 9 <div id="test"> 10 11 </div> 12 <script id="test_template" type="text/my_template"> 13 <ul> 14 {{for(var i = 0; i < data.todos.length; ++i) { }} 15 {{if(data.todos[i].todo_type) { }} 16 <li>{{data.todos[i].todo_name}}</li> 17 {{ } }} 18 {{ } }} 19 </ul> 20 </script> 21 22 <script> 23 var data = { 24 todos: [{ 25 todo_name: "eat", 26 todo_type: "todo" 27 }, { 28 todo_name: "sleep", 29 todo_type: "completed" 30 }, { 31 todo_name: "play", 32 todo_type: "todo" 33 }] 34 35 }; 36 var tpl = document.getElementById(‘test_template‘); 37 38 str = tpl.innerHTML; 39 40 template.complileTpl(str); 41 42 var root = document.getElementById(‘test‘); 43 44 template.executeTpl(root, data); 45 </script> 46 </body> 47 </html>
4. 延伸
eval等價於evil!
為什麽呢?各大js權威書籍上都不提倡使用eval。下面我詳細的解釋一下為什麽不提倡。
首先,大家需要知道,js並不是一門解釋型語言。它和其他大家熟知的編程語言(c,java,c++)一樣,是編譯型語言。但是,它和其他的編譯型語言又不完全一樣。眾所周知,C語言等是預編譯的語言,它們可以編譯成目標代碼,移植到其他機器中運行。而js呢,它並不是一門預編譯的語言,它的編譯過程可能只在執行前一秒。但是,它確實在執行前進行了編譯過程。
然後,大家要了解一下,詞法作用域。所謂的詞法作用域,是指當前作用域,可以訪問的變量。
js編譯過程,其實就是在將申明的變量添加當前詞法作用域,並將其他代碼編譯成可執行代碼。然而,在瀏覽器中,做了一些列的優化,可以通過靜態代碼分析,定位申明的變量和函數的位置,方便後續訪問。然而,我們卻可以通過eval函數,改變當前詞法作用域。這樣一樣,瀏覽器所做的優化都將付諸一炬。當出現eval,瀏覽器做的最好的處理方式,就是不做任何處理。
以上為為什麽不提倡使用eval,下面我是如何規避eval函數!
主要的思路是:我們經常使用script標簽動態添加腳本文件,同樣我們也可以通過script標簽中添加可執行語句字符串,也就可以動態添加可執行語句。
代碼如下:
1 /**
2 * 將傳入的可執行字符串,通過script標簽執行
3 * @param {[string]} str 可執行字符串
4 */
5 function strToFun(str) {
6 // 創建script標簽
7 var script = document.createElement(‘script‘);
8 script.id = ‘executableString‘;
9
10 // 處理傳入的字符串,當相應的語句執行完畢後,將script標簽移除
11 var handleStr = ‘(function() { ‘ + str + ‘;var script = document.getElementById("executableString"); document.body.removeChild(script); })();‘;
12
13 // 將待執行的代碼添加到剛創建的script標簽中
14 script.innerHTML = handleStr;
15
16 // 將創建的腳本追加到DOM樹中
17 document.body.appendChild(script);
18 }
以上,只是我一時的想法,希望大家積極提供不同的想法!!!
雖然上面在解決eval問題的同時,引入了DOM操作,可能沒有改善性能,但是,這種方法是可以解決CSP(Content-Security-Policy)問題!!(CSP中可能會禁止使用eval函數)。
js模版引擎開發實戰以及對eval函數的改進