来源:http://trac.seagullproject.org/wiki/Tutorials/BuildingAnAjaxEnabledModule

使用Ajax创建一个TODO列表模块

  • 要求Seagull 0.6.2以上版本

目标

  • 创建一个交互的 TODO 列表模块。该模块是高度独立并且通过简单单击就可以安装。
  • 学习如何使用模块生成器
  • 集成AJAX的功能
  • 了解模块可提供服务的范围

前些天在浏览网站时看到这个功能强大的拖放网站,它是一个名叫Neustaetter的家伙做的–他采用了随处可见的标准的scriptaculous的拖放实例并将它改进,这样可拖拉的元素不但可以排序还可以实现分组。

本文的目的是在Greg的工作之上,结合javascript/CSS使之可以保存列表元素的状态。我叫它们为TODOs是因为当我看到这个视频时,我认为这是一个非常妙的想法,为什么不将它移植到web上来呢?

需求

你必须使用Seagull 0.6.2或更高版本才能使用本文介绍的特性。在我写这篇文章时,该版本只能在Seagull的SVN库的BUG分枝上找到,这里有一个屏幕截图。你可以以管理员身份登陆,并观察页面底部来查看版本号。

开始

如果你已经安装了Seagull,那么你需要以管理员身份登陆.要想创建TODO模块,请在General → Maintenance菜单下选择module generator菜单项。你可能会很熟悉‘scaffolding’。模块生成器创建类,配置文件和SQL数据文件。它是浏览器驱动的而不需要一个命令行控制台。Seagull让你通过浏览器来交互的设置应用程序的各个方面而不需要在命令行下操作。

模块生成器

用下列值填充模块生成器表单:

Module name: todo
Manager name: todo
Actions: [x] list
Create Templates: [x]
Create language files: [x]
Create ini file: [x]

然后单击‘立即创建模块’按钮。这只是一次尝试。如果你是在Mac或Linux上运行,你可能会收到服务器对modules目录没有写权限的错误提示。使用你最喜爱的工具设置此目录的其它用户具有读写权限,因为模块生成向导在这里创建模块文件。如果是在Windows系统下,你不会遇到任何错误。

如果成功了,你会收到如下消息:

Files for the todo module successfully created ...

如果你单击了它提供的链接到模块的链接,你会收到一个DB错误信息因为我们还末创建相关的表。你可以进入到modules目录下以查看Seagull创建了哪些文件。 让我们返回到最初始的状态来并重新开始来考虑一下数据库。进入General > Manage modules单击‘todo'附近的卸载链接。这样就会取消注册这个模块。你会在列表的冶底部看到此模块,它已经变成灰色并带有一个红色的X。单击X会将模块目录中将刚才创建的文件删除。请执行这个操作以删掉刚才所创建的所有文件。

模块生成器: 第二步

现在,和前面一样填充表单,不过这次要选中CRUD复选框:

Module name: todo
Manager name: todo
Actions: [x] list
Create CRUD Actions: [x]
Create Templates: [x]
Create language files: [x]
Create ini file: [x]

先不要提交表单,我们要先创建它所需要的表。由于管理TODOS是相当简单的工作,所有我们就使用一个简单的模式:

CREATE TABLE `todo` (
  `todo_id` int(11) NOT NULL,
  `description` text NOT NULL,
  `order_id` int(11) NOT NULL,
  `status` int(11) NOT NULL
);

你可以使用phpMyAdmin或类似工具来创建表。继续向导并提交表单,你会达到一个成功页面。你可以单击它提供的链接你会看到一个没有记录的表单的页面。

现在再尝试添加一条记录,肯定会正常工作并收到一条成功添加的信息。接着编辑你刚创建的记录。如果你收到这样的信息:

DB_DataObject Error: No Keys available for foo

意思是你(如果你复制了上面的模式)可能忘了设置主键。所以先给你的表设置一下主键,然后不要忘了rebuild Seagull。Seagull重建功能是一个将Seagull恢复到初始状态的工具。它通过删除你的数据库,重新执行所有的SQL模式和数据文件,重建你的模块和配置数据。要了解如何完成这个任务请看rebuild method

