轉自:http://msdn.microsoft.com/zh-cn/magazine/hh975379.aspx
盡管模板很強大,但有時模板引擎提供的現成標准功能無法滿足您的需求。 您可能要轉換數據、定義自定義幫助程序函數或創建您自己的標記。 值得高興的是,您可以使用 JsRender 的核心功能執行所有這些操作以及更多操作。
我的四月專欄 (msdn.microsoft.com/magazine/hh882454) 介紹了 JsRender 模板庫的基本功能。 本專欄將在更多方案中繼續探討 JsRender,例如呈現外部模板、使用 {{for}} 標記更改上下文以及使用復雜表達式。 此外,我還將演示如何使用某些更強大的 JsRender 功能,包括創建自定義標記、轉換器和上下文幫助程序以及允許自定義代碼。 可以從 archive.msdn.microsoft.com/mag201205ClientInsight 下載所有代碼示例,並可以從 bit.ly/ywSoNu 下載 JsRender。
{{for}} 變體
您可以通過多種方法使 {{for}} 標記成為理想的解決方案。 在上月的專欄中,我演示了 {{for}} 標記如何使用塊幫助循環訪問數組,以及如何一次性循環訪問多個對象:
- <!-- looping {{for}} -->
- {{for students}}
- {{/for}}
- <!-- combo iterators {{for}} -->
- {{for teachers students staff}}
- {{/for}}
通過將塊內容替換為外部模板(可通過聲明為 tmpl 屬性的方式指向此模板),可以將 {{for}}(或任何塊標記)從塊標記(包含內容)轉換為自結束標記。 此標記隨后將呈現外部模板而非內聯內容。
這樣,您便可以輕松對模板采用模塊化方法,即,可以在不同位置重用模板標記,並組織和編寫模板:
- <!-- self closing {{for}} -->
- {{for lineItems tmpl="#lineItemsDetailTmpl" /}}
由於數據很少是平面的,因此鑽入和鑽出對象層次結構就成為模板的一個重要功能。 在上月的專欄中,我演示了使用點標記和方括號鑽入對象層次結構的核心技術,但您也可以使用 {{for}} 標記幫助減少代碼。 如果您有一個對象結構,您要鑽入對象層次結構中並需要呈現子對象中的一組屬性,此優點將變得更加明顯。 例如,在呈現人員對象的 address 時,您可能通過以下方式編寫模板(在該方式中,路徑中的“address”一詞多次重復):
- <div>{{:address.street1}}</div>
- <div>{{:address.street2}}</div>
- <div>{{:address.city}}, {{:address.state}} {{:address.postalCode}}</div>
由於無需重復 address 對象,因此 {{for}} 可以顯著簡化用於呈現地址的代碼,如下所示:
- <!-- "with" {{for}} -->
- {{for address}}
- <div>{{:street1}}</div>
- <div>{{:street2}}</div>
- <div>{{:city}}, {{:state}} {{:postalCode}}</div>
- {{/for}}
{{for}} 作用於 address 屬性,后者是包含屬性的單個對象而非對象數組。 如果 address 為 true(包含某個非 false 值),則將呈現 {{for}} 塊的內容。 {{for}} 還會將當前的數據上下文從 person 對象變為 address 對象;就此看來,它的用法類似於許多庫和語言所擁有的“with”命令。 因此,在上面的示例中,{{for}} 標記可將數據上下文更改為地址,然后呈現模板內容一次(因為只有一個地址)。 如果此人沒有地址(address 屬性為空或未定義),則不會呈現任何內容。 這使得 {{for}} 塊非常適合於包含只應在特定環境中顯示的模板。 以下示例(源自隨附的代碼下載中的文件 08-for-variations.html)演示了此代碼示例如何使用 {{for}} 顯示存在的價格信息:
- {{for pricing}}
- <div class="text">${{:salePrice}}</div>
- {{if fullPrice !== salePrice}}
- <div class="text highlightText">PRICED TO SELL!</div>
- {{/if}}
- {{/for}}
外部模板
代碼重用是使用模板的一大優勢。 如果模板在其應用於的同一頁中的 <script> 標記內部定義,則模板將不再像原本那樣可以重用。 應可以從多個頁面訪問的模板可在其自身的文件中創建,並可以根據需要進行檢索。 JavaScript 和 jQuery 用於簡化從外部文件中檢索模板,而 JsRender 用於簡化模板的呈現。
我喜歡對外部模板使用的一個約定是在文件名前面加上一個下划線前綴,這是部分視圖的通用命名約定。 我還喜歡為所有模板文件加上 .tmpl.html 后綴。 .tmpl 表示它是一個模板,而 .html 擴展名僅僅是為了便於 Visual Studio 等開發工具確定此模板包含 HTML。 圖 1 顯示了外部模板的呈現代碼。
圖 1 外部模板的呈現代碼
- my.utils = (function () {
- var
- formatTemplatePath = function (name) {
- return "/templates/_" + name + ".tmpl.html";
- },
- renderTemplate = function (tmplName, targetSelector, data) {
- var file = formatTemplatePath(tmplName);
- $.get(file, null, function (template) {
- var tmpl = $.templates(template);
- var htmlString = tmpl.render(data);
- if (targetSelector) {
- $(targetSelector).html(htmlString);
- }
- return htmlString;
- });
- };
- return {
- formatTemplatePath: formatTemplatePath,
- renderExternalTemplate: renderTemplate
- };
- })()
從外部文件中檢索模板的方法之一是編寫一個可供 Web 應用程序中的 JavaScript 調用的實用工具函數。 在圖 1 中請注意,my.utils object 中的 renderExternalTemplate 函數首先使用 $.get 函數檢索模板。 調用完成時,將使用 $.templates 函數通過響應內容創建 JsRender 模板。 最后,將使用模板的 render 函數呈現模板,並在目標中顯示最終的 HTML。 此代碼可通過以下代碼調用,其中的模板名稱、DOM 目標和數據上下網將傳遞給自定義的 renderExternalTemplates 函數:
- my.utils.renderExternalTemplate("medMovie", "#movieContainer", my.vm);
此示例的外部模板位於 _medMovie.tmpl.html 示例文件中,並只包含 HTML 和 JsRender 標記。 該模板未使用 <script> 標記封裝。 我之所以喜歡對外部模板使用此技術是因為開發環境將能夠確定內容為 HTML,這將使代碼編寫不易出錯,因為 IntelliSense 是現成可用的。 然而,此文件可能包含多個模板,每個模板均封裝在 <script> 標記中並被指定一個 id 作為其唯一標識符。 這只是處理外部模板的另一種方法。 最后的結果如圖 2 所示。
圖 2 外部模板的呈現結果
視圖路徑
JsRender 提供了多個特殊的視圖路徑來簡化對當前視圖對象的訪問。 #view 用於訪問當前視圖,#data 用於訪問視圖的當前數據上下文,#parent 用於向上遍歷對象層次結構,而 #index 用於返回索引屬性:
- <div>{{:#data.section}}</div>
- <div>{{:#parent.parent.data.number}}</div>
- <div>{{:#parent.parent.parent.parent.data.name}}</div>
- <div>{{:#view.data.section}}</div>
使用視圖路徑(除 #view 以外)時,這些路徑已作用於當前視圖。 換言之,以下路徑是等效的:
- #data
- #view.data
在導航對象層次結構 - 例如,客戶以及訂單和訂單明細或存儲位置中的數據倉庫內的電影(如代碼下載示例文件 11-view-paths.html 所示)時,視圖路徑將很有用。
表達式
通用表達式是邏輯的重要部分,並在決定如何呈現模板時很有用。 JsRender 支持通用表達式,其中包括(但不限於)圖 3 中顯示的表達式。
圖 3 JsRender 中的通用表達式
表達式 | 示例 | 注釋 |
+ | {{ :a + b }} | 加法 |
- | {{ :a - b }} | 減法 |
* | {{ :a * b }} | 乘法 |
/ | {{ :a / b }} | 除法 |
|| | {{ :a || b }} | 邏輯或 |
&& | {{ :a && b }} | 邏輯與 |
! | {{ :!a }} | 求反 |
? : | {{ :a === 1 ? b * 2: c * 2 }} | 三元表達式 |
( ) | {{ :(a||-1) + (b||-1) }} | 使用圓括號的階運算 |
% | {{ :a % b }} | 取模運算 |
<= 和 >= 以及 < 和 > | {{ :a <= b }} | 比較運算 |
=== 和 !== | {{ :a === b }} | 等式和不等式 |
JsRender 支持表達式計算,但不支持表達式賦值和隨機代碼的運行。 這可以防止表達式執行變量賦值或其他操作,例如打開警報窗口。 表達式的目的是計算表達式,然后呈現結果、根據結果采取操作或在其他運算中使用此結果。
例如,使用 JsRender 執行 {{:a++}} 將產生錯誤,因為該表達式嘗試將變量值遞增。 此外,執行 {{:alert(‘hello’)}} 也會產生錯誤,因為該表達式嘗試調用不存在的函數 #view.data.alert。
注冊自定義標記
JsRender 提供了多個強大的擴展點,例如自定義標記、轉換器、幫助程序函數以及模板參數。 用於調用其中的每個擴展點的語法如下所示:
- {{myConverter:name}}
- {{myTag name}}
- {{:~myHelper(name)}}
- {{:~myParameter}}
這些語法分別用於不同的用途;但根據相應的情況,它們可能略有重疊。 在介紹如何在這些語法之間進行選擇之前,務必了解一下每個語法的功能及其定義方法。
如果需要呈現的內容擁有“控件風格”的功能並可以獨立時,則使用自定義標簽比較適合。 例如,可以使用數據將星級評定僅呈現為數字,如下所示:
- {{:rating}}
但使用 JavaScript 邏輯通過 CSS 和一系列空白及已填充的星級圖像來呈現星級評定可能會更好:
- {{createStars averageRating max=5/}}
用於創建星級的邏輯部分可以(並且應該)與表示部分分開。 JsRender 提供了一種方法來創建封裝此功能的自定義標記。 圖 4 中的代碼定義了一個名為 createStars 的自定義標記,並在 JsRender 中進行了注冊,以便該標記能夠在加載此腳本的任何頁面中使用。 使用此自定義標記需要在頁面中包含其 JavaScript 文件,即示例代碼中的 jsrender.tag.js。
圖 4 創建自定義標記
- $.views.tags({
- createStars: function (rating) {
- var ratingArray = [], defaultMax = 5;
- var max = this.props.max || defaultMax;
- for (var i = 1; i <= max; i++) {
- ratingArray.push(i <= rating ?
- "rating fullStar" : "rating emptyStar");
- }
- var htmlString = "";
- if (this.tmpl) {
- // Use the content or the template passed in with the template property.
- htmlString = this. renderContent(ratingArray);
- } else {
- // Use the compiled named template.
- htmlString = $.render.compiledRatingTmpl(ratingArray);
- }
- return htmlString;
- }
自定義標記可能有聲明屬性,例如前面所示的 {{createStars}} 的 max=5 屬性。 可通過 this.props 在代碼中訪問這些屬性。 例如,以下代碼注冊了一個名為 sort 並接受數組(如果名為 reverse 的屬性設置為 true,即 {{sort array reverse=true/}},該數組將以相反順序返回)的自定義標記:
- $.views.tags({
- sort: function(array){
- var ret = "";
- if (this.props.reverse) {
- for (var i = array.length; i; i--) {
- ret += this.tmpl.render(array[i - 1]);
- }
- } else {
- ret += this.tmpl.render(array);
- }
- return ret;
- }}
一個有效的經驗法則是,當您需要呈現略微復雜一些的內容(例如 createStars 或 sort 標記)並且此內容可以重用時,請使用自定義標記。 自定義標記不太適合一次性方案。
轉換器
自定義標記適合於創建內容,而轉換器更適合於將源值轉換為其他值這一簡單任務。 轉換器可以將源值(例如布爾值 true 或 false)更改為完全不同的值(例如分別更改為綠色或紅色)。 例如,以下代碼將使用 priceAlert 轉換器返回一個字符串,該字符串包含一個基於 salePrice 值的價格提示:
- <div class="text highlightText">{{priceAlert:salePrice}}</div>
轉換器也非常適合於更改 URL,如下所示:
- <img src="{{ensureUrl:boxArt.smallUrl}}" class="rightAlign"/>
在以下示例中,ensureUrl 轉換器應將 boxArt.smallUrl 值轉換為限定的 URL(文件 12-converters.html 中同時使用了這兩個轉換器,並使用 JsRender $.views.converters 函數在 jsrender.helpers.js 中注冊了兩者):
- $.views.converters({
- ensureUrl: function (value) {
- return (value ? value : "/images/icon-nocover.png");
- },
- priceAlert: function (value) {0
- return (value < 10 ? "1 Day Special!" : "Sale Price");
- }
- });
轉換器適用於以非參數化方式將數據轉換為呈現值。 如果方案調用參數,則幫助程序函數或自定義標記要比轉換器更適合。 正如我們在前面所看到的,自定義標記允許命名參數,因此 createStars 標記可以使用參數來定義星級的大小、顏色、對星級應用的 CSS 類等。 此處要強調的重點是,轉換器適用於簡單轉換,而自定義標記適用於更復雜的完整呈現。
幫助程序函數和模板參數
您可以通過兩種方法傳入幫助程序函數或參數,以便在模板呈現過程中使用。 一種方法是使用 $.views.helpers 注冊它們,該方法類似於注冊標記或轉換器:
- $.views.helpers({
- todaysPrices: { unitPrice: 23.40 },
- extPrice:function(unitPrice, qty){
- return unitPrice * qty;
- }
- });
這段代碼將使它們可用於應用程序中的所有模板。 另一種方法是將它們作為呈現調用中的選項傳入:
- $.render.myTemplate( data, {
- todaysPrices: { unitPrice: 23.40 },
- extPrice:function(unitPrice, qty){
- return unitPrice * qty;
- }
- });
這段代碼使它們只能在該特定模板呈現調用的上下文中可用。 無論采用哪種方法,都可以通過為參數或函數名(或路徑)加上“~”前綴在模板中訪問幫助程序:
- {{: ~extPrice(~todaysPrices.unitPrice, qty) }}
幫助程序函數幾乎無所不能,包括轉換數據、執行計算、運行應用程序邏輯、返回數組或對象,甚至返回模板。
例如,可以創建一個名為 getGuitars 的幫助程序函數以搜索產品數組並查找吉他產品。 該函數還接受表示吉他類型的參數。 隨后,可以使用結果呈現單個值或循環訪問生成的數組(因為幫助程序函數可以返回任何內容)。 以下代碼可獲取由所有木吉他產品組成的數組,並使用 {{for}} 塊循環訪問這些產品:
- {{for ~getGuitars('acoustic')}} ... {{/for}}
幫助程序函數還可以調用其他幫助程序函數,例如使用訂單的明細項目數組並應用折扣率和稅率來計算總價:
- {{:~totalPrice(~extendedPrice(lineItems, discount), taxRate}}
要定義可供多個模板訪問的幫助程序函數,可以將包含幫助程序函數的對象文字傳遞給 JsRender $.views.helpers 函數。 以下示例定義了 concat 函數以連接多個參數:
- $.views.helpers({
- concat:function concat() {
- return "".concat.apply( "", arguments );
- }
- })
可以使用 {{:~concat(first, age, last)}} 調用 concat 幫助程序函數。 假設可以訪問第一個參數、中間參數和最后一個參數的值,並且分別為 John、25 和 Doe,則將呈現值 John25Doe。
適用於獨特方案的幫助程序函數
您可能遇到這樣的情況:您希望將某個幫助程序函數用於特定模板,但不希望在其他模板中重用此函數。 例如,購物車模板可能需要一個特定於此模板的計算。 幫助程序函數可以執行此計算,但無需使其可以由所有模板訪問。 JsRender 利用前面提到的第二種方法支持此方案 — 使用呈現調用中的選項傳入此函數:
- $.render.shoppingCartTemplate( data, {
- todaysPrices: { unitPrice: 23.40 },
- extPrice:function(unitPrice, qty){
- return unitPrice * qty;
- }
- });
本示例中將呈現購物車模板,此模板計算所需的幫助程序函數和模板參數將直接通過呈現調用提供。 此處的要點是,此示例中的幫助程序函數僅在呈現該特定模板期間存在。
使用哪個功能?
JsRender 提供了多種可選方法,使您可以通過轉換器、自定義標記和幫助程序函數創建強大的模板,但您必須了解每個功能的適用場合。 一個有效的經驗法則是使用圖 5 中顯示的決策樹,該決策樹概括了如何確定要使用這些功能中的哪一個。
圖 5 用於選擇正確幫助程序的決策樹
- if (youPlanToReuse) {
- if (simpleConversion && !parameters){
- // Register a converter.
- }
- else if (itFeelsLikeAControl && canBeSelfContained){
- // Register a custom tag.
- }
- else{
- // Register a helper function.
- }
- }
- else {
- // Pass in a helper function with options for a template.
- }
如果函數只使用一次,則無需使其在整個應用程序中均可訪問,從而避免額外的開銷。 這種情況下比較適合使用在需要時傳入的“一次性”幫助程序函數。
允許嵌入代碼
某些情況下,在模板內部編寫自定義代碼可能更簡單。 JsRender 允許嵌入代碼,但建議您僅當其他方法均告失敗時才使用此方法,這是因為此類代碼混合了表示和行為,因此很難維護。
通過使用帶有星號 {{* }} 前綴的塊封裝代碼並將 allowCode 設置為 true,可以將代碼嵌入到模板內部。 例如,名為 myTmpl 的模板(如圖 6 所示)嵌入了代碼,以便計算在一系列語言中呈現命令或單詞“and”的正確位置。 整個示例可以在文件 13-allowcode.html 中找到。 盡管邏輯並不是很復雜,但可能很難在模板中讀取代碼。
除非 allowCode 屬性設置為 true(默認值為 false),否則 JsRender 將不允許執行代碼。 以下代碼定義了名為 movieTmpl 的已編譯模板,在腳本標記中為此模板分配了標記(如圖 6 所示),並指示它應允許在模板中嵌入代碼:
- $.templates("movieTmpl", {
- markup: "#myTmpl",
- allowCode: true
- });
- $("#movieRows").html(
- $.render.movieTmpl(my.vm.movies)
- );
創建模板后,便會呈現此模板。 allowCode 功能可能導致代碼難以讀取,在某些情況下,幫助程序函數可以解決此問題。 例如,圖 6 中的示例使用 JsRender 的 allowCode 功能在需要的位置添加逗號和單詞“and”。 然而,此項工作也可以通過創建幫助程序函數完成:
- $.views.helpers({
- languagesSeparator: function () {
- var view = this;
- var text = "";
- if (view.index === view.parent.data.length - 2) {
- text = " and";
- } else if (view.index < view.parent.data.length - 2) {
- text = ",";
- }
- return text;
- }
- })
圖 6 允許在模板中嵌入代碼
- <script id="myTmpl" type="text/x-jsrender">
- <tr>
- <td>{{:name}}</td>
- <td>
- {{for languages}}
- {{:#data}}{{*
- if ( view.index === view.parent.data.length - 2 ) {
- }} and {{*
- } else if ( view.index < view.parent.data.length - 2 ) {
- }}, {{* } }}
- {{/for}}
- </td>
- </tr>
- </script>
通過為此 languagesSeparator 幫助程序函數的名稱加上“~”前綴,可以調用此函數。這使得調用幫助程序的模板代碼更易於讀取,如下所示:
- {{for languages}}
- {{:#data}}{{:~languagesSeparator()}}
- {{/for}}
通過將邏輯移動至幫助程序函數,刪除了模板中的行為並將此行為移動至遵循良好分離模式的 JavaScript 中。
性能和靈活性
除了呈現屬性值以外,JsRender 還提供了各種其他功能,包括支持復雜表達式、使用 {{for}} 標記循環訪問和更改上下文,以及用於導航上下文的視圖路徑。 JsRender 還提供了多種方法,用於根據需要添加自定義標記、轉換器和幫助程序以擴展其功能。 這些功能以及基於純字符串的模板編寫方法可幫助 JsRender 獲益於優良的性能,並使其非常靈活。
John Papa 曾任 Microsoft Silverlight 和 Windows 8 團隊推廣專家,他主持的“Silverlight 電視秀”節目深受觀眾歡迎。 他在全球參與了 BUILD、MIX、PDC、TechEd、Visual Studio Live! 和 DevConnections 活動的主題演講和研討會。 Papa 同時也是 Microsoft 區域總監、Visual Studio 雜志的專欄作家 (Papa's Perspective) 以及 Pluralsight 培訓視頻作者。 有關他的情況,請訪問 Twitter 上的 twitter.com/john_papa。
衷心感謝以下技術專家對本文的審閱: Boris Moore