超越模板引擎

超越模板引擎

作爲一個PHP/Perl的程序員,許多模板引擎(fastTemplate, Smarty, Perl的 HTML::Template)的用戶,以及我自己的(bTemplate [1] 的作者),我講這句話很多次了。


然而,在同事進行了長時間的討論之後,我確信了大量的模板引擎(包括我自己寫的)根本是錯誤的。 我想唯一的例外是Smarty [2],雖然我認爲它太龐大了,並且考慮到這篇文章的其餘部分相當的沒有觀點。然而,就你爲什麼選擇Smarty(或者類似的解決方案)有幾個理由,這些將在文章後面探究。


這篇文章討論模板的理論。我們將看到爲什麼大部分"模板引擎"是過於肥大,並且最終我們將回過頭來看一個輕量級的,小巧快速的另類選擇。



下載和授權


模板類和所有在本文中使用的例子能夠在這裏下載:template.zip [3]。你可以根據發佈 [4] OSI [5] 的 MIT Open Source License使用這些文件中的代碼。



一些關於模板引擎的背景知識


讓我們首先研究一下模板引擎的背景知識。模板引擎被設計出來用於把商業邏輯(例如從數據庫中獲取數據或者計算貿易耗費)從數據的表現分離開來。模板引擎解決了兩個主要問題:




  1. 如何實現這種分離
  2. 如何從HTML中分離"複雜"的php代碼


這從理論上使得沒有PHP經驗的HTML設計者能夠不看任何PHP代碼的條件下修改站點的外觀。


然而,模板系統也引入了一些複雜性。首先,我們現在有一個從多個文件得來的"頁面"。典型的,你可能有一個主PHP頁負責業務邏輯,一個外面的"佈局"模板把整個站點的整體佈局進行渲染,一個內部的內容特定的模板,一個數據庫抽象層,以及模板引擎本身(這些可能是也可能不是由多個文件組成)。也有可能,一些人僅僅簡單地在每個PHP頁面的首尾處包含"頭部"和"尾部"文件。


這產生的單個頁面的文件數量是很可觀的。然而,因爲PHP解析器非常快,用到的文件數量可能不是那麼重要除非你的站點流量很大。
然而,要記住模板系統引入了另外一個處理的層次。模板文件不僅僅是必須被包含,他們還必須被解析(取決於模板系統,這個行爲有很多種方式來完成 —— 使用正則表達式,字符串替換,編譯,詞法分析,等等)。這就是爲什麼對模板進行測速變得流行起來:因爲模板引擎使用各種方法來解析數據,它們中的一些比另外一些要快(而且,一些模板引擎提供了比其他引擎更加豐富的功能)。



模板引擎基礎知識


簡單地說,模板引擎利用了用C寫的腳本語言(PHP)。在這些嵌入的腳本語言中,你有另外一個僞腳本語言(無論你的模板引擎支持何種標籤)。某些提供了簡單的變量改寫和循環。另外一些呢,則提供了條件和嵌套循環。而再其他的呢(至少有Smarty)提供了一個PHP的比較大的子集的接口,以及一個緩衝層。


爲什麼我認爲Smarty最接近於正確的方向?因爲Smarty的目標是"把業務邏輯從表現中分離出來"而不是"PHP代碼和HTML代碼的分離"。這看上去區別不大,但是它正是要點所在。任何模板引擎的最終目標不應該是從HTML移除所有的邏輯。它應該是把表現邏輯從業務邏輯中分離出來。


有很多你僅僅需要邏輯來正確顯示你的數據的例子。例如,你的業務邏輯是從你的數據庫中獲取一個用戶列表。你的表現邏輯可能是把用戶列表用3列顯示。可能修改用戶列表函數使得它返回3個數組是很笨的辦法。畢竟函數不應該關心數據接下來要怎麼處理這樣的事情。然而,在你的模板文件中缺少一些邏輯,那些正是你要做的事情。


