Jadi saya fikir sama ada saya boleh menulis beberapa kod mudah untuk menambah baik enjin templat ini dan bekerjasama dengan logik sedia ada yang lain. AbsurdJS sendiri terutamanya dikeluarkan sebagai modul untuk NodeJS, tetapi ia juga mengeluarkan versi klien. Dengan ini, saya tidak boleh menggunakan enjin sedia ada secara langsung kerana kebanyakannya dijalankan pada NodeJS dan tidak boleh dijalankan pada penyemak imbas. Apa yang saya perlukan ialah sesuatu yang kecil, ditulis semata-mata dalam Javascript, yang boleh dijalankan terus dalam penyemak imbas. Apabila saya terjumpa blog ini oleh John Resig pada suatu hari, saya sangat terkejut apabila mendapati bahawa inilah yang saya cari! Saya membuat sedikit pengubahsuaian, dan bilangan baris kod adalah kira-kira 20. Logiknya sangat menarik. Dalam artikel ini, saya akan mengeluarkan semula proses menulis enjin ini langkah demi langkah Jika anda boleh membaca bersama, anda akan memahami betapa tajamnya idea John.
Fikiran awal saya adalah seperti berikut:
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 }));
Fungsi mudah, input adalah templat dan objek data kami, outputnya mungkin mudah untuk anda fikirkan, seperti berikut:
Helo, nama saya Krasimir saya berumur 29 tahun.
var re = /<%([^%>]+)?%>/g;
Ungkapan biasa ini akan menangkap semua serpihan bermula dengan <% dan berakhir dengan %>. Parameter g (global) pada penghujung bermakna bukan sahaja satu dipadankan, tetapi semua serpihan yang sepadan dipadankan. Terdapat banyak cara untuk menggunakan ungkapan biasa dalam Javascript Apa yang kita perlukan ialah mengeluarkan tatasusunan yang mengandungi semua rentetan berdasarkan ungkapan biasa.
var re = /<%([^%>]+)?%>/g; var match = re.exec(tpl);
Jika kita menggunakan console.log untuk mencetak padanan berubah, kita akan melihat:
[ "<%name%>", " name ", index: 21, input: "<p>Hello, my name is <%name%>. I\'m <%age%> years old.</p>" ]
Tetapi kita dapat melihat bahawa tatasusunan yang dikembalikan hanya mengandungi padanan pertama. Kita perlu membungkus logik di atas dengan gelung sementara supaya kita boleh mendapatkan semua padanan.
var re = /<%([^%>]+)?%>/g; while(match = re.exec(tpl)) { console.log(match); }
Jika anda menjalankan kod di atas, anda akan melihat bahawa <%name%> dan <%age%>
Kini tiba bahagian yang menarik. Selepas mengenal pasti padanan dalam templat, kami perlu menggantikannya dengan data sebenar yang dihantar ke fungsi tersebut. Cara paling mudah ialah menggunakan fungsi ganti. Kita boleh menulisnya seperti ini:
var TemplateEngine = function(tpl, data) { var re = /<%([^%>]+)?%>/g; while(match = re.exec(tpl)) { tpl = tpl.replace(match[0], data[match[1]]) } return tpl; }
Baiklah, jadi saya boleh berlari, tetapi ia tidak cukup bagus. Di sini kami menggunakan objek mudah untuk menghantar data dalam bentuk data["property"], tetapi dalam situasi sebenar kami berkemungkinan memerlukan objek bersarang yang lebih kompleks. Jadi kami mengubah suai objek data sedikit:
{ name: "Krasimir Tsonev", profile: { age: 29 } }
Walau bagaimanapun, jika anda menulisnya secara langsung seperti ini, ia tidak akan berjalan, kerana jika anda menggunakan <%profile.age%> dan hasilnya tidak dapat ditentukan. Dengan cara ini kita tidak boleh hanya menggunakan fungsi ganti, tetapi mesti menggunakan kaedah lain. Adalah lebih baik jika anda boleh menggunakan kod Javascript terus antara <% dan %>, supaya data yang masuk boleh dinilai secara langsung, seperti ini:
Helo, nama saya <%this.name%> saya <%this.profile.age%>';
Anda mungkin ingin tahu, bagaimana ini dicapai? Di sini John menggunakan sintaks Fungsi baharu untuk mencipta fungsi berdasarkan rentetan. Mari kita lihat contoh:
var fn = new Function("arg", "console.log(arg + 1);"); fn(2); // outputs 3
fn ialah fungsi tulen. Ia menerima satu parameter, dan badan fungsi ialah console.log(arg 1);. Kod di atas adalah bersamaan dengan kod berikut:
var fn = function(arg) { console.log(arg + 1); } fn(2); // outputs 3
Dengan kaedah ini, kita boleh membina fungsi daripada rentetan, termasuk parameter dan badan fungsinya. Bukankah ini yang kita mahukan! Tetapi jangan risau, sebelum membina fungsi, mari kita lihat bagaimana badan fungsi itu. Mengikut idea sebelumnya, pulangan terakhir enjin templat ini haruslah templat yang disusun. Masih menggunakan rentetan templat sebelumnya sebagai contoh, kandungan yang dikembalikan harus serupa dengan:
return "<p>Hello, my name is " + this.name + ". I\'m " + this.profile.age + " years old.</p>";
Sudah tentu, dalam enjin templat sebenar, kami akan membahagikan templat kepada kepingan kecil teks dan kod Javascript yang bermakna. Terdahulu anda mungkin pernah melihat saya menggunakan gabungan rentetan ringkas untuk mencapai kesan yang diingini, tetapi ini tidak 100% selaras dengan keperluan kami. Memandangkan pengguna berkemungkinan menghantar kod Javascript yang lebih kompleks, kami memerlukan satu lagi gelung di sini, seperti berikut:
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>' + }
当然,这个代码不能直接跑,跑了会出错。于是我用了John的文章里写的逻辑,把所有的字符串放在一个数组里,在程序的最后把它们拼接起来。
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('');
下一步就是收集模板里面不同的代码行,用于生成函数。通过前面介绍的方法,我们可以知道模板中有哪些占位符(译者注:或者说正则表达式的匹配项)以及它们的位置。所以,依靠一个辅助变量(cursor,游标),我们就能得到想要的结果。
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 } }));
上述代码中的变量code保存了函数体。开头的部分定义了一个数组。游标cursor告诉我们当前解析到了模板中的哪个位置。我们需要依靠它来遍历整个模板字符串。此外还有个函数add,它负责把解析出来的代码行添加到变量code中去。有一个地方需要特别注意,那就是需要把code包含的双引号字符进行转义(escape)。否则生成的函数代码会出错。如果我们运行上面的代码,我们会在控制台里面看见如下的内容:
var r=[]; r.push("<p>Hello, my name is "); r.push("this.name"); r.push(". I'm "); r.push("this.profile.age"); return r.join("");
等等,貌似不太对啊,this.name和this.profile.age不应该有引号啊,再来改改。
var add = function(line, js) { js? code += 'r.push(' + line + ');\n' : code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n'; } while(match = re.exec(tpl)) { add(tpl.slice(cursor, match.index)); add(match[1], true); // <-- say that this is actually valid js cursor = match.index + match[0].length; }
占位符的内容和一个布尔值一起作为参数传给add函数,用作区分。这样就能生成我们想要的函数体了。
var r=[]; r.push("<p>Hello, my name is "); r.push(this.name); r.push(". I'm "); r.push(this.profile.age); return r.join("");
剩下来要做的就是创建函数并且执行它。因此,在模板引擎的最后,把原本返回模板字符串的语句替换成如下的内容:
我们甚至不需要显式地传参数给这个函数。我们使用apply方法来调用它。它会自动设定函数执行的上下文。这就是为什么我们能在函数里面使用this.name。这里this指向data对象。
模板引擎接近完成了,不过还有一点,我们需要支持更多复杂的语句,比如条件判断和循环。我们接着上面的例子继续写。
var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<a href="#"><%this.skills[index]%></a>' + '<%}%>'; console.log(TemplateEngine(template, { skills: ["js", "html", "css"] }));
这里会产生一个异常,Uncaught SyntaxError: Unexpected token for。如果我们调试一下,把code变量打印出来,我们就能发现问题所在。
var r=[]; r.push("My skills:"); r.push(for(var index in this.skills) {); r.push("<a href=\"\">"); r.push(this.skills[index]); r.push("</a>"); r.push(}); r.push(""); return r.join("");
带有for循环的那一行不应该被直接放到数组里面,而是应该作为脚本的一部分直接运行。所以我们在把内容添加到code变量之前还要多做一个判断。
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 += 'r.push("' + line.replace(/"/g, '\\"') + '");\n'; }
这里我们新增加了一个正则表达式。它会判断代码中是否包含if、for、else等等关键字。如果有的话就直接添加到脚本代码中去,否则就添加到数组中去。运行结果如下:
var r=[]; r.push("My skills:"); for(var index in this.skills) { r.push("<a href=\"#\">"); r.push(this.skills[index]); r.push("</a>"); } r.push(""); return r.join("");
当然,编译出来的结果也是对的。
最后一个改进可以使我们的模板引擎更为强大。我们可以直接在模板中使用复杂逻辑,例如:
var template = 'My skills:' + '<%if(this.showSkills) {%>' + '<%for(var index in this.skills) {%>' + '<a href="#"><%this.skills[index]%></a>' + '<%}%>' + '<%} else {%>' + '<p>none</p>' + '<%}%>'; console.log(TemplateEngine(template, { skills: ["js", "html", "css"], showSkills: true }));
除了上面说的改进,我还对代码本身做了些优化,最终版本如下:
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); }
代码比我预想的还要少,只有区区15行!