前段时间,想着自己写一个简单的模版引擎,便于自己平时开发 demo 时使用,同时也算是之前学习的知识的一种总结吧!
首先我们先了解一下模版引擎的工作原理吧!
1. 模版引擎其实就是将指定标签的内容根据固定规则,解析为可执行语句字符串;
2. 执行可执行解析后的语句字符串,即生成我们想要的页面结构。
- 1
- /* 解析前
- 2
- 3 {{for(var i = 0; i < data.todos.length; ++i)}}
- 4 {{if(data.todos[i].todo_type)}}
- 5 {{data.todos[i].todo_name}}
- 6 {{/if}}
- 7 {{/for}}
- 8
- 9 */
- 10 11
- /* 解析后
- 12 var str = "";
- 13 str += "";
- 14 for (var i = 0; i < data.todos.length; ++i) {
- 15 if (data.todos[i].todo_type) {
- 16 str += "";
- 17 str += data.todos[i].todo_name;
- 18 str += "";
- 19 }
- 20 }
- 21 str += "";
- 22 */
- 23 24
- /* 执行后
- 25 eatsleepplay
- 26 */
1. 定义属于自己的模版引擎格式
2. 创建一个全局对象,它包括存放编译后字符串的属性,编译和执行的函数方法,以及一些工具函数
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;
- },
- 1 2 3 4 5 6 7 8 910
- 111213
- 14{{for(vari = 0; i < data.todos.length; ++i) { }}
- 15{{if(data.todos[i].todo_type) { }}
- 16
- {{data.todos[i].todo_name}}
- 17 {{ } }}
- 18 {{ } }}
- 192021
- 2223 vardata = {
- 24 todos: [{
- 25todo_name: "eat",
- 26todo_type: "todo"27 }, {
- 28todo_name: "sleep",
- 29todo_type: "completed"30 }, {
- 31todo_name: "play",
- 32todo_type: "todo"33 }]
- 34
- 35 };
- 36 vartpl = document.getElementById('test_template');
- 37
- 38str = tpl.innerHTML;
- 39
- 40 template.complileTpl(str);
- 41
- 42 varroot = document.getElementById('test');
- 43
- 44 template.executeTpl(root, data);
- 454647
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 函数)。
来源: http://www.cnblogs.com/diligentYe/p/7076720.html