不知道从什么时候开始,有人开始对
HTML 内嵌入 Server Script 觉得不太满意。然而不论是微软的 ASP 或是开放源码的 PHP,都是属于内嵌 Server Script
的网页伺服端语言。因此也就有人想到,如果能把程序应用逻辑 (或称商业应用逻辑) 与网页呈现 (Layout)
逻辑分离的话,是不是会比较好呢?
其实这个问题早就存在已久,从交互式网页开始风行时,不论是 ASP 或是 PHP
的使用者都是身兼程序开发者与视觉设计师两种身份。可是通常这些使用者不是程序强就是美工强,如果要两者同时兼顾,那可得死掉不少脑细胞...
所以模版引擎就应运而生啦!模版引擎的目的,就是要达到上述提到的逻辑分离的功能。它能让程序开发者专注于资料的控制或是功能的达成;而视觉设计师则可专注于网页排版,让网页看起来更具有专业感!因此模版引擎很适合公司的网站开发团队使用,使每个人都能发挥其专长!
就笔者接触过的模版引擎来说,依资料呈现方式大概分成:需搭配程序处理的模版引擎和完全由模版本身自行决定的模版引擎两种形式。
在需搭配程序处理的模版引擎中,程序开发者必须要负责变量的呈现逻辑,也就是说他必须把变量的内容在输出到模版前先处理好,才能做
assign 的工作。换句话说,程序开发者还是得多写一些程序来决定变量呈现的风貌。而完全由模版本身自行决定的模版引擎,它允许变量直接 assign
到模版中,让视觉设计师在设计模版时再决定变量要如何呈现。因此它就可能会有另一套属于自己的模版程序语法 (如 Smarty)
,以方便控制变量的呈现。但这样一来,视觉设计师也得学习如何使用模版语言。
模版引擎的运作原理,首先我们先看看以下的运行图:
一般的模版引擎
(如 PHPLib) 都是在建立模版对象时取得要解析的模版,然后把变量套入后,透过 parse() 这个方法来解析模版,最后再将网页输出。
对
Smarty 的使用者来说,程序里也不需要做任何 parse 的动作了,这些 Smarty 自动会帮我们做。而且已经编译过的网页,如果模版没有变动的话,
Smarty
就自动跳过编译的动作,直接执行编译过的网页,以节省编译的时间。
使用Smarty的一些概念
在一般模版引擎中,我们常看到区域的观念,所谓区块大概都会长成这样:
区域内容
这些区块大部份都会在 PHP 程序中以 if 或 for, while
来控制它们的显示状态,虽然模版看起来简洁多了,但只要一换了显示方式不同的模版, PHP 程序势必要再改一次!
在 Smarty
中,一切以变量为主,所有的呈现逻辑都让模版自行控制。因为 Smarty 会有自己的模版语言,所以不管是区块是否要显示还是要重复,都是用 Smarty 的模版语法
(if, foreach, section) 搭配变量内容作呈现。这样一来感觉上好象模版变得有点复杂,但好处是只要规划得当, PHP
程序一行都不必改。
由上面的说明,我们可以知道使用Smarty 要掌握一个原则:将程序应用逻辑与网页呈现逻辑明确地分离。就是说 PHP
程序里不要有太多的 HTML 码。程序中只要决定好那些变量要塞到模版里,让模版自己决定该如何呈现这些变量 (甚至不出现也行)
。
Smarty的基础
安装Smarty
首先,我们先决定程序放置的位置。
Windows下可能会类似这样的位置:「
d:appservwebdemo 」。
Linux下可能会类似这样的位置:「 /home/jaceju/public_html/
」。
到Smarty的官方网站下载最新的Smarty套件:http://smarty.php.net。
解开 Smarty
2.6.0 后,会看到很多档案,其中有个 libs 资料夹。在 libs 中应该会有 3 个 class.php ?n + 1 个 debug.tpl + 1 个
plugin 资料夹 + 1 个 core 资料夹。然后直接将 libs 复制到您的程序主资料夹下,再更名为 class
就可以了。就这样?没错!这种安装法比较简单,适合一般没有自己主机的使用者。
至于 Smarty
官方手册中为什么要介绍一些比较复杂的安装方式呢?基本上依照官方的方式安装,可以只在主机安装一次,然后提供给该主机下所有设计者开发不同程序时直接引用,而不会重复安装太多的
Smarty 复本。而笔者所提供的方式则是适合要把程序带过来移过去的程序开发者使用,这样不用烦恼主机有没有安装 Smarty
。
程序的资料夹设定
以笔者在Windows安装Appserv为例,程序的主资料夹是「d:appservwebdemo」。安装好Smarty后,我们在主资料夹下再建立这样的资料夹:
在
Linux 底下,请记得将 templates_c 的权限变更为 777 。Windows
下则将其只读取消。
第一个用Smarty写的小程序
我们先设定 Smarty
的路径,请将以下这个档案命名为 main.php ,并放置到主资料夹下:
代码如下 | 复制代码 |
main.php: include "class/Smarty.class.php"; define('__SITE_ROOT', 'd:/appserv/web/demo'); // 最后没有斜线 $tpl = new Smarty(); $tpl->template_dir = __SITE_ROOT . "/templates/"; $tpl->compile_dir = __SITE_ROOT . "/templates_c/"; $tpl->config_dir = __SITE_ROOT . "/configs/"; $tpl->cache_dir = __SITE_ROOT . "/cache/"; $tpl->left_delimiter = '<{'; $tpl->right_delimiter = '}>'; ?> |
代码如下 | 复制代码 |
templates/test.htm: http-equiv="Content-Type" c> <{$content}> |
现在我们要将上面的模版显示出来,并将网页标题
($title) 与内容 ($content) 更换,请将以下档案内容命名为 test.php
,并放置在主资料夹下:
test.php:
代码如下 | 复制代码 |
require "main.php"; $tpl->assign("title", "测试用的网页标题"); $tpl->assign("content", "测试用的网页内容"); // 上面两行也可以用这行代替 // $tpl->assign(array("title" => "测试用的网页标题", "content" => "测试用的网页内容")); $tpl->display('test.htm'); ?> |
请打开浏览器,输入
http://localhost/demo/test.php 试试看(依您的环境决定网址),应该会看到以下的画面:
再到
templates_c 底下,我们会看到一个奇怪的资料夹 (%%179) ,再点选下去也是一个奇怪的资料夹 (%%1798044067)
,而其中有一个档案:
代码如下 | 复制代码 |
templates_c/%%179/%%1798044067/test.htm.php: Smarty version 2.6.0, created on 2003-12-15 22:19:45 compiled from test.htm */ ?> c> ?> $this->_tpl_vars['content']; ?> |
没错,这就是 Smarty
编译过的档案。它将我们在模版中的变量转换成了 PHP 的语法来执行,下次再读取同样的内容时, Smarty
就会直接抓取这个档案来执行了。
最后我们整理一下整个 Smarty 程序撰写步骤:
Step 1. 加载 Smarty
模版引擎。
Step 2. 建立 Smarty 对象。
Step 3. 设定 Smarty 对象的参数。
Step
4. 在程序中处理变量后,再用 Smarty 的 assign 方法将变量置入模版里。
Step 5. 利用 Smarty 的 display
方法将网页秀出。
如何安排你的程序架构
上面我们看到除了 Smarty 所需要的资料夹外
(class 、 configs 、 templates 、 templates_c) ,还有两个资料夹: includes 、 modules
。其实这是笔者模仿 XOOPS 的架构所建立出来的,因为 XOOPS 是笔者所接触到的程序中,少数使用 Smarty
模版引擎的架站程序。所谓西瓜偎大边,笔者这样的程序架构虽没有 XOOPS 的百分之一强,但至少给人看时还有 XOOPS 撑腰。
includes
这个资料夹主要是用来放置一些 function 、 sql ?n,这样在 main.php
就可以将它们引入了,如下:
main.php:
代码如下 | 复制代码 |
include "class/Smarty.class.php"; define('__SITE_ROOT', 'd:/appserv/web/demo'); // 最后没有斜线 // 以 main.php 的位置为基准 require_once "includes/functions.php"; require_once "includes/include.php"; $tpl = new Smarty(); $tpl->template_dir = __SITE_ROOT . "/templates/"; $tpl->compile_dir = __SITE_ROOT . "/templates_c/"; $tpl->config_dir = __SITE_ROOT . "/configs/"; $tpl->cache_dir = __SITE_ROOT . "/cache/"; $tpl->left_delimiter = '<{'; $tpl->right_delimiter = '}>'; ?> |
modules
这个资料夹则是用来放置程序模块的,如此一来便不会把程序丢得到处都是,整体架构一目了然。
上面我们也提到 main.php
,这是整个程序的主要核心,不论是常数定义、外部程序加载、共享变量建立等,都是在这里开始的。所以之后的模块都只要将这个档案包含进来就可以啦。因此在程序流程规划期间,就必须好好构思
main.php 中应该要放那些东西;当然利用 include 或 require 指令,把每个环节清楚分离是再好不过了。
在上节提到的
Smarty 程序 5 步骤, main.php 就会帮我们先将前 3
个步骤做好,后面的模块程序只要做后面两个步骤就可以了。
从变量开始
如何使用变量
从上一章范例中,我们可以清楚地看到我们利用
<{ 及 }> 这两个标示符号将变量包起来。预设的标示符号为 { 及 } ,但为了中文冲码及 Javascript 的关系,因此笔者还是模仿
XOOPS ,将标示符号换掉。变量的命名方式和 PHP 的变量命名方式是一模一样的,前面也有个 $ 字号 (这和一般的模版引擎不同)。标示符号就有点像是 PHP
中的 (事实上它们的确会被替换成这个) ,所以以下的模版变量写法都是可行的:
代码如下 | 复制代码 |
1. <{$var}> 2. <{ $var }> 3. <{$var }> |
代码如下 | 复制代码 |
<{$var|nl2br}> <{$var|string_format:"%02d"}> |
好,那为什么要让模版自行决定变量呈现的风貌?先看看底下的 HTML ,这是某个购物车结帐的部份画面。
name="total" type="hidden" value="21000" />
总金额:21,000
元
一般模版引擎的模版可能会这样写:
value="{total}" />
总金额:{format_total} 元
它们的 PHP
程序中要这样写:
代码如下 | 复制代码 |
$total = 21000; $tpl->assign("total", $total); $tpl->assign("format_total", number_format($total)); ?> |
而 Smarty 的模版就可以这样写: (number_format
修饰函式请到Smarty 官方网页下载)
代码如下 | 复制代码 |
value="<{$total}>" /> |
总金额:<{$total|number_format:""}>
元
Smarty 的 PHP 程序中只要这样写:
代码如下 | 复制代码 |
$total = 21000; $tpl |
->assign("total", $total);
?>
所以在 Smarty
中我们只要指定一次变量,剩下的交给模版自行决定即可。这样了解了吗?这就是让模版自行决定变量呈现风貌的好处!
控制模版的内容
重复的区块
在
Smarty 样板中,我们要重复一个区块有两种方式: foreach 及 section 。而在程序中我们则要 assign
一个数组,这个数组中可以包含数组数组。就像下面这个例子:
首先我们来看 PHP
程序是如何写的:
test2.php:
代码如下 | 复制代码 |
require "main.php"; $array1 = array(1 => "苹果", 2 => "菠萝", 3 => "香蕉", 4 => "芭乐"); $tpl->assign("array1", $array1); $array2 = array( array("index1" => "data1-1", "index2" => "data1-2", "index3" => "data1-3"), array("index1" => "data2-1", "index2" => "data2-2", "index3" => "data2-3"), array("index1" => "data3-1", "index2" => "data3-2", "index3" => "data3-3"), array("index1" => "data4-1", "index2" => "data4-2", "index3" => "data4-3"), array("index1" => "data5-1", "index2" => "data5-2", "index3" => "data5-3")); $tpl->assign("array2", $array2); $tpl->display("test2.htm"); ?> 而模版的写法如下: templates/test2.htm: http-equiv="Content-Type" c>
|
执行上例后,我们发现不管是
foreach 或 section 两个执行结果是一样的。那么两者到底有何不同呢?
第一个差别很明显,就是foreach
要以巢状处理的方式来呈现我们所 assign 的两层数组变量,而 section 则以「主数组[循环名称].子数组索引」即可将整个数组呈现出来。由此可知,
Smarty 在模版中的 foreach 和 PHP 中的 foreach 是一样的;而 section 则是 Smarty
为了处理如上列的数组变量所发展出来的叙述。当然 section 的功能还不只如此,除了下一节所谈到的巢状资料呈现外,官方手册中也提供了好几个 section
的应用范例。
不过要注意的是,丢给 section 的数组索引必须是从 0 开始的正整数,即 0, 1, 2, 3,
...。如果您的数组索引不是从 0 开始的正整数,那么就得改用 foreach 来呈现您的资料。您可以参考官方讨论区中的此篇讨论,其中探讨了 section 和
foreach
的用法。
巢状资料的呈现
模版引擎里最令人伤脑筋的大概就是巢状资料的呈现吧,许多著名的模版引擎都会特意强调这点,不过这对
Smarty
来说却是小儿科。
最常见到的巢状资料,就算论?程序中的讨论主题区吧。假设要呈现的结果如下:
公告区
站务公告
文学专区
好书介绍
奇文共赏
计算机专区
硬件外围
软件讨论
代码如下 | 复制代码 |
require "main.php"; $forum = array( array("category_id" => 1, "category_name" => "公告区", "topic" => array( array("topic_id" => 1, "topic_name" => "站务公告") ) ), array("category_id" => 2, "category_name" => "文学专区", "topic" => array( array("topic_id" => 2, "topic_name" => "好书介绍"), array("topic_id" => 3, "topic_name" => "奇文共赏") ) ), array("category_id" => 3, "category_name" => "计算机专区", "topic" => array( array("topic_id" => 4, "topic_name" => "硬件外围"), array("topic_id" => 5, "topic_name" => "软件讨论") ) ) ); $tpl->assign("forum", $forum); $tpl->display("test3.htm"); ?> 模版的写法如下: templates/test3.htm: cellspacing="0"> <{section name=sec1 loop=$forum}> | |
colspan="2"><{$forum[sec1].category_name}> | |
| width="164"><{$forum[sec1].topic[sec2].topic_name}> |
执行的结果就像笔者举的例子一样。
因此呢,在程序中我们只要想办法把所要重复值一层一层的塞到数组中,再利用
<{第一层数组[循环1].第二层数组[循环2].第三层数组[循环3]. ... .数组索引}>
这样的方式来显示每一个巢状循环中的值。至于用什么方法呢?下一节使用数据库时我们再提。
转换数据库中的资料
上面提到如何显示巢状循环,而实际上应用时我们的资料可能是从数据库中抓取出来的,所以我们就得想办法把数据库的资料变成上述的多重数组的形式。这里笔者用一个
DB 类别来抓取数据库中的资料,您可以自行用您喜欢的方法。
我们只修改 PHP 程序,模版还是上面那个 (这就是模版引擎的好处~),其中 $db
这个对象假设已经在 main.php
中建立好了,而且抓出来的资料就是上面的例子。
test3.php:
代码如下 | 复制代码 |
require "main.php"; // 先建立第一层数组 $category = array(); $db->setSQL($SQL1, 'CATEGORY'); if (!$db->query('CATEGORY')) die($db->error()); // 抓取第一层循环的资料 while ($item_category = $db->fetchAssoc('CATEGORY')) { // 建立第二层数组 $topic = array(); $db->setSQL(sprintf($SQL2, $item_category['category_id']), 'TOPIC'); if (!$db->query('TOPIC')) die($db->error()); // 抓取第二层循环的资料 while ($item_topic = $db->fetchAssoc('TOPIC')) { // 把抓取的数据推入第二层数组中 array_push($topic, $item_topic); } // 把第二层数组指定为第一层数组所抓取的数据中的一个成员 $item_category['topic'] = $topic; // 把第一层数据推入第一层数组中 array_push($category, $item_category); } $tpl->assign("forum", $category); $tpl->display("test3.htm"); ?> |
在数据库抓取一笔资料后,我们得到的是一个包含该笔数据的数组。透过
while 叙述及 array_push 函式,我们将数据库中的资料一笔一笔塞到数组里。如果您只用到单层循环,就把第二层循环 (红色的部份)
去掉即可。
代码如下 | 复制代码 |
<{if $is_login == true}> 显示使用者操作选单 <{else}> 显示输入帐号和密码的窗体 <{/if}> |
要注意的是,「==」号两边一定要各留至少一个空格符,否则
Smarty 会无法解析。
if
语法一般的应用可以参照官方使用说明,所以笔者在这里就不详加介绍了。不过笔者发现了一个有趣的应用:常常会看到程序里要产生这样的一个表格:
(数字代表的是资料集的顺序)
代码如下 | 复制代码 |
1 2 3 4 5 6 7 8 |
这个笔者称之为「横向重复表格」。它的特色和传统的纵向重复不同,前几节我们看到的重复表格都是从上而下,一列只有一笔资料。而横向重复表格则可以横向地在一列中产生
n 笔资料后,再换下一列,直到整个循环结束。要达到这样的功能,最简单的方式只需要 section 和 if
搭配即可。
我们来看看下面这个例子:
test4.php:
代码如下 | 复制代码 |
require "main.php"; $my_array = array( array("value" => "0"), array("value" => "1"), array("value" => "2"), array("value" => "3"), array("value" => "4"), array("value" => "5"), array("value" => "6"), array("value" => "7"), array("value" => "8"), array("value" => "9")); $tpl->assign("my_array", $my_array); $tpl->display('test4.htm'); ?> |
模版的写法如下:
templates/test4.htm:
代码如下 | 复制代码 |
cellpadding="3"> | |
<{$my_array[sec1].value}> | |
重点在于
$smarty.section.sec1.rownum 这个 Smarty 变量,在 section 循环中这个变量会取得从 1 开始的索引值,所以当
rownum 能被 2 除尽时,就输出
代码如下 | 复制代码 |
require "main.php"; $tpl->assign("title", "Include 测试"); $tpl->assign("content", "这是模版 2 中的变量"); $tpl->assign("dyn_page", "test5_3.htm"); $tpl->display('test5_1.htm'); ?> |
模版 1
的写法如下:
templates/test5_1.htm:
代码如下 | 复制代码 |
http-equiv="Content-Type" c> <{include file="test5_2.htm"}> <{include file=$dyn_page}> <{include file="test5_4.htm" custom_var="自订变量的内容"}> |
模版 2
的写法如下:
templates/test5_2.htm:
<{$content}>
模版 3
的写法如下:
templates/test5_3.htm:
这是模版 3 的内容
模版 4
的写法如下:
templates/test5_4.htm:
<{$custom_var}>
这里注意几个重点:1.
模版的位置都是以先前定义的 template_dir 为基准;2. 所有 include 进来的子模版中,其变量也会被解译。;3. include
中可以用「变量名称=变量内容」来指定引含进来的模版中所包含的变量,如同上面模版 4 的做法。