zoukankan      html  css  js  c++  java
  • 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上' '表示换行。

      首先根据换行符,将标签中的字符串,分隔为数组。然后分别根据每一行中是否包含指令,进行不同的处理。

      如果不包含指令,创建一个将该字符串添加到存储字符串的变量jsStr中。

      如果包含指令,由于我设置了格式要求,只有赋值操作可以和html标签在同一行,其他的指令都要独占一样,所以,当为赋值情况下,将指令左右的标签元素作为字符串操作,添加到变量jsStr中,如过是其他指令,直接去掉{{}},添加到变量jsStr即可。

        /**
         * 将模版中的字符串解析为可执行的js语句
         * @param  {string} tpl 模版字符串
         */
        complileTpl: function(tpl) {
            // 模版字符串按行分隔
            var tplArrs = tpl.split('
    ');
    
            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函数)。  

  • 相关阅读:
    CodeForces 288A Polo the Penguin and Strings (水题)
    CodeForces 289B Polo the Penguin and Matrix (数学,中位数)
    CodeForces 289A Polo the Penguin and Segments (水题)
    CodeForces 540C Ice Cave (BFS)
    网站后台模板
    雅图CAD
    mbps
    WCF学习-协议绑定
    数据库建表经验总结
    资源位置
  • 原文地址:https://www.cnblogs.com/diligentYe/p/7076720.html
Copyright © 2011-2022 走看看