テンプレートはデータとプレゼンテーションを分離するため、プレゼンテーションのロジックと効果の保守が容易になります。 JavaScript の Function オブジェクトを使用して、非常にシンプルなテンプレート変換エンジンを段階的に構築します
テンプレートの紹介
テンプレートは通常、ある種の動的プログラミング言語コードが埋め込まれたテキストを指し、データとテンプレートを何らかの形式で組み合わせることで、さまざまな結果を生成できます。テンプレートは通常、表示フォームを定義するために使用されます。これにより、データの表示がより充実し、保守が容易になります。たとえば、テンプレートの例は次のとおりです:
<ul> <% for(var i in items){ %> <li class='<%= items[i].status %>'><%= items[i].text %></li> <% } %> </ul>
以下の項目データがある場合:
items:[ { text: 'text1' ,status:'done' }, { text: 'text2' ,status:'pending' }, { text: 'text3' ,status:'pending' }, { text: 'text4' ,status:'processing' } ]
これを何らかの方法で組み合わせると、次の HTML コードを生成できます。
<ul> <li class='done'>text1<li> <li class='pending'>text2<li> <li class='pending'>text3<li> <li class='processing'>text4<li> </ul>
テンプレートを使用せずに同じ効果を実現したい場合、つまり結果として上記のデータを表示したい場合は、次の手順を実行する必要があります:
var temp = '<ul>'; for(var i in items){ temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>"; } temp += '</ul>';
テンプレートを使用すると次の利点があることがわかります:
簡略化された HTML 記述
プログラミング要素 (ループや条件分岐など) を通じてデータのプレゼンテーションをより詳細に制御できます
データと表示を分離し、表示ロジックと効果のメンテナンスを容易にします
テンプレート エンジン
データとテンプレートを組み合わせて、テンプレートを解析して最終的な結果を出力するプログラムをテンプレートエンジンと呼びます。テンプレートには多くの種類があり、対応するテンプレートエンジンも多数あります。古いテンプレートは ERB と呼ばれ、ASP.NET、Rails などの多くの Web フレームワークで使用されています。上記の例は ERB の例です。 ERB には、評価と補間という 2 つの中心的な概念があります。表面的には、evaluate は <% %> に含まれる部分を指し、interpolate は <%= %> に含まれる部分を指します。テンプレート エンジンの観点から見ると、evaluate の部分は結果に直接出力されず、通常はプロセス制御に使用されますが、interpolate の部分は結果に直接出力されます。
テンプレート エンジンの実装の観点から見ると、実装を簡素化し、パフォーマンスを向上させるために、プログラミング言語の動的コンパイルまたは動的解釈機能に依存する必要があります。例: ASP.NET は、.NET の動的コンパイルを使用してテンプレートを動的クラスにコンパイルし、リフレクションを使用してクラス内のコードを動的に実行します。 C# は静的プログラミング言語であるため、この実装は実際にはより複雑ですが、JavaScript を使用すると、Function を使用して、非常に少ないコードで単純なテンプレート エンジンを実装できます。この記事では、JavaScript の威力を示すために、単純な ERB テンプレート エンジンを実装します。
テンプレートテキスト変換
上記の例について、テンプレートを使用する場合と使用しない場合の違いを確認してください。
テンプレートの作成:
<ul> <% for(var i in items){ %> <li class='<%= items[i].status %>'><%= items[i].text %></li> <% } %> </ul>
非テンプレートの記述:
var temp = '<ul>'; for(var i in items){ temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>"; } temp += '</ul>';
注意深く見てみると、この 2 つの方法は実際には非常に「似ており」、ある意味 1 対 1 対応していることがわかります。テンプレートのテキストを実行用のコードに変換できれば、テンプレートの変換を実現できます。変換プロセスには 2 つの原則があります:
通常のテキストに遭遇すると、文字列に直接連結されます
補間 (つまり、<%= %>) が発生すると、コンテンツは変数として扱われ、文字列
に結合されます。
評価 (つまり、<% %>) が発生すると、コード
として直接処理されます。
上記の原則に従って上記の例を変換し、一般的な関数を追加します:
var template = function(items){ var temp = ''; //开始变换 temp += '<ul>'; for(var i in items){ temp += "<li class='" + items[i].status + "'>" + items[i].text + "</li>"; } temp += '</ul>'; }
最後にこの関数を実行し、データパラメータを渡します:
var result = template(items);
JavaScript 動的関数
上記の変換ロジックは実際には非常に単純であることがわかりますが、重要な問題はテンプレートが変更されることです。つまり、生成されるプログラム コードも実行時に生成され、実行される必要があります。幸いなことに、JavaScript には多くの動的機能があり、その 1 つが Function です。 通常、js で関数を宣言するには function キーワードを使用しますが、Function が使用されることはほとんどありません。 js では、function はリテラル構文です。js のランタイムはリテラル関数を Function オブジェクトに変換するため、Function は実際にはより低レベルで柔軟なメカニズムを提供します。
Function クラスを使用して関数を直接作成するための構文は次のとおりです:
var function_name = new Function(arg1, arg2, ..., argN, function_body)
例:
//创建动态函数 var sayHi = new Function("sName", "sMessage", "alert(\"Hello \" + sName + sMessage);"); //执行 sayHi('Hello','World');
関数本体とパラメータの両方を文字列を通じて作成できます。とてもクールです!この機能を使用すると、テンプレート テキストを関数本体の文字列に変換できるため、動的関数を作成して動的に呼び出すことができます。
実装のアイデア
最初に正規表現を使用して補間と評価を記述し、括弧を使用してキャプチャをグループ化します。
var interpolate_reg = /<%=([\s\S]+?)%>/g; var evaluate_reg = /<%([\s\S]+?)%>/g;
テンプレート全体を継続的に照合するために、これら 2 つの正規表現はマージされますが、interpolate に一致するすべての文字列は評価に一致するため、interpolate の優先順位を高くする必要があることに注意してください。
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g
テンプレートを変換する関数を設計します。入力パラメーターはテンプレートのテキスト文字列とデータ オブジェクトです
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ ... }
使用replace方法,进行正则的匹配和“替换”,实际上我们的目的不是要替换interpolate或evaluate,而是在匹配的过程中构建出“方法体”:
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>/g //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ var index = 0;//记录当前扫描到哪里了 var function_body = "var temp = '';"; function_body += "temp += '"; text.replace(matcher,function(match,interpolate,evaluate,offset){ //找到第一个匹配后,将前面部分作为普通字符串拼接的表达式 function_body += text.slice(index,offset); //如果是<% ... %>直接作为代码片段,evaluate就是捕获的分组 if(evaluate){ function_body += "';" + evaluate + "temp += '"; } //如果是<%= ... %>拼接字符串,interpolate就是捕获的分组 if(interpolate){ function_body += "' + " + interpolate + " + '"; } //递增index,跳过evaluate或者interpolate index = offset + match.length; //这里的return没有什么意义,因为关键不是替换text,而是构建function_body return match; }); //最后的代码应该是返回temp function_body += "';return temp;"; }
至此,function_body虽然是个字符串,但里面的内容实际上是一段函数代码,可以用这个变量来动态创建一个函数对象,并通过data参数调用:
var render = new Function('obj', function_body); return render(data);
这样render就是一个方法,可以调用,方法内部的代码由模板的内容构造,但是大致的框架应该是这样的:
function render(obj){ var temp = ''; temp += ... ... return temp; }
注意到,方法的形参是obj,所以模板内部引用的变量应该是obj:
<script id='template' type='javascript/template'> <ul> <% for(var i in obj){ %> <li class="<%= obj[i].status %>"><%= obj[i].text %></li> <% } %> </ul> </script>
看似到这里就OK了,但是有个必须解决的问题。模板文本中可能包含\r \n \u2028 \u2029等字符,这些字符如果出现在代码中,会出错,比如下面的代码是错误的:
temp += ' <ul> ' + ... ;
我们希望看到的应该是这样的代码:
temp += '\n \t\t<ul>\n' + ...;
这样需要把\n前面的转义成\即可,最终变成字面的\\n。
另外,还有一个问题是,上面的代码无法将最后一个evaluate或者interpolate后面的部分拼接进来,解决这个问题的办法也很简单,只需要在正则式中添加一个行尾的匹配即可:
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g;
相对完整的代码
var matcher = /<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g //模板文本中的特殊字符转义处理 var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\t': 't', '\u2028': 'u2028', '\u2029': 'u2029' }; //text: 传入的模板文本字串 //data: 数据对象 var template = function(text,data){ var index = 0;//记录当前扫描到哪里了 var function_body = "var temp = '';"; function_body += "temp += '"; text.replace(matcher,function(match,interpolate,evaluate,offset){ //找到第一个匹配后,将前面部分作为普通字符串拼接的表达式 //添加了处理转义字符 function_body += text.slice(index,offset) .replace(escaper, function(match) { return '\\' + escapes[match]; }); //如果是<% ... %>直接作为代码片段,evaluate就是捕获的分组 if(evaluate){ function_body += "';" + evaluate + "temp += '"; } //如果是<%= ... %>拼接字符串,interpolate就是捕获的分组 if(interpolate){ function_body += "' + " + interpolate + " + '"; } //递增index,跳过evaluate或者interpolate index = offset + match.length; //这里的return没有什么意义,因为关键不是替换text,而是构建function_body return match; }); //最后的代码应该是返回temp function_body += "';return temp;"; var render = new Function('obj', function_body); return render(data); }
调用代码可以是这样:
<script id='template' type='javascript/template'> <ul> <% for(var i in obj){ %> <li class="<%= obj[i].status %>"><%= obj[i].text %></li> <% } %> </ul> </script> ... var text = document.getElementById('template').innerHTML; var items = [ { text: 'text1' ,status:'done' }, { text: 'text2' ,status:'pending' }, { text: 'text3' ,status:'pending' }, { text: 'text4' ,status:'processing' } ]; console.log(template(text,items));
可见,我们只用了很少的代码就实现了一个简易的模板。
遗留的问题
还有几个细节的问题需要注意: