模板,從服務端到客戶端
英文原文: Client-Side Templating
在瀏覽器中使用模板是一個日漸熱門的趨勢。將服務端的邏輯應用到客戶端上,還有越來越多的類MVC模式(模型-視圖-控制器:model-view-controller)的使用都使得在瀏覽器中“模板”的角色越來越重要。在過去,“模板”從來都是服務端的事情,但事實上在客戶端開發中,模板的作用是非常強大又具有表現力的。
為什么要使用模板?
大體上來說,借助模板是一種能很好地將視圖(views)中標記和邏輯分開的方法,還能將代碼的重用性和可維護性最大化。如果使用的是語法與最終所得結果很相近的語言(比如HTML),你就能又快又好地把任務完成了。雖然模板可以用來輸出任何形式的文本,但由于我們想要討論的客戶端開發是有關于HTML的,所以在這篇文章里,我們還是以HTML作為例子。
現在的動態應用中,客戶端常常需要頻繁地刷新界面。這個效果可以通過服務端將HTML片段插入到客戶端的文檔中。這樣做的話,服務器要能支持傳送HTML的片段(與之相對:傳送完整的頁面)。還有就是,作為一個要處理這些標記片段的客戶端的開發者,你應該會想能完全控制你的模板。而模板引擎(Smarty)、流量(Velocity)還有ASP這些服務器端的內容你都不用了解,也不用管那些“面條式代碼”(spaghetti code):例如在HTML文檔里是不是出現的臭名昭著的<?或者<%。
那么現在來看看客戶端模板吧。
第一印象
對初學者而言,理解“模板”的含義很重要,foldoc(免費在線計算機詞典)中的解釋是:模板是一種文檔,不過文檔中有形參,再通過模板處理系統的特定語法用實參代替形參。
讓我們來看看最基本的模板長什么樣子:
<h1>{{title}}</h1> <ul> {{#names}} <li>{{name}}</li> {{/names}} </ul>
如果你寫過HTML,那么你一定很熟悉上面的代碼。上文的HTML中有一些占位符。這些占位符將會被真實的數據取代。例如這個對象:
var data = { "title": "Story", "names": [ {"name": "Tarzan"}, {"name": "Jane"} ] }
把數據和模板結合起來,就會得到下面的HTML代碼:
<h1>Story</h1> <ul> <li>Tarzan</li> <li>Jane</ul> </ul>
將模板和數據分離開來對于維護HTML來說是一件好事。比如說,如果想要更改標簽或者添加類(class)就只需要更改模板就可以了。另外,對于需要迭代出現的元素(比如<li>),程序員只需要寫一次就好了。
模板引擎
模板的語法是根據你需要的模板引擎來決定的(例如:占位符{{title}})。引擎是負責分析模板,用提供的數據替換占位符(變量、函數、循環等等)。
有些模板引擎看起來沒有什么邏輯性。這指的不是在模板中只能插入簡單的占位符,而是說智能標簽(intelligent tags)方面的特性很少(比如數組迭代器,條件渲染等等)。有些引擎就有很多特性和很好的可擴展性。關于這一點就不在這展開講了,你需要問問自己,在模板中你是否需要、需要多少邏輯。
每個模板引擎都有自己的API,不過通常你都能找到像render()和compile()這樣的方法。渲染的過程就是將真正的數據放入模板然后呈現出來。也就是說,渲染就是用真正的數據替代了占位符。如果在此期間木板上有什么邏輯,就會被執行。編譯模板指的是解析模板,然后將它轉換成一個JavaScript函數。模板中的邏輯都會被解釋為純JS(plain JavaScript),給定的數據會被傳入這些JS函數中,這么做可以最大程度地優化HTML。
Mustache實例
上文中的例子可以借助模板引擎實現,例如使用了Mustache模板語法的mustache.js。關于這種語法更多信息,我會在后面告訴你的。現在先來看看下面的JS代碼能得到什么效果:
var template = '<h1>{{title}}</h1><ul>{{#names}}<li>{{name}}</li>{{/names}}</ul>'; var data = {"title": "Story", "names": [{"name": "Tarzan"}, {"name": "Jane"}]}; var result = Mustache.render(template, data);
現在我們需要在頁面上顯示模板,你需要寫這么一行代碼:
document.body.innerHTML = result;
第一個客戶端模板就完成了!在代碼文件中加入下面這句,你就可以試一試上面的例子了,或者看下在線演示
<script src="https://raw.github.com/janl/mustache.js/master/mustache.js"></script>
組織模板
如果你和我一樣,不喜歡HTML文檔里出現很長的內容,既造成了閱讀的困難還增加了維護的負擔。理想情況下,我們可以把模板分開維護,既能享受模板的語法高亮的便利,又能保證HTML的可讀性。
但事情總不會十全十美的。如果一個項目中要使用非常多的模板,出于避免過多Ajax請求而影響性能的原因,我們不希望這么多文件被分開加載下來。
場景1:腳本標簽
常見的解決方案就是把所有的模板直接放在<scrpit>標簽中,<script>標簽的可選類型要稍作更改,比如改成type=”type/template”(瀏覽器在渲染或解析時會將這個屬性忽略)。
<script id="myTemplate" type="text/x-handlebars-template"> <h1>{{title}}</h1> <ul> {{#names}} <li>{{name}}</li> {{/names}} </ul> </script>
這樣的做,你就可以把所有的模板都放在HTML文檔中,避免了額外的Ajax請求。
script標簽中的內容會后面被JavaScript當做模板來使用。請看下面的代碼,這次我們用的是Handlebars模板引擎再結合一些jQuery,模板就用剛剛的里的。也可以直接看在線演示
var template = $('#myTemplate').html(); var compiledTemplate = Handlebars.compile(template); var result = compiledTemplate(data);
最終效果和上文的Mustache例子是一樣的。Handlebars也可以使用Mustache格式的模板,所以在這里我們就用一樣的模板了。不過要注意,它們之間還是有一個很重要的區別:Handlebars是先得到一個中間結果,再通過這個中間值得到HTML的。它先是將模板編譯成一個JS函數(稱之為compiledTemplate),然后數據再被傳入這個函數中執行,再返回最終結果。
場景2:預編譯模板
雖然說將渲染模板包裝在一個方法里看起來要方便多了,但是將編譯和渲染分開也有顯而易見的優點。最重要的是,分開以后,可以把編譯放在服務器端完成。我們可以在服務器上執行JS代碼(比如使用Node),有些模板引擎支持這樣的預編譯。
我們可以用一個JS文檔(叫它comiled.js吧)將多個預編譯好的文件放在一起。這個文件的內容看起來可能是這樣的:
var myTemplates = { templateA: function() { ….}, templateB: function() { ….}; templateC: function() { ….}; };
然后在應用中,我們只需要將數據傳入這些預編譯好的模板中:
var result = myTemplates.templateB(data);
這個方法遠比上文中討論過的將所有的模板放在<script type=”text/javascript”>中要好,客戶端會忽略編譯過程。取決于你的應用套件(application stack),這個解決方式并不一定很難實現,我們會在下文看到它具體的實現。
Node.js示例
任何模板預編譯腳本至少要滿足下面的要求:
- 讀取模板文件,
- 編譯模板,
- 最后的結果可以被合并入一個或多個文件、
下文中的Node.js腳本就實現了上面說的那3點(使用Hogan.js模板引擎):
var fs = require('fs'), hogan = require('hogan.js'); var templateDir = './templates/', template, templateKey, result = 'var myTemplates = {};'; fs.readdirSync(templateDir).forEach(function(templateFile) { template = fs.readFileSync(templateDir + templateFile, 'utf8'); templateKey = templateFile.substr(0, templateFile.lastIndexOf('.')); result += 'myTemplates["'+templateKey+'"] = '; result += 'new Hogan.Template(' + hogan.compile(template, {asString: true}) + ');' }); fs.writeFile('compiled.js', result, 'utf8');
這段代碼先是讀取了在templates目錄下所有的文件,再編譯了這些模板,最后將它們寫入compiled.js。
注意!現在得到的結果是完全沒有優化過的代碼,也沒有做任何錯誤處理。不過它還是完成我們想要它做的事,也不需要很長的代碼來預編譯模板。
場景3:AMD和RequireJS
隨著異步牽引模塊(通常我們都稱之為AMD)越來越多地被使用,為了更好地組織你的APP,建議將模塊解耦。RequireJS是現在主流的模塊加載器之一,在模塊定義中,你可以特定某些依賴,在實際的模塊里你就可以使用它們了(工廠模式)。
在使用模塊時,RequireJS有一個text插件用于規定基于文本的依賴。默認是將AMD的依賴當做JavaScript來處理,不過模板并不是JS而是文本(比如HTML格式的模板),所以我們需要用上這個插件:
define(['handlebars', 'text!templates/myTemplate.html'], function(Handlebars, template) { var myModule = { render: function() { var data = {"title": "Story", "names": [{"name": "Tarzan"}, {"name": "Jane"}]}; var compiledTemplate = Handlebars.compile(template); return compiledTemplate(data); } }; return myModule; });
這樣,就能在單獨的文件中管理各個模板了,雖然這么做是挺好的,但無疑增加了很多額外的Ajax請求,而且仍然需要在客戶端編譯模板。但是,可以用RequireJS中的r.js來優化這些額外的請求。這個決定了依賴,將模板或者依賴植入模塊定義中,大大減小了請求數。
你會發現我們還沒有說到預處理,事實上有兩個方法可以完成預處理。可以寫一個r.js的插件或者別的程序來預處理模板。這么做的話就會改動了模塊定義:我們需要在優化之前先使用一個模板*字符串*,然后再使用一個模板*方法*。不過這些問題也不是很難處理,你可以去檢測它的變量類型或者將邏輯抽象出來(寫在插件中或者直接寫在應用中)。
監聽模板
在場景2和場景3中,如果將模板當做未編譯的資源我們還能將應用構建地更好。就像你在寫CoffeeScript、Less或者SCSS,在開發時,可以監聽模板文件的變化,一旦發現文件出現變化,就立刻自動重新編譯,就像從CoffeeScript編譯到JavaScript一樣。這樣我們在代碼中處理的模板都是已經預編譯過了的,還方便了在開發過程匯中將預編譯模板做相關的內聯優化。
define(['templates/myTemplate.js'], function(compiledTemplate) { var myModule = { render: function() { var data = {"title": "Story", "names": [{"name": "Tarzan"}, {"name": "Jane"}]}; return compiledTemplate(data); }; }; return myModule; }
性能問題
用客戶端模板完成UI更新時的渲染是常見的方法。還是那句話,想要達到性能最優,那就要在第一次請求頁面時盡可能少的請求額外的資源。這樣瀏覽器在渲染HTML頁面時不會因為要去加載JS資源或者別的數據而中斷渲染。這聽起來挺難的,特別是在又要動態加載內容又要盡可能減少加載時間的頁面上。理想情況下,模板是既可以在客戶端也可以在服務端使用的,這樣可以提供最優的性能還能保持它的可維護性。
有兩個問題還需要考慮一下:
- 我的應用中哪里是有最多動態加載的呢?又是哪部分需要最短的加載時間的呢?
- 處理種種問題的程序是要放在客戶端還是服務端呢?
實際問題實際分析。確實使用預處理過的模板,客戶端可以比較輕易地快速渲染出效果。但是如果你需要重用模板,你會偏愛邏輯較少的模板一些。
結論
我們已經看到了客戶端模板的種種好處,比如:
- 服務器和API最好只負責提供數據(比如JSON);客戶端模板就能直接把數據套上了。
- 客戶端方向的開發者可以自如地使用HTML和JS。
- 使用模板的話,你就必須把邏輯和表現分離開。
- 模板可以預編譯好然后緩存起來,這樣服務器每次都只要發送數據就可以了。
- 不在服務器端渲染而在客戶端渲染,多少會影響性能。
上述的文字已經介紹了很多關于(客戶端)模板的知識,希望現在你對這些內容有了更深的認識。