运行'rebuild Seagull'

要重建Seagull先进入到General → maintenance并进入屏幕底部的重建区段。注意这会删掉你的数据库,所以不要在发布服务器上直接使用这个功能。由于重建Seagull你刚创建的todo表会被删掉,所有我们需要将它保存在Seagull在重建时能找到的地方。

所有让我们先把表的模式文件保存。在todo模块的data目录中创建一个名为schema.my.sql的文件并将你的模式文件复制到这里。

单击'rebuild Seagull',现在不需要担心样品数据的复选框了。

  • 注意:如果你的MySQL用户账户没有删掉数据库权限那你就会有麻烦了。
  • 注意:在写这篇文章时该功能只能在MySQL数据库上实现

现在浏览 http://yourhost.local/seagull/index.php/todo/ 你应该可以正常编辑记录。

:tutorials:maintenance.png

设置模块的默认数据

对于初学者你会注意到在data目录中已经有一个data.default.my.sql文件。它只包含一条记录。这条记录会在安装模块时被插入到module表中,如果成功注册了todo模块那么它就被启用了。单击禁用只是简单的删除了那条记录,就像上面提到的,你可以删除整个模块的文件。

创建导航

接下来,让我们创建导航菜单。我们至少需要一个链接到todo模块的导航元素。从faq模块复制现存的实例。将seagull/modules/faq/data/navigation.php复制到todo模块的data目录。现在我们来编辑它,Seagull保存导航数据在一个单独的PHP数组文件中,内容差不多是这样的

Seagull默认启用了导航模块。导航模块允许你使用嵌套创建复杂的导航层次结构。这意味着你可以很容易的调整任意层次的导航结点。默认的导航数据分成两个分支,用户能访问的导航结点和管理员能看见的导航结点。在下面的例子中我们将简化刚才复制的faq数据并在用户分支添加一个导航元素,但只有管理员才有权限访问。那是因为我们是以管理员身份开发这个模块的,但是我们想查看普通成员和匿名用户所看见的一样的效果和主题。 编辑文件后它应该是这样的:

<?php

$aSections = array(

    //  users
    array (
      'title'           => 'TODO',
      'parent_id'       => SGL_NODE_USER,
      'uriType'         => 'dynamic',
      'module'          => 'todo',
      'manager'         => 'TodoMgr.php',
      'actionMapping'   => '',
      'add_params'      => '',
      'is_enabled'      => 1,
      'perms'           => SGL_ADMIN,
        ),
    );
?>

数组结构是很显然的,你可以随意设置标题,惟一一个比较复杂的元素是SGL_NODE_USER,如果你想将它放在管理员界面分支你可以将些属性设置为SGL_NODE_ADMIN。设置为SGL_NODE_GROUP将会显示在子组里面。你可以查看管理员菜单和它的子菜单为例。

  • 技巧:如果你以管理员登陆进入管理员界面,单击Seagull的LOGO标志可进入普通用户页面。

在结束这部分内容之前请先执行重建Seagull来确保你的表模式,默认数据和导航数据被正确载入。你会看见在用户界面上有一个TODO链接。

添加自定义的Javascript

现在我们需要调用包含在<head>标签中的javascript。先创建一个文件,复制下列的代码并保存成todo.js文件,之后你的模块目录应该是:

