DTL 語言著力於為靜態文本提供足夠的編程功能,如提供分支和迴圈等決定呈現相關的邏輯。它主要用於分割文檔的表示和資料的字串文本,模板定義了預留位置和各種定義文檔應該如何顯示的基本邏輯(模板標籤“template tag”),模板可用來產生 HTML 或者各種基於文本的格式。Dojo 的 DTL 工具包實現了 DTL 文法的解析,並提供了一系列簡單易用的介面用於接收參數,產生和解析基於 DTL 的各種文本或 HTML 頁面。這樣一來,我們可以基於簡單的 DTL 文法構造 HTML 範本,並基於 Dojo 的 DTL 介面實現代碼的充分複用。這篇文章將重點介紹 Dojo 的 DTL 工具包以及他們的各種使用方式和技巧。
DTL 範本語言簡介
DTL 範本語言是 Django 架構內建的一套包含特殊標籤(tag)和過濾器(filters)的規則集。它包括 autoescape、block、comment、cycle、for、if、ifequal 等等標籤和 add、capfirst、center、first、last、length、lower、safe 等等過濾器。這些標籤和過濾器如同我們的程式設計語言一樣,可以協助我們實現各種各樣的邏輯。Dojo 的 DTL 工具包基於這套規則實現了上述的大部分標籤和過濾器的文法解析功能。所以,我們可以直接在 Dojo 中基於 DTL 範本語言實現我們的各種邏輯。
Dojo 的 DTL 工具包
Dojo 的 DTL 工具包提供了很多介面用於解析我們的“DTL 程式”,它不僅適用於文本的解析,也適用於 HTML 頁面。對於之前已經使用 DTL 開發的工程師們,將已有代碼移植到 Dojo 中是相當方便的,基本是無縫移植。
接下來我們來看看 Dojo 的 DTL 工具包的一些具體對象和介面。
Context 對象
Context 對象是 Dojo 的 DTL 工具包裡用於存放內容的對象,Dojo 通過一些介面將該對象“注入”到模板裡,其實 Context 有點類似於實參。來看一個簡單樣本:
清單 1. 簡單 Context 樣本
var context = new dojox.dtl.Context( {foo: "foo", bar: "bar", get: function(key){ return key + "TEST"; }}); var tpl = new dojox.dtl.Template("{{ foo }}-{{ bar }}"); t.is("fooTEST-barTEST", tpl.render(context));
這裡我們初始化了一個 Context 對象,並定義了它的“get”方法,之後我們用該對象“填充”之後建立的 Template。這裡的“{{ foo }}”和“{{ bar }}”代表“foo”變數和“bar”變數,所以最後的傳回值為"fooTEST-barTEST"。這就是 Context 和 Template 協同的工作模式。
在來看一個內容過濾器的例子:
清單 2. Context 過濾
var context = new dojox.dtl.Context({ foo: "one", bar: "two", baz: "three" }); var filtered = context.filter("foo", "bar"); t.is(filtered.foo, "one"); t.is(filtered.bar, "two"); t.f(filtered.baz);
filtered = context.filter({ bar: true, baz: true }); t.f(filtered.foo); t.is(filtered.bar, "two"); t.is(filtered.baz, "three");
filtered = context.filter(new dojox.dtl.Context({ foo: true, baz: true })); t.is(filtered.foo, "one"); t.f(filtered.bar); t.is(filtered.baz, "three");
這裡我們提供了三種過濾的樣本:
1. “context.filter("foo", "bar")”:直接通過屬性名稱可以過濾掉除“foo”和“bar”以外的所有其它屬性。
2. “context.filter({ bar: true, baz: true })”:也可以通過 true 或者 false 實現過濾。
3. “context.filter(new dojox.dtl.Context({ foo: true, baz: true }))”:建立一個 Context 對象過濾也是可以的。
除了過濾,我們的 Context 對象也可以被擴充:
清單 3. Context 擴充
var context = new dojox.dtl.Context({ foo: "one" }); var extended = context.extend({ bar: "two", baz: "three" }); t.is(extended.foo, "one"); t.is(extended.bar, "two"); t.is(extended.baz, "three"); extended = context.extend({ barr: "two", bazz: "three" }); t.is(extended.foo, "one"); t.f(extended.bar); t.f(extended.baz); t.is(extended.barr, "two"); t.is(extended.bazz, "three"); t.f(context.bar) t.f(context.baz); t.f(context.barr); t.f(context.bazz);
通過“extend”方法,我們可以在已有 Context 對象的基礎上擴充新的屬性。“context.extend({ bar: "two", baz: "three" })”擴充了“bar”和“baz”屬性,其值分別為“two”和“three”。這裡需要注意:原來的 Context 對象(這裡是“context”變數)是沒有任何變化的,改變的只是擴充後的那個變數(這裡是“extended”變數)。
文本模板之標籤(Tag)
介紹完了 Context 對象,我們要來說說模板了。Context 內容對象是和模板(Template)對象協同工作的,並且它們基於 DTL 範本語言。我們先來看一個簡單的文本模板樣本:
清單 4. 簡單文本模板
var dd = dojox.dtl; var template = new dd.Template( '{% extends "../../dojox/dtl/tests/templates/pocket.html" %} {% block pocket %}Simple{% endblock %}'); t.is("Simple Pocket", template.render());
這裡我們建立了一個模板,裡麵包含了“extends”和“block”,所以很明顯這裡是 DTL 裡面的一個簡單的模板繼承文法,用子模板“pocket ”:“{% block pocket %}Simple{% endblock %}”來替代父模板“pocket.html”(“{% block pocket %}Hot{% endblock %} Pocket”)。通過“template.render()”來執行文法的解析,最後的結果為"Simple Pocket",完全符合 DTL 文法的解析結果。
再來看幾個複雜點的例子:
清單 5. 簡單文本模板進階
var dd = dojox.dtl; // 參數傳遞 1 var context = new dd.Context({ parent: "../../dojox/dtl/tests/templates/pocket.html" }) template = new dd.Template(' {% extends parent %}{% block pocket %}Variabled{% endblock %}'); t.is("Variabled Pocket", template.render(context)); // 參數傳遞 2 context.parent = dojo.moduleUrl("dojox.dtl.tests.templates", "pocket.html"); template = new dd.Template(' {% extends parent %}{% block pocket %}Slightly More Advanced{% endblock %}'); t.is("Slightly More Advanced Pocket", template.render(context)); // 參數傳遞 3 context.parent = { url: dojo.moduleUrl("dojox.dtl.tests.templates", "pocket.html") } template = new dd.Template(' {% extends parent %}{% block pocket %}Super{% endblock %}'); t.is("Super Pocket", template.render(context));
這裡列出了三種參數傳遞方式。我們不用像之前那樣將 HTML 檔案的路徑寫在 Template 裡面了,可以通過 Context 的參數傳遞到 Template 裡,通過 moduleUrl 或者擁有 url 屬性的對象傳遞均可。
除了傳遞模板 HTML,Context 也支援其它各種方式的實參傳遞:
清單 6. 文本模板傳參
var dd = dojox.dtl; var context = new dd.Context({ parent: dojo.moduleUrl("dojox.dtl.tests.templates", "pocket2.html"), items: ["apple", "banana", "lemon" ] }); var template = new dd.Template(" {% extends parent %}{% block pocket %}My {{ item }}{% endblock %}"); t.is("(My apple) (My banana) (My lemon) Pocket", template.render(context));
這裡有兩個參數:“parent”和“items”,我們來看看父模板的內容:
清單 7. 父模板 pocket2.html
{% for item in items %}( {% block pocket %} Hot {% endblock %} ) {% endfor %} Pocket
根據 DTL 文法的規則,父模板的 block--“pocket ”會被子模板的 block--“pocket ”替代,然後通過解析 Context 傳入的 items 參數,最後的執行結果應該為:"(My apple) (My banana) (My lemon) Pocket"。
其實,我們也可以通過“block.super”在子模板裡面訪問父模板的內容:“{% extends parent %}{% block pocket %}My {{ item }} {{ block.super }} { % endblock %}”。
當然,DTL 語句也支援注釋:
清單 8. 文本模板注釋
var template = new dd.Template(' Hot{% comment %}<strong>Make me disappear</strong>{% endcomment %} Pocket'); t.is("Hot Pocket", template.render());
通過“{% comment %}”和“{% endcomment %}”來添加註釋,注釋中的代碼不會被執行。
再來看看 Cycle 標籤:
清單 9. 文本模板 Cycle 標籤
var context = new dd.Context({ items: ["apple", "banana", "lemon"], unplugged: "Torrey" }); var template = new dd.Template(" {% for item in items %}{% cycle 'Hot' 'Diarrhea' unplugged 'Extra' %} Pocket. {% endfor %}"); t.is("Hot Pocket. Diarrhea Pocket. Torrey Pocket. ", template.render(context));
Cycle 標籤表示迴圈在列表裡面取值,由於“items”裡面只有三個元素,所以這裡的取值應該是"Hot Pocket. Diarrhea Pocket. Torrey Pocket.",最後的“Extra”不會被取到。
再來看一個關於 Cycle 的稍微複雜一點的例子:
清單 10. 文本模板 Cycle 標籤進階
context = new dojox.dtl.Context({ unplugged: "Torrey" }); template = new dd.Template(" {% cycle 'Hot' 'Diarrhea' unplugged 'Extra' as steakum %} Pocket. {% cycle steakum %} Pocket. {% cycle steakum %} Pocket."); t.is("Hot Pocket. Diarrhea Pocket. Torrey Pocket.", template.render(context));
這裡的 Cycle 我們通過“as steakum”來給這個 Cycle 定義了一個引用,這樣,我們可以在以後的代碼裡重複利用該 Cycle,通過“{% cycle steakum %}”來取它列表裡面的下一個值。
接下來我們介紹一下“filter”標籤:
清單 11. 文本模板 Filter 標籤
var template = new dd.Template(' {% filter lower|center:"15" %}Hot Pocket{% endfilter %}'); t.is(" hot pocket ", template.render());
這裡的 Filter 標籤用于格式化常值內容,“{% filter lower|center:"15" %}”表示將字型轉為小寫,置中並保持 15 個字元(不足補空格)。
再來看看“firstof”標籤:
清單 12. 文本模板 Firstof 標籤
var dd = dojox.dtl; var context = new dd.Context({ found: "unicorn" }); var template = new dd.Template("{% firstof one two three four found %}"); t.is("unicorn", template.render(context)); context.four = null; t.is("null", template.render(context)); context.three = false; t.is("false", template.render(context));
這裡的“firstof”會取得第一個有值的變數,由於只有“found”被賦值,所以這裡的“{% firstof one two three four found %}”應為“unicorn”,當然,如果您手動給 context 變數賦值,該語句的運行結果也會有相應改變。
關於“include”標籤,樣本如下:
清單 13. 文本模板 Include 標籤
var dd = dojox.dtl; var context = new dd.Context({ hello: dojo.moduleUrl("dojox.dtl.tests.templates", "hello.html"), person: "Bob", people: ["Charles", "Ralph", "Julia"] }); var template = new dd.Template("{% include hello %}"); t.is("Hello, <span>Bob</span>", template.render(context));
和 HTML 裡面的“include”標籤一樣,它用來引入外部的常值內容。
清單 14. 文本模板 Spaceless 標籤
var dd = dojox.dtl; var template = new dd.Template("{% spaceless %}<ul> \n <li>Hot</li> \n\n<li>Pocket </li>\n </ul>{% endspaceless %}"); t.is("<ul><li>Hot</li><li>Pocket </li></ul>", template.render());
這裡的“spaceless”標籤用於去除所有空格,只留下非空白字元。
接下來還有各種 Dojo 支援的標籤,如:for,if,ifchanged,ifequal,include,with,withratio 等等,它們的文法與 DTL 一樣,有興趣的讀者可以參考 DTL 的文法介紹,我們這裡不再一一介紹。
文本模板之過濾(Filter)
接下來我們要介紹過濾器,過濾器在 DTL 裡面更多的是一種輔助的操作,我們先來看一個例子:
清單 15. 文本過濾 Add 操作
var dd = dojox.dtl; var context = new dd.Context({ four: 4 }); tpl = new dd.Template('{{ four|add:"6" }}'); t.is("10", tpl.render(context)); context.four = "4"; t.is("10", tpl.render(context)); tpl = new dd.Template('{{ four|add:"six" }}'); t.is("4", tpl.render(context)); tpl = new dd.Template('{{ four|add:"6.6" }}'); t.is("10", tpl.render(context));
注意這裡的“add”操作:“{{ four|add:"6" }} ”,由於“context”裡面指定了“four”的值為“4”,所以這裡的輸出為“10”。
其實這裡的“add”操作相容範圍很廣泛,它會強制轉換相應的操作對象,如果不行,則會按照一種預設的方式實現“加”操作,適用於整數,小數甚至數組或列表型資料。
再來看一個“cut”的樣本
清單 16. 文本過濾 Cut 操作
var dd = dojox.dtl; var context = new dd.Context({ uncut: "Apples and oranges" }); var tpl = new dd.Template('{{ uncut|cut }}'); t.is("Apples and oranges", tpl.render(context)); tpl = new dd.Template('{{ uncut|cut:"A" }}'); t.is("pples and oranges", tpl.render(context)); tpl = new dd.Template('{{ uncut|cut:" " }}'); t.is("Applesandoranges", tpl.render(context)); tpl = new dd.Template('{{ uncut|cut:"e" }}'); t.is("Appls and orangs", tpl.render(context));
"cut"主要用於刪除相關字元,這裡的“{{ uncut|cut:"A" }}”用於刪除“uncut”變數值裡面的所有“A”字元,注意:這裡是區分大小寫。
清單 17. 文本過濾 Default
var dd = dojox.dtl; var context = new dd.Context(); tpl = new dd.Template('{{ empty|default }}'); t.is("", tpl.render(context)); tpl = new dd.Template('{{ empty|default:"full" }}'); t.is("full", tpl.render(context)); context.empty = "not empty"; t.is("not empty", tpl.render(context));
“default”這裡的基本原理是:如果變數有值,則取變數值,否則取“default”的值。
以上是幾個比較簡單的過濾操作,再來看幾個稍微複雜的:
清單 18. 文本過濾 Dictsort
var dd = dojox.dtl; var context = new dd.Context({ fruit: [ { name: "lemons", toString: function(){ return this.name; } }, { name: "apples", toString: function(){ return this.name; } }, { name: "grapes", toString: function(){ return this.name; } } ] }); tpl = new dd.Template('{{ fruit|dictsort|join:"|" }}'); t.is("lemons|apples|grapes", tpl.render(context)); tpl = new dd.Template('{{ fruit|dictsort:"name"|join:"|" }}'); t.is("apples|grapes|lemons", tpl.render(context));
“dictsort”主要用於排序,這裡對“fruit”排序並通過“|”串連起來(join:“|”),所以結果為:“lemons|apples|grapes”。如果有多個屬性,可以指定屬性排序:|dictsort:“name”,即基於“name”屬性排序。
清單 19. 文本過濾 Truncatewords
var dd = dojox.dtl; var context = new dd.Context({ word: "potted meat writes a lot of tests" }); var tpl = new dd.Template("{{ word|truncatewords }}"); t.is(context.word, tpl.render(context)); tpl = new dd.Template('{{ word|truncatewords:"1" }}'); t.is("potted", tpl.render(context)); tpl = new dd.Template('{{ word|truncatewords:"2" }}'); t.is("potted meat", tpl.render(context)); tpl = new dd.Template('{{ word|truncatewords:20" }}'); t.is(context.word, tpl.render(context)); context.word = "potted \nmeat \nwrites a lot of tests"; tpl = new dd.Template('{{ word|truncatewords:"3" }}'); t.is("potted \nmeat \nwrites", tpl.render(context));
顧名思義,“truncatewords”操作主要用於截取文字:“{{ word|truncatewords:"2" }}”相當於截取前兩個文字,所以其結果為“potted meat”,這種操作在我們日常開發中,尤其是頁面排版中經常需要用到。
關於過濾器還有很多很多的 Dojo 支援的操作,如:filesizeformat,fix_ampersands,iriencode,pluralize,removetags,slice,urlencode 等等,它們的文法也與 DTL 一樣,有興趣的讀者可以參考 DTL 的關於 Filter 相關內容,我們這裡也不再一一介紹了。
DOM 模板
之前介紹的一些標籤和過濾器是基於文本的,接下來我們會介紹 Dojo 的 DTL 工具包的針對 DOM 的一些標籤和過濾相關的介面,這些介面會預設您的模板是標準 HTML 格式的,如果不是,它會檢測到並拋出異常。這些介面非常適用於將我們之前寫好的基於 DTL 的 HTML 範本無縫的整合到 Dojo 中來。
清單 20. DOM 模板錯誤偵測
var dd = dojox.dtl; var template; var found = false; try { template = new dd.DomTemplate('No div'); dd.tests.dom.util.render(template); }catch(e){ t.is("Text should not exist outside of the root node in template", e.message); found = true; } t.t(found); t.is("potted meat", tpl.render(context)); template = new dd.DomTemplate('<div></div>extra content'); found = false; try { dd.tests.dom.util.render(template); }catch(e){ t.is("Content should not exist outside of the root node in template", e.message); found = true; } t.t(found);
這裡我們的內容為“No div”,顯然不符合 HTML 的規範,所以 Dojo 的介面會拋出異常:“文本不能在根節點以外存在”。同樣,對於“<div></div>extra content”的情況也是如此。
再來看兩個關於報錯的樣本:
清單 21. DOM 模板錯誤偵測進階
template = new dd.DomTemplate('<div></div><div></div>'); found = false; try { dd.tests.dom.util.render(template); }catch(e){ t.is("Content should not exist outside of the root node in template", e.message); found = true; } t.t(found); template = new dd.DomTemplate('{% if missing %}<div></div>{% endif %}'); found = false; try { dd.tests.dom.util.render(template); }catch(e){ t.is("Rendered template does not have a root node", e.message); found = true; } t.t(found);
可見,“<div></div><div></div>”這種沒有獨立根目錄節點,或者說又多餘一個的根節點的情況也是不被允許的。同樣,“{% if missing %}<div></div>{% endif %}”這種在根節點外存在邏輯控制的情況也是禁止的。
我們可以儘可能的利用 DTL 語言的方便為我們構建“動態”的 HTML 頁面,一下是一個關於元素屬性的樣本:
清單 22. DOM 模板元素屬性
var dd = dojox.dtl; var template = new dd.DomTemplate(' <div>{% for item in items %}<a index="{{forloop.counter0}}" id="id_{{item.param}}">{{item.param}}</a>{% endfor %}</div>'); var context = new dd.Context({ items: [ { name: "apple", param: "appleparam" }, { name: "banana", param: "bananaparam" }, { name: "orange", param: "orangeparam" } ] }); doh.is('<div><a index="0" id="id_appleparam">appleparam</a><a index="1" id="id_bananaparam">bananaparam</a><a index="2" id="id_orangeparam">orangeparam</a></div>', dd.tests.dom.util.render(template, context));
可以看到,這裡的 HTML 元素的各種屬性都是可以通過變數和邏輯構建出來的:“<a index="{{forloop.counter0}}" id="id_{{item.param}}">”就是一個構建“index”屬性和“id”屬性的樣本。
當然,也可以添加自訂的屬性:
清單 23. DOM 模板元素自訂屬性
var dd = dojox.dtl; var context = new dd.Context({frag: {start: 10, stop: 20}}); var template = new dd.DomTemplate(' <div startLine="{{ frag.start }}" stopLine="{{ frag.stop }}">abc</div>'); doh.is(' <div startline="10" stopline="20">abc</div>', dd.tests.dom.util.render(template, context));
注意這裡我們定義的是嵌套的 Context,所以使用的時候也應該通過“{{ frag.start }}”這種方式來使用。
關於 DOM 模板的相關標籤和過濾還有很多,這裡我們不再一一介紹,有興趣的讀者可以參考 Dojo 的 DTL 使用案例。
基於 DTL 模板的 Widget
前面我們主要介紹了如何使用 DTL 的基本介面,接下來我們會介紹一下它的一個進階用法:構建基於 DTL 模板的自訂 Widget。
我們知道,Dojo 的 Widget 通常都有一個 HTML 範本,用來構建該 Widget 的初始基本結構。但是有時我們的 Widget 比較大,比較複雜,這回導致我們的 Widget 的 HTML 範本也比較複雜,由於 Widget 的 HTML 範本是比較“靜態”的,所以如果 Widget 的 HTML 範本一旦複雜,勢必導致我們的模板 HTML 程式碼量的劇增。如果我們能把 DTL 引入到模板中,使我們的模板具有一定的邏輯控制能力,即“動態”的模板,這樣便能大大簡化我們的模板 HTML 程式碼複雜度,使代碼更易於維護,同時也可降低網路請求 HTML 範本需要的頻寬,提高網路效能。
清單 24. 基於 DTL 模板的 Widget
dojo.declare("Fruit", [dijit._WidgetBase, dojox.dtl._DomTemplated], { widgetsInTemplate: true, items: ["apple", "banana", "orange"], keyUp: function(e){ if((e.type == "click" || e.keyCode == dojo.keys.ENTER) && this.input.value){ console.debug(this.button); var i = dojo.indexOf(this.items, this.input.value); if(i != -1){ this.items.splice(i, 1); }else{ this.items.push(this.input.value); } this.input.value = ""; this.render(); } }, templateString: dojo.cache("dojox.dtl.demos.templates", "Fruit.html"), });
大家注意這裡的“items”參數:“items: ["apple", "banana", "orange"]”和“templateString”參數,這裡的“items”變數的內容會被注入到 Widget 的模板“Fruit.html”中,從而構建最終的 Widget 的 HTML 範本。來看看“Fruit.html”的內容:
清單 25. 基於 DTL 的 HTML 範本
<div> <input dojoAttachEvent="onkeyup: keyUp" dojoAttachPoint="input"> <button dojoType="dijit.form.Button" dojoAttachPoint="button" dojoAttachEvent="onClick: keyUp"> Add/Remove Item </button> <div id="pane" dojoType="dijit.layout.ContentPane parsed"> <ul> {% for item in items %} <li> <button dojoType="dijit.form.Button parsed" title="Fruit: {{ item }}" otherAttr2="x_{{item}}"> {{ item }} <script type="dojo/connect" event="onClick" args="e"> console.debug(" You clicked", this.containerNode.innerHTML);</' + 'script> </button> </li> {% endfor %} </ul> </div> </div>
可以看到,這裡的“{% for item in items %}”會去遍曆我們“Fruit”的 Widget 中的“items”成員變數,從而構建“Fruit”這個 Widget 的最終模板。
結束語
這篇文章介紹了 Dojo 中 DTL 工具包的一些特性,首先從 DTL 語言本身入手,介紹了 DTL 語言的出處,基本規則和特點,然後引出 Dojo 的 DTL 工具包,並由淺入深的逐步介紹了 Dojo 的 DTL 工具包的各種對象和介面,如 Context 對象,Template 對象以及 Render 方法等等。從文字標籤,文本過濾器和 DOM 模板三個方面分別闡述了這些介面的特點和使用方式。最後,介紹了如何基於 Dojo 的 DTL 工具包構建基於 DTL 模板的 Widget。這些介面對我們的日常開發都很有協助,建議大家平時可以多關注一下。
本文首發於IBM Developer Works:http://www.ibm.com/developerworks/cn/web/1206_zhouxiang_dojodtl/