在這點上Smarty是正確的(使得你利用PHP的很多東西),但是仍然有許多問題。基本上,它僅僅提供了一個以新語法訪問PHP的接口。以那開始,它看上去不那麼聰明瞭。是不是事實上寫 {foreach --args}<? foreach --args ?> 更加簡單?如果你認爲這樣簡單一些,問問你自己是不是在包含一個巨大的模板庫來到成這種分離時能夠看到真正的意義要更加簡單一些。誠然,Smarty提供了許多其他很好的特性,但是看上去這些益處能夠在不用承擔包含Smarty類庫的情況下也能獲得。



別樣的解決方案


我主要要鼓吹的一個解決方案是一個使用PHP代碼作爲它的原生腳本語言的"模板引擎"。我知道這以前有人做過。而且當我第一次看到的時候,我想,"爲什麼要這樣做?",然而我在考慮過我同事的論據之後,並且實現了一個直接使用PHP代碼仍然實現了把業務邏輯和表現邏輯分離的最終目標的模板系統時(只用了大約25行代碼,不包括註釋),我意識到了好處所在。


這個系統給像我們這樣的開發者提供了對PHP核心函數的訪問權利,我們能夠使用他們來格式化輸出——像日期格式化這樣的任務應該在模板中處理。而且,因爲模板是普通的PHP文件,像Zend Performance Suite [6]PHP Accelerator [7] 這樣的字節碼緩存程序,能夠自動緩存模板(因而,它們不需要在每次被訪問時都被重新解釋執行)。只要你記得把你的模板文件命名爲程序能夠辨認出是PHP文件的名字(通常,你僅僅需要確保它們有一個.php的後綴),這確實是一個好處。

當我認爲這種方法比經典的模板引擎要高明得多時,肯定還有一些要商榷的問題。最明顯的反面意見是,PHP代碼太複雜了,而且設計者不應該強迫去學習PHP。事實上,PHP代碼和像Smarty這樣的高級模板引擎的語法差不多簡單(如果不是更簡單的話)。而且,設計者能夠使用像<?=$var;?>這樣的簡寫PHP。這要比{$var}複雜很多?當然,這要長一些,但是如果你習慣了,你能夠獲得了PHP的威力而且不用承受解析模板文件帶來的負擔。

第二,而且可能更重要的,在基於PHP的模板中沒有固有的安全。Smarty提供了選項在模板文件中徹底禁用PHP代碼。它使得開發者能夠約束模板能夠訪問的函數和變量。如果你沒有不懷好意的設計者,這不會是什麼問題。然而,如果你允許外部的用戶上傳或者修改模板,我在此展示的基於PHP的解決方案絕對沒有任何安全可言!任何代碼都能放入模板中並且得到運行。是的,甚至是一個print_r($GLOBALS)(這將改有惡意的用戶訪問腳本中任何變量的權利)。


但是,我個人或者工作上寫過的項目中,絕大多數不允許最終的用戶修改或者上傳模板。如果是這樣,問題就不存在了。因此現在讓我們來看看代碼吧。



例子


這是一個簡單的用戶列表頁面的例子。


<?php  
require_once('template.php');  

/**  
* This variable holds the file system path to all our template files.  
*/  
$path = './templates/';  

/**  
* Create a template object for the outer template and set its variables.  
*/  
$tpl = & new Template($path);  
$tpl->set('title', 'User List');  

/**  
* Create a template object for the inner template and set its variables.  The  
* fetch_user_list() function simply returns an array of users.  
*/  
$body = & new Template($path);  
$body->set('user_list', fetch_user_list());  

/**  
* Set the fetched template of the inner template to the 'body' variable in  
* the outer template.  
*/  
$tpl->set('body', $body->fetch('user_list.tpl.php'));  

/**  
* Echo the results.  
*/  
echo $tpl->fetch('index.tpl.php');  
?>


其中有兩個值得注意的重要的概念。第一個就是內部和外部模板的概念。外部模板包含定義站點主要外觀的HTML代碼。而內部模板包含定義站點內容區域的HTML代碼。當然,你能夠在任意數目的層上有任意數目的模板。因爲通常我們給每個區域使用不同的模板對象,所以沒有名字空間的問題。例如,我能在內部和外部模板中都有變量叫"title",而不用害怕有什麼衝突。