|-- todo
|   `-- www
|       `-- js
|           `-- todo.js
<code>

我们要让Seagul载入这个文件,这需要在管理类的display()方法中完成。所以打开classes目录中的TodoMgr.php,找到display方法,可以像下面这样调用js文件:
<code>
$output->addJavascriptFile(array(
    'todo/js/todo.js',
    ));

addJavascriptFile()也接受字符串参数,但是由于我们将要包含多个js文件,所以我们使用了一个数组。

现在细心的读者会注意到模块的javascript目录是在webroot之上,所有你可能想知道浏览器是什么能访问到js文件的呢?

js dir:  seagull/modules/todo/www/js/todo.js
webroot: seagull/www

当你重建Seagull时,Seagull将所有的web资源(modules/$module/www目录中css,js)做软链接到webroot。软链接只在Macs和Linux系统上支持。所以对Windows用户Seagull会复制这些web资源文件到webroot目录。这存在一些争论,因为再微小的变化也必须在重建Seagull之后才能起效。解决方案在这里有描述。

所以执行重构来设置软链接或复制目录。测试js文件是否被正确包含,也是考虑路径,权限等的设置的最简单的方法是将

alert('foo');

放在js文件的顶部并刷新你的浏览器。

下一步我们将Peter的html代码复制到TODO模板文件中。我们使用默认的list动作,编辑todo/templates/todoList.html,将内容替换成这样

现在js已经能正常的载入到网页的<head>标签内,在<body>标签结束之前调用Sortable.create()初始化页面,我们要确保scriptaculous库被正确载入。

Seagull默认提供了scriptaculous库,所以只需要将它包含就可以了。scriptaculous使用逗号分隔的参数列表载入它所需要的模块。使用Firebug可以很容易取得参数,当库开发人员出错时会给你一个错误信息。正确的加载顺序应该是

'js/scriptaculous/src/scriptaculous.js?load=builder,effects,dragdrop',

所以载入你所指定的js库的代码应该是这样的:

$output->addJavascriptFile(array(
    'js/scriptaculous/lib/prototype.js',
    'js/scriptaculous/src/scriptaculous.js?load=builder,effects,dragdrop',
    'todo/js/todo.js',
    ));

添加CSS

要正常显示页面我们还需要使用Peter页面的CSS文件。创建todo.css并将这里的代码复制进去,之后的目录应该是:

|-- todo
|   `-- www
|       `-- css
|           `-- todo.css

要让Seagull在你页面的<head>标签内包含它,你需要将这行代码

$output->addCssFile('todo/css/todo.css');

放到js包含语句下面一行,放在管理类中display方法的任何地方都是可以的。 更新页面确认你的JS和CSS正确载入。

清除代码

正如上面所说的,在本文中只会用到list方法,这是因为所有的其它事情将由Ajax来处理。

所有打开TodoMgr.php删除掉除了list之外所有自动生成的其它行为方法。

  • 技巧:行为方法是以_cmd_为前缀的方法

添加todo_group表

因为我们的TODOs要分组保存,我们需要一个表来代表组的关系。下面我们建议的表定义:

CREATE TABLE `todo_group` (
  `todo_group_id` int(11) NOT NULL,
  `name` varchar(128) NOT NULL,
  `order_id` int(11) NOT NULL,
  PRIMARY KEY  (`todo_group_id`)
);

将这段代码添加到你的模式文件(schema.my.sql)中。

设置认默样品数据

通常当你开发模块或其它的应用程序时,在数据库中保存一些样品数据是很有用的。使用phpMyAdmin或相关的工具,添加一个或两个TODOs样品数据,并添加一些TODO组的记录,并将这些insert语句保存在data.sample.my.sql中。将该文件放在data目录中。 重建Seagull以载入样品数据,记住选中‘with sample data'复选框。

Query for Groups

你可能已经非常熟悉DB_DataObject,我不会在这里详细介绍它。

  • 在TodoMgr.php的_cmd_list方法中,删除$output→pageTitle之后的所有代码
  • 要获得一个基本的组结果集,我们从FaqMgr的list方法中复制代码:
$faqList = DB_DataObject::factory($this->conf['table']['faq']);
$faqList->orderBy('item_order');
$result = $faqList->find();
$aFaqs  = array();
if ($result > 0) {
    while ($faqList->fetch()) {
        $faqList->question = $faqList->question;
        $faqList->answer   = nl2br($faqList->answer);
        $aFaqs[]           = clone($faqList);
    }
}
$output->results = $aFaqs;

将它改成下面这样:

$groupList = DB_DataObject::factory($this->conf['table']['todo_group']);
$groupList->orderBy('order_id');
$result = $groupList->find();
$aGroups  = array();
if ($result > 0) {
    while ($groupList->fetch()) {
        $aGroups[] = clone($groupList);
    }
}
$output->results = $aGroups;

