Bookmark and Share

Lee's 程序人生

HTML CSS Javascript XML AJAX ATLAS C# C++ 数据结构 软件工程 设计模式 asp.net Java 数字图象处理 Sql 数据库
  博客园  :: 首页  :: 新随笔  :: 联系 :: 管理

使用QeePHP创建可复用的用户界面组件

Posted on 2007-08-08 02:02  analyzer  阅读(1851)  评论(0编辑  收藏  举报

使用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 将帮助开发者将一个页面区域封装成有自定义属性、方法、行为和数据的

我要啦免费统计