總體來說,模板引擎是一個"好東西"
作為一個PHP/Perl的程式員,許多模板引擎(fastTemplate, Smarty, Perl的 HTML::Template)的使用者,以及我自己的(bTemplate [1] 的作者),我講這句話很多次了。
然而,在同事進行了長時間的討論之後,我確信了大量的模板引擎(包括我自己寫的)根本是錯誤的。 我想唯一的例外是Smarty [2],雖然我認為它太龐大了,並且考慮到這篇文章的其餘部分相當的沒有觀點。然而,就你為什麼選擇Smarty(或者類似的解決方案)有幾個理由,這些將在文章後面探究。
這篇文章討論模板的理論。我們將看到為什麼大部分"模板引擎"是過於肥大,並且最終我們將回過頭來看一個輕量級的,小巧快速的另類選擇。
下載和授權
模板類和所有在本文中使用的例子能夠在這裡下載:template.zip [3]。你可以根據發布 [4]在 OSI [5] 的 MIT Open Source License使用這些檔案中的代碼。
一些關於模板引擎的背景知識
讓我們首先研究一下模板引擎的背景知識。模板引擎被設計出來用於把商業邏輯(例如從資料庫中擷取資料或者計算貿易耗費)從資料的表現分離開來。模板引擎解決了兩個主要問題:
- 如何?這種分離
- 如何從HTML中分離"複雜"的php代碼
這從理論上使得沒有PHP經驗的HTML設計者能夠不看任何PHP代碼的條件下修改網站的外觀。
然而,模板系統也引入了一些複雜性。首先,我們現在有一個從多個檔案得來的"頁面"。典型的,你可能有一個主PHP頁負責商務邏輯,一個外面的"布局"模板把整個網站的整體布局進行渲染,一個內部的內容特定的模板,一個資料庫抽象層,以及模板引擎本身(這些可能是也可能不是由多個檔案組成)。也有可能,一些人僅僅簡單地在每個PHP頁面的首尾處包含"頭部"和"尾部"檔案。
這產生的單個頁面的檔案數量是很可觀的。然而,因為PHP解析器非常快,用到的檔案數量可能不是那麼重要除非你的網站流量很大。
然而,要記住模板系統引入了另外一個處理的層次。模板檔案不僅僅是必須被包含,他們還必須被解析(取決於模板系統,這個行為有很多種方式來完成 —— 使用Regex,字串替換,編譯,詞法分析,等等)。這就是為什麼對模板進行測速變得流行起來:因為模板引擎使用各種方法來解析資料,它們中的一些比另外一些要快(而且,一些模板引擎提供了比其他引擎更加豐富的功能)。
模板引擎基礎知識
簡單地說,模板引擎利用了用C寫的指令碼語言(PHP)。在這些嵌入的指令碼語言中,你有另外一個偽指令碼語言(無論你的模板引擎支援何種標籤)。某些提供了簡單的變數改寫和迴圈。另外一些呢,則提供了條件和嵌套迴圈。而再其他的呢(至少有Smarty)提供了一個PHP的比較大的子集的介面,以及一個緩衝層。
為什麼我認為Smarty最接近於正確的方向?因為Smarty的目標是"把商務邏輯從表現中分離出來"而不是"PHP代碼和HTML代碼的分離"。這看上去區別不大,但是它正是要點所在。任何模板引擎的最終目標不應該是從HTML移除所有的邏輯。它應該是把表現邏輯從商務邏輯中分離出來。
有很多你僅僅需要邏輯來正確顯示你的資料的例子。例如,你的商務邏輯是從你的資料庫中擷取一個使用者列表。你的表現邏輯可能是把使用者列表用3列顯示。可能修改使用者列表函數使得它返回3個數組是很笨的辦法。畢竟函數不應該關心資料接下來要怎麼處理這樣的事情。然而,在你的模板檔案中缺少一些邏輯,那些正是你要做的事情。
在這點上Smarty是正確的(使得你利用PHP的很多東西),但是仍然有許多問題。基本上,它僅僅提供了一個以新文法訪問PHP的介面。以那開始,它看上去不那麼聰明了。是不是事實上寫 {foreach --args}
比
更加簡單?如果你認為這樣簡單一些,問問你自己是不是在包含一個巨大的模板庫來到成這種分離時能夠看到真正的意義要更加簡單一些。誠然,Smarty提供了許多其他很好的特性,但是看上去這些益處能夠在不用承擔包含Smarty類庫的情況下也能獲得。
別樣的解決方案
我主要要鼓吹的一個解決方案是一個使用PHP代碼作為它的原生指令碼語言的"模板引擎"。我知道這以前有人做過。而且當我第一次看到的時候,我想,"為什麼要這樣做?",然而我在考慮過我同事的論據之後,並且實現了一個直接使用PHP代碼仍然實現了把商務邏輯和表現邏輯分離的最終目標的模板系統時(只用了大約25行代碼,不包括注釋),我意識到了好處所在。
這個系統給像我們這樣的開發人員提供了對PHP核心函數的訪問權利,我們能夠使用他們來格式化輸出——像日期格式化這樣的任務應該在模板中處理。而且,因為模板是普通的PHP檔案,像Zend Performance Suite [6] 和PHP Accelerator [7] 這樣的位元組程式碼快取程式,能夠自動緩衝模板(因而,它們不需要在每次被訪問時都被重新解釋執行)。只要你記得把你的模板檔案命名為程式能夠辨認出是PHP檔案的名字(通常,你僅僅需要確保它們有一個.php的尾碼),這確實是一個好處。
當我認為這種方法比經典的模板引擎要高明得多時,肯定還有一些要商榷的問題。最明顯的反面意見是,PHP代碼太複雜了,而且設計者不應該強迫去學習PHP。事實上,PHP代碼和像Smarty這樣的進階模板引擎的文法差不多簡單(如果不是更簡單的話)。而且,設計者能夠使用像
這樣的簡寫PHP。這要比
{$var}
複雜很多?當然,這要長一些,但是如果你習慣了,你能夠獲得了PHP的威力而且不用承受解析模板檔案帶來的負擔。
第二,而且可能更重要的,在基於PHP的模板中沒有固有的安全。Smarty提供了選項在模板檔案中徹底禁用PHP代碼。它使得開發人員能夠約束模板能夠訪問的函數和變數。如果你沒有不懷好意的設計者,這不會是什麼問題。然而,如果你允許外部的使用者上傳或者修改模板,我在此展示的基於PHP的解決方案絕對沒有任何安全可言!任何代碼都能放入模板中並且得到運行。是的,甚至是一個print_r($GLOBALS)
(這將改有惡意的使用者訪問指令碼中任何變數的權利)。
但是,我個人或者工作上寫過的項目中,絕大多數不允許最終的使用者修改或者上傳模板。如果是這樣,問題就不存在了。因此現在讓我們來看看代碼吧。
例子
這是一個簡單的使用者列表頁面的例子。
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檔案,為什麼不去獲得這些好處?
這個layout.tpl.php是一個簡單的例子(定義了整個頁面看上去是什麼樣子的模板檔案)
<?=$title;?>
而這是解析後的輸出。
User List
User List
Id |
Name |
Email |
Banned |
1 |
bob |
bob@mozilla.org |
|
2 |
judy |
judy@php.net |
|
3 |
joe |
joe@opera.com |
|
4 |
billy |
billy@wakeside.com |
X |
5 |
eileen |
eileen@slashdot.org |
|
緩衝
因為解決方案簡單如斯,實現模板緩衝成為了一個非常簡單的任務。為了實現緩衝,我們有一個二級類,它擴充了原來的模板類。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
。當然下面有例子來說明。
使用緩衝包括以下步驟。
include()
模板源檔案
- 建立一個新的
CachedTemplate
對象(並且傳遞路徑,唯一的cache_id
和緩衝到期時間給模板)
- 測試內容是否已經被緩衝了
- 如果還促拿了,顯示檔案並且結束指令碼
- 否則,進行所有的處理並且
fetch()
模板
- 對
fetch_cache()
的調用將自動產生一個新的快取檔案
這個指令碼假定你的快取檔案將放到./cache/
中,因此你必須建立那個目錄並且改變它的目錄許可權(chmod
)使得Web伺服器能夠寫入檔案。而且還要注意如果你在編寫指令碼的過程中發現了錯誤,錯誤也會被緩衝!因而在你開發的過程中禁用緩衝是一個好主意。最好的辦法是給cache的生存周期傳遞0——這樣,緩衝總是立即就失效了。
這是一個實際的緩衝的例子。
/**
* 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貢獻的函數的例子。
require_once('template.php');
$tpl = & new Template('./templates/');
$tpl->set('title', 'User Profile');
$profile = array(
'name' => 'Frank',
'email' => 'frank@bob.com',
'password' => 'ultra_secret'
);
$tpl->set_vars($profile);
echo $tpl->fetch('profile.tpl.php');
?>
相關的模板是這樣的:
而且解析後的輸出是這樣的:
Name |
Frank |
Email |
frank@bob.com |
Password |
ultra_secret |
特別感謝Ricardo Garcia和Harry Fuecks他們的對這篇文章的貢獻。
相關的連結
這兒是一個總體上探究模板引擎的好去處的列表。
- Web Application Toolkit Template View [9] - 許多關於模板實現方法的資訊
- MVC Pattern [10] - 描述3層應用程式的設計
- SimpleT [11] - 另一個使用PEAR::Cache_Lite的基於php的模板引擎
- Templates and Template Engines [12] - 更多關於各種模板實現的資訊
- Smarty [13] - 編譯型模板引擎
模板類原始碼
以及最後出場的,模板類。
/**
* Copyright © 2003 Brian E. Lozier (brian@massassi.net)
*
* set_vars() method contributed by Ricardo Garcia (Thanks!)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
class Template {
var $vars; /// Holds all the template variables
var $path; /// Path to the templates
/**
* Constructor
*
* @param string $path the path to the templates
*
* @return void
*/
function Template($path = null) {
$this->path = $path;
$this->vars = array();
}
/**
* Set the path to the template files.
*
* @param string $path path to template files
*
* @return void
*/
function set_path($path) {
$this->path = $path;
}
/**
* Set a template variable.
*
* @param string $name name of the variable to set
* @param mixed $value the value of the variable
*
* @return void
*/
function set($name, $value) {
$this->vars[$name] = $value;
}
/**
* Set a bunch of variables at once using an associative array.
*
* @param array $vars array of vars to set
* @param bool $clear whether to completely overwrite the existing vars
*
* @return void
*/
function set_vars($vars, $clear = false) {
if($clear) {
$this->vars = $vars;
}
else {
if(is_array($vars)) $this->vars = array_merge($this->vars, $vars);
}
}
/**
* Open, parse, and return the template file.
*
* @param string string the template file name
*
* @return string
*/
function fetch($file) {
extract($this->vars); // Extract the vars to local namespace
ob_start(); // Start output buffering
include($this->path . $file); // Include the file
$contents = ob_get_contents(); // Get the contents of the buffer
ob_end_clean(); // End buffering and discard
return $contents; // Return the contents
}
}
/**
* An extension to Template that provides automatic caching of
* template contents.
*/
class CachedTemplate extends Template {
var $cache_id;
var $expire;
var $cached;
/**
* Constructor.
*
* @param string $path path to template files
* @param string $cache_id unique cache identifier
* @param int $expire number of seconds the cache will live
*
* @return void
*/
function CachedTemplate($path, $cache_id = null, $expire = 900) {
$this->Template($path);
$this->cache_id = $cache_id ? 'cache/' . md5($cache_id) : $cache_id;
$this->expire = $expire;
}
/**
* Test to see whether the currently loaded cache_id has a valid
* corrosponding cache file.
*
* @return bool
*/
function is_cached() {
if($this->cached) return true;
// Passed a cache_id?
if(!$this->cache_id) return false;
// Cache file exists?
if(!file_exists($this->cache_id)) return false;
// Can get the time of the file?
if(!($mtime = filemtime($this->cache_id))) return false;
// Cache expired?
if(($mtime + $this->expire) < time()) {
@unlink($this->cache_id);
return false;
}
else {
/**
* Cache the results of this is_cached() call. Why? So
* we don't have to double the overhead for each template.
* If we didn't cache, it would be hitting the file system
* twice as much (file_exists() & filemtime() [twice each]).
*/
$this->cached = true;
return true;
}
}
/**
* This function returns a cached copy of a template (if it exists),
* otherwise, it parses it as normal and caches the content.
*
* @param $file string the template file
*
* @return string
*/
function fetch_cache($file) {
if($this->is_cached()) {
$fp = @fopen($this->cache_id, 'r');
$contents = fread($fp, filesize($this->cache_id));
fclose($fp);
return $contents;
}
else {
$contents = $this->fetch($file);
// Write the cache
if($fp = @fopen($this->cache_id, 'w')) {
fwrite($fp, $contents);
fclose($fp);
}
else {
die('Unable to write cache.');
}
return $contents;
}
}
}
?>
另外一個值得注意的重要的事情是這裡展示的解決辦法是我們傳遞模板的檔案名稱給fetch()
函數。如果你需要重用模板對象而不去re-set()
所有的變數,這將比較有用。
並且記住:模板引擎的要點是把你的商務邏輯從你的表現邏輯中分離出來,而不是把你的PHP代碼從HTML代碼中分離出來。
本文附件下載:template.zip
[1] http://www.massassi.com/bTemplate/
[2] http://smarty.php.net/
[3] http://www.sitepoint.com/examples/tempeng/template.zip
[4] http://opensource.org/licenses/mit-license.html
[5] http://www.opensource.org/
[6] http://zend.com/store/products/zend-performance-suite.php
[7] http://www.php-accelerator.co.uk/
[8] http://www.php.net/manual/en/control-structures.alternative-syntax.php
[9] http://wact.sourceforge.net/index.php/TemplateView
[10] http://www.phppatterns.com/index.php/article/articleview/11/
[11] http://simplet.sourceforge.net/
[12] http://phppatterns.com/index.php/article/articleview/4/1/1/
[13] http://smarty.php.net/
本文英文原版地址:http://www.sitepoint.com/article/1218/