使用QeePHP创建可复用的用户界面组件
任何一个Web开发者都会发现,一个应用中,大多数页面上都有完全相同或类似的区域。为了
提高效率和保持页面表现的一致性,开发者开始将这些重复出现的区域抽离出现,做成一个个子模
板,然后通过模板引擎将这些子模板和完整的页面模板组合在一起。
但是,这种做法仍然有许多不足:
? 如果一个子模板需要特定的数据,那么在显示包含该子模板的页面时,就必须提供这些数据;
? 如果子模板需要根据用户输入或者应用程序的状态来显示不同内容,那么程序中就不得不随
时调用该子模板需要的数据处理代码,然后在模板中写上一大堆判断条件;
? 只有很少的模板引擎支持子模板的嵌套,因此开发者没有办法将页面更进一步的组件化。
对于上面三个不足,出现了两种典型的解决方案:
? 通过类继承或者公用函数库,在显示模板前处理好所有子模板需要的数据,这样在显示页面
时,就不用考虑各个子模板的数据要求了。但这样一来,如果子模板数量过多或者某些子模
板需要的数据涉及到大量耗时的操作,那么对应用程序的整体性能有很大影响。因为即便当
前显示的页面上没有这些子模板,但你仍然需要去准备所有子模板需要的数据;
? 将各个子模板需要的数据处理代码包装成一个个的方法或者函数,需要时调用一下。这样一
来避免了性能问题,但在显示完整页面前,就需要记得调用该页面中子模板对应的函数。不
但繁琐,而且容易遗漏或调用多余的函数。
在许多开发框架中,允许开发者将页面组件封装为对象。这些对象不但要处理数据,还要负责
组件的显示,从而将一个组件完整的包装起来。但问题是,为了使用这些组件,通常都需要使用该
开发框架自带的模板引擎,这样才能解析那些代表不同组件的特别标记。
典型的例子就是Prado。当然,Prado远比封装数据和显示更进一步,它还将各个组件的行为
也封装到了对象中。可惜由于没有好的IDE支持和不理想的性能,Prado这种模仿ASP.NET的页面
组件对象模型始终没能成为主流。
所以,如何提供一种既方便又高效,同时容易使用的页面组件化的解决方案,就成了一个有趣
的问题。目前,在QeePHP中,通过WebControls来部分解决了这个问题。
QeePHP 的WebControls 有下列主要特征:
? 与具体的模板引擎无关,可以用在任何模板引擎或者PHP 页面中;
? 为Smarty 等常用的模板引擎提供了插件,简化使用;
? 允许将一个重用区域的数据和表现封装起来;
? 允许组件的嵌套;
? 只有用到的组件才会被加载和调用,遵循“按需加载”原则。
一个WebControl实例
这个WebControl实现了一个和digg.com类似的分页条,效果图如下:
要使用这个 WebControl,只需要在代码中构造一个 FLEA_Helper_Pager 对象,然后通过
FLEA_Helper_Pager::getPagerData()获取分页需要的信息,并在模板中(以Smarty为例,后文均
是)调用即可:
{ webcontrol type=”pagernav” pager=$pagerData controller=”products”
action=”list” }
演示的调用代码如下:
// 获得当前页数据,并指定每页大小、查询条件和排序方式
class Controller_Products extends FLEA_Controller_Action
{
function actionList()
{
$page = isset($_GET[‘page’]) ? (int)$_GET[‘page’] : 0;
$pagesize = 50;
$conditions = null; // 查询条件为null,表示查询所有记录
$sortby = ‘created DESC’;
FLEA::loadClass(‘FLEA_Helper_Pager’);
$tableProducts = FLEA::getSingleton(‘Table_Products’);
// $tableProducts 是用于操作产品数据表的表数据入口对象
$pager = new FLEA_Helper_Pager($tableProducts, $conditions, $page,
$pagesize, $sortby);
$view = $this->_getView();
$view->assign(‘pagerData’, $pager->getPagerData);
$view->assign(‘products_list’, $pager->findAll()); // 查询指定页的产品数据
$view->display(‘products_list.html’);
}
}
上面的代码中,并没有直接调用WebControl,只是准备了该WebControl 需要的基本数据。
通过这种方式,开发者可以在不同的页面重复使用这个分页导航栏,唯一的要求就是提供需要的基
本数据。
这个WebControl的实现代码如下:
/**
* 分页导航栏
*/
function _ctlPagenav($name, $attribs)
{
$opts=array('pager', 'controller', 'action', 'length', 'slider', 'prevLabel',
'nextLabel');
$data = FLEA_WebControls::extractAttribs($attribs, $opts);
FLEA_WebControls::mergeAttribs($attribs);
if ($data['slider'] <= 0) { $data['slider'] = 2; }
if ($data['length'] <= 0) { $data['length'] = 9; }
if ($data['prevLabel'] == '') { $data['prevLabel'] = 'prev'; }
if ($data['nextLabel'] == '') { $data['nextLabel'] = 'next'; }
$output = "<div id=\"pagenav\">\n<ul";
if ($name) {
$name = h($name);
$output .= " id=\"{$name}\"";
}
$output .= ">\n";
if ($data['pager']['currentPage'] == $data['pager']['firstPage']) {
$output .= "<li class=\"disabled\">« {$data['prevLabel']}</li>\n";
} else {
$attribs['page'] = $data['pager']['prevPage'];
$url = url($data['controller'], $data['action'], $attribs);
$output .= "<li><a href=\"{$url}\">« {$data['prevLabel']}</a></li>\n";
}
$currentPage = $data['pager']['currentPage'];
$mid = intval($data['length'] / 2);
if ($currentPage < $data['pager']['firstPage']) {
$currentPage = $data['pager']['firstPage'];
}
if ($currentPage > $data['pager']['lastPage']) {
$currentPage = $data['pager']['lastPage'];
}
$begin = $currentPage - $mid;
if ($begin < $data['pager']['firstPage']) { $begin =
$data['pager']['firstPage']; }
$end = $begin + $data['length'] - 1;
if ($end >= $data['pager']['lastPage']) {
$end = $data['pager']['lastPage'];
$begin = $end - $data['length'] + 1;
if ($begin < $data['pager']['firstPage']) { $begin =
$data['pager']['firstPage']; }
}
if ($begin > $data['pager']['firstPage']) {
for ($i = $data['pager']['firstPage']; $i < $data['pager']['firstPage']
+ $data['slider'] && $i < $begin; $i++) {
$attribs['page'] = $i;
$in = $i + 1;
$url = url($data['controller'], $data['action'], $attribs);
$output .= "<li><a href=\"{$url}\">{$in}</a></li>\n";
}
if ($i < $begin) {
$output .= "<li class=\"none\">...</li>\n";
}
}
for ($i = $begin; $i <= $end; $i++) {
$attribs['page'] = $i;
$in = $i + 1;
if ($i == $data['pager']['currentPage']) {
$output .= "<li class=\"current\">{$in}</li>\n";
} else {
$url = url($data['controller'], $data['action'], $attribs);
$output .= "<li><a href=\"{$url}\">{$in}</a></li>\n";
}
}
if ($data['pager']['lastPage'] - $end > $data['slider']) {
$output .= "<li class=\"none\">...</li>\n";
$end = $data['pager']['lastPage'] - $data['slider'];
}
for ($i = $end + 1; $i <= $data['pager']['lastPage']; $i++) {
$attribs['page'] = $i;
$in = $i + 1;
$url = url($data['controller'], $data['action'], $attribs);
$output .= "<li><a href=\"{$url}\">{$in}</a></li>\n";
}
if ($data['pager']['currentPage'] == $data['pager']['lastPage']) {
$output .= "<li class=\"disabled\">{$data['nextLabel']} »</li>\n";
} else {
$attribs['page'] = $data['pager']['nextPage'];
$url = url($data['controller'], $data['action'], $attribs);
$output .= "<li><a href=\"{$url}\">{$data['nextLabel']}
»</a></li>\n";
}
$output .= "</ul></div>\n";
return $output;
}
配套的CSS 样式表:
/* pagenav */
#pagenav {
font-size: 12px;
font-weight: bold;
}
#pagenav ul {
list-style: none;
margin: 0px;
padding: 0px;
}
#pagenav li {
list-style: none;
background-color: #fff;
margin: 0px;
display: block;
float: left;
margin-left: 2px;
margin-right: 2px;
}
#pagenav li.disabled {
border: 1px solid #DDDDDD;
padding: 2px 6px 2px 6px;
color: #ccc;
}
#pagenav li.current {
border: 1px solid #2E6AB1;
padding: 2px 6px 2px 6px;
background-color: #2E6AB1;
color: #fff;
}
#pagenav li.none {
border: 1px none;
padding: 2px 6px 2px 6px;
}
#pagenav li a {
border: 1px solid #9AAFE5;
padding: 2px 6px 2px 6px;
display: block;
text-decoration: none;
}
#pagenav li a:hover {
border: 1px solid #2E6AB1;
}
如何在创建自己的WebControls 类型
每一个WebControl 有一个必须的属性:type (类型),例如 textbox、dropdownlist等等。
创建一个新的 WebControl 实际上就是创建一个新的 WebControl 类型。因此在选择新
WebControl 的type属性时,一定不能出现重复。
而一个WebControl 总是由一个函数或方法来产生,函数或方法的原型如下:
function _ctl类型($name, $attribs)
假设WebControl 的类型为“userLoginForm”,那么对应的函数名就是:
function _ctlUserLoginForm($name, $attribs)
要让QeePHP 能够加载这些自定义的WebControl,有两种不同的方式:
? 每种类型的WebControl 对应一个函数,该函数放到一个单独的 .php 文件中;
? 每种类型的WebControl 对应一个类的方法,该类在初始化WebControls 时自动载入。
具体选择哪种方式,依开发者的个人喜好而定。
如果选择第一种方式,那么首先创建一个空目录,然后创建以 WebControl 类型首字母大写
的.php文件,例如Wizard.php。为了能够在Linux/Unix 等区分文件名大小写的操作系统中正常
工作,务必注意文件命名规则。文件名只能第一个字母大写,其余全部小写。
创建文件后,既在该文件内添加函数:
Function _ctlUserLoginForm($name, $attribs)
{
return ....
}
该函数最后必须返回一个包含控件输出内容的字符串。这个字符串通常就是控件的HTML代码。
添加了WebControl对应的.php文件后,还要修改应用程序设置webControlsExtendsDir,指
示保存自定义WebControls 的目录。例如:
FLEA::setAppInf('webControlsExtendsDir', APP_DIR . '/WebControls');
第二种方式则是创建一个新的类,并且将每种WebControl 类型实现为该类的一个方法。
首先还是创建一个新的 .php文件,并在其中实现一个如下的class:
class UI_WebControls extends FLEA_WebControls
{
Function _ctlUserLoginForm($name, $attribs)
{
return ....
}
}
这个新的类必须从FLEA_WebControls 继承,而每一个WebControl 类型都是该类的一个方法。
方法命名规则与前一种方式相同。最后修改应用程序设置 webControlsClassName 为
“UI_WebControls ”。当然,为了能够让QeePHP 在需要时能够自动加载该文件,需要将文件放置
搜索路径中。
完成了创建WebControl 的第一步工作后,接下来就是编写实现WebControl 的代码。下面以一
个简单的用户登录框为例说明WebControl 的具体实现。
实现一个WebControl 类型
下面我们来看看如何具体实现一个WebControl 类型。
示例一:
用户登录框有两种主要的模式:未登录状态和已登录状态。在未登录状态,显示两个输入框和
一个提交按钮;在已登录状态,则显示当前用户的用户名和一个注销连接。
下面是基本的代码:
function _ctlUserLoginForm($name, $attribs)
{
If (!empty($_SESSION[‘username’])) {
// 用户已经登录
return ....
} else {
// 用户未登录
return ....
}
}
在 WebControl 中除了直接取得数据,还有两个重要的参数:$name和 $attribs。$name参
数指示该WebControl 的名字,而 $attribs是一个数组,包含附加的属性。
示例二:
由于我们可能在同一个页面中包含多个同类型的WebControl,所以需要用 $name属性来区分
这些WebControl实例。同样,对于同样类型的WebControl,可以提供不同的附加属性。
例如我们创建一个输入用户姓名的WebControl:
function _ctlUsernameInput($name, $attribs)
{
$output = <<<EOT
<input type="textbox" name="{$name}_firstname" size=10 />
<input type="textbox" name="{$name}_lastname" size=20 />
EOT;
return $output;
}
在模板中可以这样创建该类型WebControl 的实例:
作者的名字:
{ webcontrol type="UsernameInput" name="author" }
<br />
助手的名字:
{ webcontrol type="UsernameInput" name="assistant" }
生成的页面会包含4个输入框,其name属性分别是author_firstname、author_lastname、
assistant_firstname、assistant_lastname 。提交表单时,PHP 程序可以很容易的处理这些输入
信息。
更进一步,可以通过$attribs传递更多的属性给WebControl实例:
作者的名字:
{ webcontrol type="UsernameInput" name="author" size=10 maxlength=10
class="field" }
<br />
助手的名字:
{ webcontrol type="UsernameInput" name="assistant" size=20 maxlength=20 class="field" }
生成WebControl 的函数则改为:
function _ctlUsernameInput($name, $attribs)
{
$attb = FLEA_WebControls::attribsToString($attribs);
$output = <<<EOT
<input type="textbox" name="{$name}_firstname" $attb />
<input type="textbox" name="{$name}_lastname" $attb />
EOT;
return $output;
}
FLEA_WebControls::attribsToString() 方法可以将包含属性的数组转换为对应的字符串表
现形式,从而直接使用。
更多WebControls 的用法
WebControls 相对子模板来说,可以用很简单的方式封装可重用区域需要的数据和表现。例如
每个页面头部都需要的顶部导航栏,就可以做成一个WebControl。这个WebControl可以根据当前
访问者的身份返回不同的连接地址。
实际上,可以在WebControl 内再调用模板引擎来加载模板。从而将WebControl 的表现和实
现分离。如果WebControl加载的模板中还有WebControl标记,那么可以将WebControl嵌套到一
个WebControl中。利用这些特点,用户界面可以细分为多个重复使用的区域。这样既能提高效率,
又可以避免各个页面同类信息的表现形式上的不一致性。
如果是相当复杂的WebControl,那么可以将其对应的函数作为一个入口。在该函数里面调用
其他对象生成WebControl。例如笔者实现的一个多步骤向导(将另外撰文详述该向导)就是通过多
个类的协作来实现向导式界面。
WebControls 的改进
目前,WebControls 只实现了将重用区域的数据和表现封装起来,还没能将区域的行为也封装
起来。但随着QeePHP 的不断发展,相信实现该特征指日可待。
将来,WebControls 将帮助开发者将一个页面区域封装成有自定义属性、方法、行为和数据的