• 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函数)。

  • 相关阅读:
    实验10:Problem D: STL——管道二
    实验10:Problem C: STL——呵呵型自动机
    实验10:Problem B: STL——哈哈型自动机
    实验10:Problem A: STL——整理唱片
    实验9:Problem I: 学生干部虚基类
    hihocoder1994 树与落叶 DFS+前缀和+二分
    [Offer收割]编程练习赛108
    【模板】左偏树(可并堆)
    P2993 [FJOI2014]最短路径树问题 点分治+最短路
    E
  • 原文地址:https://www.cnblogs.com/PopularProdigal/p/7092646.html
Copyright © 2020-2023  润新知