当你刷新浏览器时你会收到一条错误信息

Undefined index: todo_group

意思是说我们需要添加新表名到ableAliases.ini文件中如下:

todo_group = todo_group

  • 技巧: tableAliases.ini在data文件夹中

现在再重建Seagull。如果将来你想要改变表名,你也可以这么做。只需要改变配置文件而不需要更改SQL而你的代码仍能正常工作。

现在在模板文件中就可以使用”results”的值,所以我们遍历组列表。在模板文件的顶部添加下列代码:

{foreach:results,k,oGroup}
{oGroup:r}
{end:}
  • 技巧:Flexy的:r修饰符和print_r()一个变量具有一样的效果。

刷新你的浏览器,记住在validate()将默认动作设置为list。你会得和下面相似的结果集:

DataObjects_Todo_group Object
(
    [__table] => todo_group
    [todo_group_id] => 1
    [name] => Today
    [order_id] => 1
    [_DB_DataObject_version] => 1.8.4
    [N] => 3
    [_database_dsn] => 
    [_database_dsn_md5] => 4a290d26f308959cbefed881d8c78719
    [_database] => seagull
    [_DB_resultid] => 1
    [_resultFields] => 
    [_link_loaded] => 
    [_join] => 
    [_lastError] => 
)

DataObjects_Todo_group Object
(
    [__table] => todo_group
    [todo_group_id] => 2
    [name] => my first sample group
    [order_id] => 2
    [_DB_DataObject_version] => 1.8.4
    [N] => 3
    [_database_dsn] => 
    [_database_dsn_md5] => 4a290d26f308959cbefed881d8c78719
    [_database] => seagull
    [_DB_resultid] => 1
    [_resultFields] => 
    [_link_loaded] => 
    [_join] => 
    [_lastError] => 
)

DataObjects_Todo_group Object
(
    [__table] => todo_group
    [todo_group_id] => 3
    [name] => my second sample group
    [order_id] => 3
    [_DB_DataObject_version] => 1.8.4
    [N] => 3
    [_database_dsn] => 
    [_database_dsn_md5] => 4a290d26f308959cbefed881d8c78719
    [_database] => seagull
    [_DB_resultid] => 1
    [_resultFields] => 
    [_link_loaded] => 
    [_join] => 
    [_lastError] => 
)

我们现在需要将结果输出到divs中,所以我们为每个组取得todo记录。替换模板代码为:

{foreach:results,k,oGroup}
	<div id="group{k}" class="section">
		<h3 class="handle">{oGroup.name}</h3>
	</div>
{end:}
  • 技巧:Flexy中所以包含在{}中的东西都被视为一个变量。

刷新浏览器,结果应该是从样品数据中取出的三个组名称。Flexy支持添加自定义的HTML元素属性,所以你可以重写上面代码为更简洁的:

<div flexy:foreach="results,k,oGroup" id="group{k}" class="section">
	<h3 class="handle">{oGroup.name}</h3>
</div>

这是保持你的模板简洁的一个技巧,所以页面设计人员可以使用Dreamwearver打开或相关的工具打开。

从TODO组中取得TODO列表项

