编写一个简单的JavaScript模板引擎
随着Nodejs的流行,JavaScript在前端和后端都开始流行起来。有许多成熟的JavaScript模板引擎,例如Swig,既可以用在后端,又可以用在前端。
不过很多时候,前端模板仅仅需要简单地创建一个HTML片段,用Swig这种全功能模板有点大材小用。我们来尝试自己编写一个简单的前端模板引擎,实际上并不复杂。
在编写前端模板引擎代码之前,我们应该想好如何来调用它,即这个模板引擎的接口应该是什么样的。我们希望这样调用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 创建一个模板引擎: var tpl = new Template( '<p>Today: { date }</p>\n<a href="/{ user.id|safe }">{ user.company }</a>' ); // 渲染得到HTML片段: var model = { date: 20150316 , user: { id: 'A-000&001' , company: 'AT&T' } }; var html = tpl.render(model); console.log(html); // <p>Today: 20150316</p> // <a href="/A-000&001">AT&T</a> |
因此,一个模板引擎就是把一个字符串中的变量用model
的变量替换掉,就完成了。
像Swig这种类Jinja2的模板引擎,它可以替换{{ model.prop }}
这样的变量。
我们选用{ model.prop }
来实现我们自己的变量替换,基本思想是用一个正则表达式来匹配{ xxx.xxx }
:
1 2 | var re = /\{\s*([a-zA-Z\.\_0- 9 ()]+)\s*\}/m var match = re.exec( 'a { template } string' ); |
如果正则匹配成功,则match
不为空,match[0]
是匹配到的字符串{ template }
,match[1]
是捕获的变量template
,match.index
是匹配的索引。
只要不断地匹配到变量,然后用model
的内容替换,就可以得到最终的HTML。但是,分析user.addr.zipcode
然后去model
中查找并不容易。而且,模板应该可以预编译,这样,后续渲染速度就会很快。
JavaScript允许用new Function('source')
来通过字符串创建一个函数,这个函数和我们用function ()
定义的函数是一模一样的,因此,一个模板引擎的编译过程就是创建一个函数,然后调用该函数就实现了模板渲染。
需要编译的函数代码应该像这样:
1 2 3 4 5 6 7 8 9 10 11 | function () { var r = []; r.push( '<p>Today: ' ); r.push( this .date); r.push( '</p>\n<a href="/' ); r.push( this .user.id); r.push( '">' ); r.push( this .user.company); r.push( '</a>' ); return r.join( '' ); } |
注意到变量名从variable.prop
变成了this.variable.prop
,是因为调用该函数时我们会把model
绑定到this
变量上。
因此,模板引擎的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | function Template(tpl) { var fn, match, code = [ 'var r=[];' ], re = /\{\s*([a-zA-Z\.\_0- 9 ()]+)\s*\}/m, addLine = function (text) { code.push( 'r.push(\'' + text.replace(/\ '/g, ' \\\ '' ).replace(/\n/g, '\\n' ).replace(/\r/g, '\\r' ) + '\');' ); }; while (match = re.exec(tpl)) { if (match.index > 0 ) { addLine(tpl.slice( 0 , match.index)); } code.push( 'r.push(this.' + match[ 1 ] + ');' ); tpl = tpl.substring(match.index + match[ 0 ].length); } addLine(tpl); code.push( 'return r.join(\'\');' ); // 创建函数: fn = new Function(code.join( '\n' )); // 用render()调用函数并绑定this参数: this .render = function (model) { return fn.apply(model); }; } |
现在,这个简单的模板引擎已经可以工作了。但是它还有几个小问题需要解决,一是默认的变量在替换时应该做HTML转义,二是如果某些不需要转义的变量,可以用{ user.id|safe }
这样的表达式表示user.id
无需转义。
经过HTML转义和{ variable|safe }
处理的最终代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | function Template(tpl) { var fn, match, code = [ 'var r=[];\nvar _html = function (str) { return str.replace(/&/g, \'&\').replace(/"/g, \'"\').replace(/\'/g, \''\').replace(/</g, \'<\').replace(/>/g, \'>\'); };' ], re = /\{\s*([a-zA-Z\.\_0- 9 ()]+)(\s*\|\s*safe)?\s*\}/m, addLine = function (text) { code.push( 'r.push(\'' + text.replace(/\ '/g, ' \\\ '' ).replace(/\n/g, '\\n' ).replace(/\r/g, '\\r' ) + '\');' ); }; while (match = re.exec(tpl)) { if (match.index > 0 ) { addLine(tpl.slice( 0 , match.index)); } if (match[ 2 ]) { code.push( 'r.push(String(this.' + match[ 1 ] + '));' ); } else { code.push( 'r.push(_html(String(this.' + match[ 1 ] + ')));' ); } tpl = tpl.substring(match.index + match[ 0 ].length); } addLine(tpl); code.push( 'return r.join(\'\');' ); fn = new Function(code.join( '\n' )); this .render = function (model) { return fn.apply(model); }; } |
现在就可以用我们预设的代码来使用这个模板引擎了。不过,把模板写在字符串中也不是一个好办法。最佳解决方案是利用<script>
标签,把模板写在里面,注意一定要加上type="text/plain"
:
1 2 3 4 | <script id= "tpl" type= "text/plain" > <p>Today: { date }</p> <a href= "/{ user.id|safe }" >{ user.company }</a> </script> |
然后,用jQuery来获得模板内容并渲染:
1 2 3 4 5 6 7 8 9 | var tpl = new Template($( '#tpl' ).html()); var s = tpl.render({ date: 20150101 , user: { id: 'A-000&001' , company: 'AT&T' } }); $( '#other' ).html(s); |
这样,我们就用不到30行代码实现了一个简单的JavaScript模板引擎。
建议继续学习:
- 前端模板引擎 (阅读:3856)
- 高性能JavaScript模板引擎原理解析 (阅读:3291)
- 用于前端的模板引擎 (阅读:3230)
- Juicer – 一个Javascript模板引擎的实现和优化 (阅读:2540)
- Javascript模板引擎分享 (阅读:2407)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:廖雪峰 来源: 廖雪峰的官方网站
- 标签: 模板引擎
- 发布时间:2015-04-26 23:00:33
-
[54] 转载:cassandra读写性能原理分析
-
[50] memory prefetch浅析
-
[43] 《web前端最佳实践》—高维护性css
-
[40] MySQL半同步存在的问题
-
[39] 深入浅出cassandra 4 数据一致性问
-
[36] 不是书评 :《我是一只IT小小鸟》
-
[35] 获取Dom元素的X/Y坐标
-
[35] javascript插入样式
-
[35] 程序员技术练级攻略
-
[34] MySQL vs NoSQL 效率与成本之争