這是一個用來顯示用戶列表的模板的簡單例子。注意特殊的foreach和endforeach;語法在PHP手冊中有說明 [8]。它完全是可選擇的。


而且,你可能奇怪我爲什麼要用.php的後綴來命名我的模板文件。呵呵,許多PHP字節碼緩存解決方案(比如 phpAccelerator)如果要被認成PHP文件,需要文件有一個.php後綴。因爲這些模板是PHP文件,爲什麼不去獲得這些好處?


<table>  
   <tr>  
       <th>Id</th>  
       <th>Name</th>  
       <th>Email</th>  
       <th>Banned</th>  
   </tr>  
<? foreach($user_list as $user): ?>  
   <tr>  
       <td align="center"><?=$user['id'];?></td>  
       <td><?=$user['name'];?></td>  
       <td><a href="mailto:<?=$user['email'];?>"><?=$user['email'];?></a></td>  
       <td align="center"><?=($user['banned'] ? 'X' : '&nbsp;');?></td>  
   </tr>  
<? endforeach; ?>  
</table>


這個layout.tpl.php是一個簡單的例子(定義了整個頁面看上去是什麼樣子的模板文件)


<html>
   <head>
       <title><?=$title;?></title>
   </head>

   <body>

       <h2><?=$title;?></h2>

<?=$body;?>

   </body>
</html>


而這是解析後的輸出。


<html>
 <head>
   <title>User List</title>
 </head>

 <body>

   <h2>User List</h2>

<table>
 <tr>
   <th>Id</th>
   <th>Name</th>
   <th>Email</th>
   <th>Banned</th>
 </tr>
 <tr>
   <td align="center">1</td>
   <td>bob</td>
   <td><a href="mailto:[email protected]">[email protected]</a></td>
   <td align="center">&nbsp;</td>
 </tr>
 <tr>
   <td align="center">2</td>
   <td>judy</td>
   <td><a href="mailto:[email protected]">[email protected]</a></td>
   <td align="center">&nbsp;</td>
 </tr>
 <tr>
   <td align="center">3</td>
   <td>joe</td>
   <td><a href="mailto:[email protected]">[email protected]</a></td>
   <td align="center">&nbsp;</td>
 </tr>
 <tr>
   <td align="center">4</td>
   <td>billy</td>
   <td><a href="mailto:[email protected]">[email protected]</a></td>
   <td align="center">X</td>
 </tr>
 <tr>
   <td align="center">5</td>
   <td>eileen</td>
   <td><a href="mailto:[email protected]">[email protected]</a></td>
   <td align="center">&nbsp;</td>
 </tr>
</table>
 </body>
</html>



緩存


因爲解決方案簡單如斯,實現模板緩存成爲了一個非常簡單的任務。爲了實現緩存,我們有一個二級類,它擴展了原來的模板類。CachedTemplate類事實上使用和原來的模板類相同的API。不同點是我們必須傳遞緩存的設置給構造函數,並且調用fetch_cache()而不是fetch()


緩存的概念是簡單的。簡單的說,我們設置一個緩存時間來調錶輸出應該被保存的時長(以秒爲單位)。在產生一個頁面的所有工作開展之前,我們必須首先測試頁面是否已經被緩存了,而且緩存是否仍然沒有過期。如果緩存在這那,我們不需要在去麻煩數據庫和業務邏輯來產生頁面——我們可以簡單地輸出原先緩存地內容。


這種方法需要解決唯一地標識緩存文件的問題。如果一個站點是被一個顯示基於GET變量的中心腳本所控制,對每個PHP文件只有一個緩存不會有什麼幫助。例如,如果index.php?page=about_us和用戶調用index.php?page=contact_us得到的顯示完全不同。


問題是通過給每個頁面產生一個唯一的cache_id來解決的。爲了做到這個目的,我們把事實上被請求的文件變成REQUEST_URI(基本上就是整個URL:index.php?foo=bar&bar=foo)。當然,這個轉換過程是受到CachedTemplate類控制的,但是要記住的重要的事情是你絕對要在創建CachedTemplate對象時傳遞一個唯一的cache_id。當然下面有例子來說明。


