20行实现javascript模板引擎
20行实现javascript模板引擎
我仍然在用AbsurdJS预处理器写javascript。起初,这只是一个CSS预处理器,后来我把它扩展为CSS/HTML预处理器。最近,它可以实现javascript到CSS/HTML的转换,因为它可以作为模板引擎来生成HTML。比如,可以用数据填充HTML模板。
然后,我就想写一个简单的模板引擎可以完美地与我当前的开发工作相配合。AbsurdJS主要是作为nodejs模块来发布的,但是它也可以作为客户端使用。有了这种想法,我意识到我不能利用现有的模板引擎。因为现在的大多数模板引擎只是基于nodejs,很难把他们复制到浏览器来使用。我需要一个体积小的用原生JS写的模板引擎。我曾经拜读过John Resig的一篇文章JavaScript Micro-Templating。这好像就是我所需要的。我把里面的代码做了些许变动,使之缩减为20行。我想这脚本的运行机制是非常有趣的。本文中,我一步步地重新创建一个模板引擎,然后你就会体会到来自John的伟大创意。
我们以下面的代码作为开始吧:
- var TemplateEngine = function(tpl, data) {
- // magic here ...
- }
- var template = '<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>';
- console.log(TemplateEngine(template, {
- name: "Krasimir",
- age: 29
- }));
一个简单的函数,来处理我们的模板与数据对象。
你可能会猜想到,我们最终想要达到的结果就是下面的样子:
- <p>Hello, my name is Krasimir. I'm 29 years old.</p>
首先我们必须处理模板内部的动态语法,然后我们用传递给模板引擎的真实数据来替换这些动态语法。我决定利用正则表达式来实现。正则表达式不是我的强项,所以你可以留言建议给我一个更好的正则表达式。
- var re = /<%([^%>]+)?%>/g;
这样,我们会捕获到分别以“%”开始与结束的分组。“g”(global全局)表示我们得到的不是一个,而是全部匹配到的。采用正则表达式的exec方法可以把匹配到分组的以数组的形式表现:
- var re = /<%([^%>]+)?%>/g; var match = re.exec(tpl);
- console.log(match);
结果:
- [
- "<%name%>",
- " name ",
- index: 21,
- input:
- "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>"
- ]
但是我们只得到了一个,再来改善一下:
- var TemplateEngine = function(tpl, data) {
- var re = /<%([^%>]+)?%>/g;
- while(match = re.exec(tpl)) {
- tpl = tpl.replace(match[0], data[match[1]])
- }
- return tpl;
- }
OK,我们的最初目标达到了,但这远远不够。这只能容易获取到data['property']。但在实践中,我们可能遇到复杂的嵌套对象。比如:
- {
- name: "Krasimir Tsonev",
- profile: { age: 29 }
- }
我们先前所做的工作就这样失效了!
那我们就分析一下其他的情况。比如,我们有个这样的模板:
- var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
再或者我们可能还见过这样的模板:
- var template =
- 'My skills:' +
- '<%for(var index in this.skills) {%>' +
- '<a href=""><%this.skills[index]%></a>' +
- '<%}%>';
这该怎么办呢? John采用了 new Function 语法实现——可以从字符串来创建函数。
那我们先来熟悉一下这种语法。看一个简单的例子:
- var fn = new Function("arg", "console.log(arg + 1);");
- fn(2); // outputs 3
上述代码创建的函数fn等价于:
- function fn(arg){
- console.log(arg+1);
- }
- fn(2) // outputs 3
这样我们可以自定义函数,其参数与函数体可以来自简单的字符串。而我们所需要的方法应该能够返回为最终的编译模板。就像这样:
- return
- "<p>Hello, my name is " +
- this.name +
- ". I\'m " +
- this.profile.age +
- " years old.</p>";
而对于
- var template =
- 'My skills:' +
- '<%for(var index in this.skills) {%>' +
- '<a href=""><%this.skills[index]%></a>' +
- '<%}%>';
我们所需要的应该是这样:
- return
- 'My skills:' +
- for(var index in this.skills) { +
- '<a href="">' +
- this.skills[index] +
- '</a>' +
- }
当然我们所设想的会产生语法错误,那我们可以改变一下:
- var r = [];
- r.push('My skills:');
- for(var index in this.skills) {
- r.push('<a href="">');
- r.push(this.skills[index]);
- r.push('</a>');
- }
- return r.join('');
有了预想结果,下一步就是分别处理每一行来产生自定义函数。
在进行处理每一行的时候,我们应该考虑到以下问题:
- 引号的转义,否则产生的脚本不可用
- <% %>里面的字符不应该被当做字符串处理
- var TemplateEngine = function(tpl, data) {
- var re = /<%([^%>]+)?%>/g,
- code = 'var r=[];\n',
- cursor = 0;
- var add = function(line) {
- // 引号转义,将"替换为\"放入定义的函数体
- code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';
- }
- //匹配到<% %>
- while(match = re.exec(tpl)) {
- // <% %>之前当做字符串放入函数体
- add(tpl.slice(cursor, match.index));
- // <% %>中间部分
- add(match[1]);
- //迭代处理<% %>后面部分
- cursor = match.index + match[0].length;
- }
- add(tpl.substr(cursor, tpl.length - cursor));
- code += 'return r.join("");'; // <-- return the result
- console.log(code);
- return tpl;
- }
- var template = '<p>Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.</p>';
- console.log(TemplateEngine(template, {
- name: "Krasimir Tsonev",
- profile: { age: 29 }
- }));
考虑到if/else等JS语句,我们再做优化:
- var TemplateEngine = function(html, options) {
- var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0;
- var add = function(line, js) {
- js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :
- (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');
- return add;
- }
- while(match = re.exec(html)) {
- add(html.slice(cursor, match.index))(match[1], true);
- cursor = match.index + match[0].length;
- }
- add(html.substr(cursor, html.length - cursor));
- code += 'return r.join("");';
- return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);
- }
终极目标实现!!
详见最终版本
原文作者:http://krasimirtsonev.com/blog/article/Javascript-template-engine-in-just-20-line