js模版引擎開發實戰以及對eval函數的改進

 

 

簡介

  前段時間,想着本身寫一個簡單的模版引擎,便於本身平時開發demo時使用,同時也算是以前學習的知識的一種總結吧!html

  首先咱們先了解一下模版引擎的工做原理吧!java

  1. 模版引擎其實就是將指定標籤的內容根據固定規則,解析爲可執行語句字符串;c++

  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. 定義屬於本身的模版引擎格式json

  2. 建立一個全局對象,它包括存放編譯後字符串的屬性,編譯和執行的函數方法,以及一些工具函數數組

3. 具體實現

  1. 自定義模版引擎格式瀏覽器

    1. 賦值 {{data}}
        2. 判斷 {{if(...) { }} {{ } else if(...) { }} {{ } else { }} {{ } }}
        3. 對象 {{for(key in object) { }} {{ } }}
        4. 數組 {{for(var i = 0); i < arrays.length; ++i) { }} {{ } }}
        處理賦值之外,其餘語句須要獨佔一行app

  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函數)。

相關文章
相關標籤/搜索