使用緩存包括以下步驟。




  1. include() 模板源文件



  2. 創建一個新的CachedTemplate對象(並且傳遞路徑,唯一的cache_id和緩存過期時間給模板)



  3. 測試內容是否已經被緩存了



  4. 如果還促拿了,顯示文件並且結束腳本



  5. 否則,進行所有的處理並且fetch()模板



  6. fetch_cache()的調用將自動產生一個新的緩存文件


這個腳本假定你的緩存文件將放到./cache/中,因此你必須創建那個目錄並且改變它的目錄權限(chmod)使得Web服務器能夠寫入文件。而且還要注意如果你在編寫腳本的過程中發現了錯誤,錯誤也會被緩存!因而在你開發的過程中禁用緩存是一個好主意。最好的辦法是給cache的生存週期傳遞0——這樣,緩存總是立即就失效了。


這是一個實際的緩存的例子。


<?php
/**
* Example of cached template usage.  Doesn't provide any speed increase since
* we're not getting information from multiple files or a database, but it
* introduces how the is_cached() method works.
*/

/**
* First, include the template class.
*/
require_once('template.php');

/**
* Here is the path to the templates.
*/
$path = './templates/';

/**
* Define the template file we will be using for this page.
*/
$file = 'list.tpl.php';

/**
* Pass a unique string for the template we want to cache.  The template
* file name + the server REQUEST_URI is a good choice because:
*    1. If you pass just the file name, re-used templates will all
*       get the same cache.  This is not the desired behavior.
*    2. If you just pass the REQUEST_URI, and if you are using multiple
*       templates per page, the templates, even though they are completely
*       different, will share a cache file (the cache file names are based
*       on the passed-in cache_id.
*/
$cache_id = $file . $_SERVER['REQUEST_URI'];
$tpl = & new CachedTemplate($path, $cache_id, 900);

/**
* Test to see if the template has been cached.  If it has, we don't
* need to do any processing.  Thus, if you put a lot of db calls in
* here (or file reads, or anything processor/disk/db intensive), you
* will significantly cut the amount of time it takes for a page to
* process.
*
* This should be read aloud as "If NOT Is_Cached"
*/
if(!($tpl->is_cached())) {
   $tpl->set('title', 'My Title');
   $tpl->set('intro', 'The intro paragraph.');
   $tpl->set('list', array('cat', 'dog', 'mouse'));
}

/**
* Fetch the cached template.  It doesn't matter if is_cached() succeeds
* or fails - fetch_cache() will fetch a cache if it exists, but if not,
* it will parse and return the template as usual (and make a cache for
* next time).
*/
echo $tpl->fetch_cache($file);
?>



設置多個變量


我們如何能夠同時設置多個變量?這又一個使用由Ricardo Garcia貢獻的函數的例子。


<?php  
require_once('template.php');  

$tpl = & new Template('./templates/');  
$tpl->set('title', 'User Profile');  

$profile = array(  
   'name' => 'Frank',  
   'email' => '[email protected]',  
   'password' => 'ultra_secret'  
);  

$tpl->set_vars($profile);  

echo $tpl->fetch('profile.tpl.php');  
?>


相關的模板是這樣的:


<table cellpadding="3" border="0" cellspacing="1">  
   <tr>  
       <td>Name</td>  
       <td><?=$name;?></td>  
   </tr>  
   <tr>  
       <td>Email</td>  
       <td><?=$email;?></td>  
   </tr>  
   <tr>  
       <td>Password</td>  
       <td><?=$password;?></td>  
   </tr>  
</table>


而且解析後的輸出是這樣的:


<table cellpadding="3" border="0" cellspacing="1">
 <tr>
   <td>Name</td>
   <td>Frank</td>
 </tr>
 <tr>
   <td>Email</td>
   <td>[email protected]</td>
 </tr>
 <tr>
   <td>Password</td>
   <td>ultra_secret</td>
 </tr>
</table>


特別感謝Ricardo Garcia和Harry Fuecks他們的對這篇文章的貢獻。



相關的鏈接


這兒是一個總體上探究模板引擎的好去處的列表。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章