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

相关文章
相关标签/搜索