我们想要做的是获取所有组的列表,然后遍历每个组的todo列表。我们要创建一个辅助方法来通过组ID获得所有的todo列表项目。我们不是在通过DB_DataObject对象来生成Todo_group实体类(查看seagull/var/cache/entities/*)创建这个方法,而是会把所有的SQL逻辑放在管理类中然后提取到数据存取对象中。创建getTodosByGroupId($groupId)方法

function getTodosByGroupId($groupId)
{
    $query = 
        "SELECT todo_id, description
         FROM {$this->conf['table']['todo']}
         WHERE group_id = {$groupId}
         ORDER BY order_id";
     $aRes = $this->dbh->getAll($query);
     return $aRes;
}

注意$this→dbh(数据库对象的参考)和$this→conf(全局配置信息数组的参考)已经是可用的。这对所有继承SGL_Manager的类都会有效的,这两个资源几乎在所有的行为方法中使用。

所以,新的结果集中包含的数据对象应该是这样的:

DataObjects_Todo_group Object
(
    [__table] => todo_group
    [todo_group_id] => 1
    [name] => Today
    [order_id] => 1
    [_DB_DataObject_version] => 1.8.4
    [N] => 3
    [_database_dsn] => 
    [_database_dsn_md5] => 4a290d26f308959cbefed881d8c78719
    [_database] => seagull
    [_DB_resultid] => 1
    [_resultFields] => 
    [_link_loaded] => 
    [_join] => 
    [_lastError] => 
    [aTodos] => Array
        (
            [0] => stdClass Object
                (
                    [todo_id] => 1
                    [description] => my first sample todo
                )

            [1] => stdClass Object
                (
                    [todo_id] => 2
                    [description] => my second sample todo
                )

        )

)

哪一个会容易集成到我们的模板中呢?

Tweaking the Javascript using Scriptaculous

我稍微修改了Peter的代码因为我想让每个组都有它自己的'Add Todo'表单。也就是说用户可以直接添加项目到某个组当中,如添加”call plumber”到”domestic”组。新的组框和小部件是使用Scriptaculous的Builder对象创建的。如果你还末使用过,它是你所能使用的一个直接了当的DOM生成器。

使用Ajax保存TODOs

这是最简单的部分 - 在Seagull你可以使用在独立的Prototype脚本所使用的语法。可以肯定的是Scriptaculous/Prototype API已经是简洁而且是一流的,没有必要进行再包装,强迫开发人员再去学另一种语法,增加额外的负担。

Seagull的最近一次修改意味着当SGL_Request解析$_REQUEST时,请求的header被解析。所以如果检测到一个Ajax调用,它将会执行一个轻量级的过滤链。不搜索URL alias(对alias的解释可以是:路由),因为标准的Seagull URLs对Ajax调用也是可用的。看这边的代码

所以对onclick事件我们只是使用了一个典型的rototype Ajax.request()方法,如下:

<input 	type="button" 
        onClick="
            createNewTodo($('new_todo_{oGroup.todo_group_id}').value, $('group{oGroup.todo_group_id}').id);
            new Ajax.Request(
                '{makeUrl(#addTodo#,##,#todo#)}', 
                {asynchronous:true, 
                 method:'post',
                 parameters:'todo=' + $('new_todo_{oGroup.todo_group_id}').value + '&groupId='+ {oGroup.todo_group_id},
                 onSuccess:handlerFunc}); 
            return false;" 
		value="Add Todo" />	

第一个方法在DOM中创建了todo,然后Ajax.Request将它保存在数据库中。唯一一个不平常的地方需要注意的是URL中的第二个参数,makeUrl Flexy方法不需要第二个参数。

{makeUrl(#addGroup#,##,#todo#)}
  • 提醒: makeUrl(#action#,#manager#,#module#)

之所以不需要第二个参数是因为对于Ajax调用,整个模块使用了一个简单的数据存取类型的对象所以只知道行为名和模块名就足够了。我之所以说数据存取类型,是因为它和现存的Seagull数据存取对象相似,但它也允许返回HTML,简单地说它提供了一个Ajax调用所能请求的任何数据。

创建Ajax Provider

只要在你的AjaxProvider前面加上你的模块名,那么它会在出现Ajax调用时自动载入。所以你需要为todo模块创建一个名为TodoAjaxProvider.php的文件,并将它保存在classes目录。

复制下列代码到你的TodoAjaxProvider.php文件:

<?php

require_once 'DB/DataObject.php';
require_once 'HTML/Template/Flexy.php';

class TodoAjaxProvider extends SGL_Manager
{
    function TodoAjaxProvider()
    {
        SGL::logMessage(null, PEAR_LOG_DEBUG);
        parent::SGL_Manager();
        $options = &PEAR::getStaticProperty('DB_DataObject', 'options');
        $options = array(
            'database'              => SGL_DB::getDsn(SGL_DSN_STRING),
            'schema_location'       => SGL_ENT_DIR,
            'class_location'        => SGL_ENT_DIR,
            'require_prefix'        => SGL_ENT_DIR . '/',
            'class_prefix'          => 'DataObjects_',
            'debug'                 => $this->conf['debug']['dataObject'],
            'production'            => 0,
            'ignore_sequence_keys'  => 'ALL',
            'generator_strip_schema' => 1,
        );
    }
}

我们不会输出Flexy模板的内容,但是稍后将会有使用的例子。DB_DataObject是在构造函数中初始化的,不久将会被提取出来。所以现在的问题是取得传送过来的变量,并执行数据库操作。

创建addTodo() Ajax 方法

正如你在上面的Prototype代码中看到的那样,参数'todo'和'groupId'是通过POST传递给AjaxProvider。所以我们第一个要做的是获取这两个参数。我们可以测试返回了什么值,因为回调函数'handlerFunc'将返回的Ajax值输出到一个隐藏的id为'response'的div中。这样调试就容易了。所以请添加下面代码到你的CSS文件:

#response {
	line-height: 10px;
	background-color: green;
	padding: 10px;
	width: 400px;
	margin: auto;
	color: #fff;
	font-weight: bold;
}

当你做Ajax请求时,单击'Add Todo'时的返回值,也就是响应的输出将显示在response div内。Any problems with typos, variable values or includes can be resolved easily.

所以TodoAjaxProvider::addTodo()方法应该是很简单的:我们先获取POST参数,将它们保存在DataObject。代码如下:

    function addTodo()
    {
        $todoTxt = SGL_Registry::singleton()->getRequest()->get('todo');
        $groupId = SGL_Registry::singleton()->getRequest()->get('groupId');
        
        $nextId = $this->dbh->nextId($this->conf['table']['todo']);
        $todo = DB_DataObject::factory($this->conf['table']['todo']);
        $todo->todo_id = $nextId; 
        $todo->description = $todoTxt;
        $todo->status = 1;
        $todo->order_id = $nextId;
        $todo->group_id = $groupId;
        $success = $todo->insert();
        
        //return SGL_Registry::singleton()->getRequest()->debug();
        if ($success) {
        	$ret = 'Todo added successfully';
        } else {
        	$ret = 'There was a problem adding the todo';
        }
        return $ret;
    }
  • 技巧:注意注释掉的debug调用,这是用来检查你的请求数值的非常方便的方法

:tutorials:debug.png

总结

在这篇教程中我们涉及到了Seagull框架的下列这几个方面:

  • 使用Seagull的模块生成器创建一个模块
  • 学习关于重建Seagull应用程序环境的知识
  • 添加自定义的javascript和CSS文件到管理类中
  • 创建并保存导航,默认和样品数据
  • 使用DB_DataObject来检索数据库
  • 使用Scriptaculous和Prototype来集成Ajax的web 2.0效果

我希望你喜欢这个教程并对你有用,希望你能给我反馈信息。

下载代码

我对最后发布的todo模块做了一些改进,你可以在这里下载。改进了很多地方,包括保存todo和groups的顺序,以及每个todo项的状态,如完成了或得是末完成。你也可以在线查看效果。

附件

  • todo.tar.gz (6.1 kB) -完成的todo模块,demian 在 01/05/07 12:28:47 添加.
  • debug.png (35.2 kB) - 调试Ajax请求,demian 在 01/05/07 20:59:10 添加.
  • maintenance.png (50.1 kB) -维护界面,demian 在 01/05/07 20:59:50 添加.
  • module_generator.png (24.7 kB) -t模块生成器,demian 在 01/05/07 21:00:30 添加.
  • files.png (41.6 kB) - 文件框架,demian 在 01/05/07 21:18:56 添加.
 
tutorials/buildinganajaxenabledmodule.txt · 最后更改: 2010/05/30 00:21 (外部编辑)
 
Except where otherwise noted, content on this wiki is licensed under the following license:GNU Free Documentation License 1.2