控制器

控制器是网页应用程序的关键类. 它接收浏览器的请求, 从模型(model)的角度调用业务逻辑, 在基本的结果上生成视图(view)的HTML, 返回请求响应.

定义操作(Actions)

操作(action)是基于请求URL的一种叫法, 它决定了调用控制器(controller)定义的何种方法.

让我们给已经生成的骨架上增加一些操作(Action). 首先, 在头文件声明的’pulic slots’部分的操作(action). 请注意, 如果你希望增加操作(action)的参数时, 这些参数应该声明成QString类型. 当你声明任何其类型时, 它们将不被识别(通过正常的方法). 最多可以定义10个参数.

class T_CONTROLLER_EXPORT FooController : public ApplicationController
{
    Q_OBJECT
     :
public slots:   // 动作(action)定义在这里
    void bar();  
    void baz(const QString &str);
     :

完成后, 按照往常一样实现这些操作(actions).

操作(action)的选择判断

当一个URL字符串被请求时, 需要决定哪个操作(action)被调用才是正确的. 默认情况下, 将使用下面的规则.

/controller-name/action-name/argument1/argument2/ ...

这些参数将被当成”操作(Action)参数”.

如果使用了超过10个参数, 只有10个参数有效. 如果你希望使用11个或者更多参数, 请按照下面的方法使用Post或者URL数据参数.

这里有一些具体的例子. BlogContrller类的操作(actions)被每个用例调用.

/blog/show        -> show();
/blog/show/2      -> show(QString("2"));
/blog/show/foo/5  -> show(QString("foo"), QString("5")); 

如果省略操作(Action), 默认情况下会调用index操作. 这个看起来像这样:

/blog   -> index();

如果请求的URL对应的操作(action)没有定义, 将返回状态码500(内部服务器错误)给浏览器.

在大多数情况下,每个调用的操作(action)都会进行以下处理过程:

  • 请求的审查
  • 访问cookies和会话
  • 访问上传的文件
  • 调用模型(业务逻辑)
  • 传递变量到视图(view)
  • 请求建立HTML反馈

这些对于编程者来说, 通常意味着许多的工作和大量的代码编写. 因此, 导致的结果就是一些人编写了一些庞大而且复杂的控制器(controller). 为了防止这种情况, 请仅在模型(model)实现业务逻辑. 你应该一直保存控制器尽可能的简单.

一旦你明白了操作(action)如何调用的机制, 还能够通过URL客户化操作(action). 更多信息请参见URL 路由.

获取请求

一个HTTP请求由THttpRequest类表示. 在控制器(controller)中, 你可以用httpRequest()方法获得ThttpRequest对象.

const THttpRequest & TActionController::httpRequest () const;

你可以在这里获得各自数据.

接收请求数据

从浏览器发送的HTTP请求包含方法(method), 头(header), 主体(body). 到达服务器的数据,可以通过以下方式:

  • post数据 - 使用POST方式从表单提交的数据
  • URL参数(查询参数) - 附加在URL上在”?”后的参数,(格式为Key=value&…)
  • Action参数 - 在Action后的参数(“blog/edit/3”的第三部分) <-见上面的内容.

接下来的例子说明了如何在控制器中获取post数据. 我们假设视图中已经有了一个标签.

<input type="text" name="title" />

在服务器端要获取浏览器发送给控制器的值, 使用下面的代码:

QString val = httpRequest().formItemValue("title")

同时, 如果你希望将值转变为int整形, 可以使用Qt的toInt()方法.

如果有大量的数据项发送, 一个一个的获取显得有点麻烦. 这里有另外的一种方法一次性获取所有数据. 例如, 你可以将标签写成这样:

<input type="text" name="blog[title]" />
<input type="text" name="blog[body]" />

在控制器内, 可以这样获得数据:

 QVariantMap blog = httpRequest().formItems("blog");
 QVariant t = blog["title"];
 QVariant b = blog["body"];
  :

请求的数据可以表现为hash格式.

如何获取URL参数(查询参数)?这里有个例子:

http://example.com/blog/index?mode=normal

你可以使用下面的方法获得blog控制器(controller)的Index操作(action)的mode的值.

QString val = httpRequest().queryItemValue("mode");  //val ="narmal"

同时, 如果你想获得不是URL参数和post数据的数据, 你可以使用allParameters()和methods().

为了在应用服务器端检查请求的数据是否是要求的形式, 还提供了验证器功能. 更多信息请参见验证器章节.

传递变量到视图(view)

要传递变量到视图(view), 使用texport(variable)或者T_EXPORT(variable)宏. 你可以指定的参数类型为QVariant, int, QString, QList, 或者 QHash. 当然你也可以指定一个自定义的模型(model)这儿是它的用法:

 QString foo = "Hello world";
 texport(foo);
 // 或者
 int bar;
 bar = ...
 texport(bar);

说明:texport的参数必须指定为变量. 你不能直接指定一个字符串(“Hello world”)或者数字(例如100).

在视图(view)内要使用变量, 你必须首先用tfetch(Type,variable)声明变量. 更多详细内容请参见视图(view)章节.

概要: 使用tfetch()传递一个对象到视图(view).

** 用户自定义的类的情况**

当你需要传递一个新的类(自定义的类)到视图(view), 请件下面的宏增加到头文件的尾部. 更多详细的信息请参见 “新建原始模型(original model)”节.

 Q_DECLARE_METATYPE(ClassName)     // 请更换类名

Qt Q_DECLARE_METATYE已经提供的类不需要再声明. 但是, 如果需要传递一个模版类, 声明还是需要的, 如QHash和QList在这个列子中, 请在helpers/applicationhelper.h尾部添加下面的内容

    :
 Q_DECLARE_METATYPE(QList<float>)

Q_DECLARE_METATYPE 宏的参数是一个类名. 但是,如果你包含了一个逗号(如QHash <Foo, Bar>), 将会产生编译错误. 在这种情况下, 名字应该使用typedef QHash <Foo, Bar>来声明.

 typedef QHash<Foo, Bar> BarHash;
 Q_DECLARE_METATYPE(BarHash)

导出对象

我们视导出对象到视图(view)(在texport()方法设置的对象)为”导出对象”.

请求建立反馈内容

在处理完业务逻辑后, 处理的结果作为HTML反馈将会返回. 如果BlogController show 操作(action)被执行, 在views/blog/show.xxx中的反馈内容被模版名(扩展名基于模版系统)生成. 要请求反馈内容, 你可以使用render()方法.

 bool render(const QString &action = QString(), const QString &layout = QString());

如果你想反馈一个不同的模版, 指定操作(action)名为参数即可. 布局(layout)文件名可以被指定为布局参数.

方法render()表示”请求 view/template 建立反馈内容”. 这也称之为”绘图(drawing)”.

概要:要渲染模版,使用 render() 方法.

布局(Layout)

当年创建一个网站的是, 网页页眉, 网页页脚还有一些其他的部分通常所有的页面中是通用的, 只是网页的内容不同而已. 术语布局(layout)是给这些通用的部分使用的, 模版基于这些通用的部分. 布局(layout)文件应该放在views/layouts文件夹内.

关于模版系统

TreeFrog目前为止采用了两种模版系统:OtamaERB. 在ERB内, 代码通过<%…%>嵌入. 在Treefrog内, Otama系统有不同的处理方式, 因为它完全分离了逻辑(.tom)和界面模版(.html).

  • ERB 使用文件扩展名: xxx.erb
  • Otama 使用文件扩展名: xxx.otm和xxx.html
    (xxx指操作(action)的名称)

定向到渲染一个字符串

要直接渲染一个字符串,使用 renderText()方法.

 // "Hello world" 渲染这个字符串
 renderText("Hello world");

默认情况下, 是不使用布局(layout)的. 如果想要使用布局, 你需要指定第二个参数为true.

 // 渲染字符串 "Hello world" 同时使用布局(layout)
 renderText("Hello world", true);

关于布局(layout)的更多详细的信息请参见 视图(view) 章节.

重定向

要重定向浏览器到另外的URL, 可以使用redirect()方法. 第一个参数, 指定一个QUrl类的实例.

 // 重定向到 www.example.org 
 redirect(QUrl("www.example.org"));

你可以重定向到同一台主机的其他操作(action)上.

 // 重定向到Blog控制器的index操作
 redirect( url("blog", "index") );

下面是另外一个有用的url方法. 它在你传递控制器名和操作名时返回相应的QUrl实例.

要重定向到同一个控制器内的其他操作,可以忽略控制名.

 // 重定向到同一个控制器的show操作
 redirect( urla("show") );

使用 urla()方法是非常重要的.

在重定向目标页显示信息

重定向指向了另外一个URL. 这是因为, 从服务器的视角看, URL接收到了一个新的目标页, 这样产生了调用另外一个控制器的操作的效果.

Treefrog框架有一种机制(通过变量)可以实现传递信息到重定向的控制器上. 见下面的例子使用tflash()或T_FLASH()方法传递变量.

 // foo - 瞬时对象(flash object)
 QString foo = "successfully";
 tflash( foo );

传递的对象在这里叫做”瞬时对象(flash object)”.

瞬时对象(flash object)转换成重定向后的视图(view)的输出对象. 正因为如此, 它能够被echo()或者eh()方法输出显示信息.

使用瞬时对象(flash object)的好处

事实上, 你可以创建一个完全没有瞬时对象(flash object)的网页应用. 然而, 在遇到合适的情况下, 如果你使用它, 代码变得非常容易理解(一旦你开始用它).

我个人认为保持每个操作(action)完全对立, 减少各个操作(action)之间的依赖是比较好的也比更容易理解. 我建议不要在单个请求上调用超过一个以上的操作(action). 尽可能的保存一个请求一个操作(action)的关系.

使用瞬时对象(flash object)时, 如果为几乎相同的内容建立独立分离的显示, 代码可以变得简单些. blogapp (教程)使用createshow 操作(action)的方式就是一个好的例子. 虽然这些操作(action)的处理是不一样的, 但都是仅仅显示一条blog的处理结果. 在create操作, 成功完成录入后, 这个结果将显示在重定向的show操作上. 同时, 这个信息”Created successfully.” 通过使用瞬时对象(flash object)显示.

事实上, 一个应用不会都是这么简单的, 因为不是经常需要获取和使用瞬时对象(flash object). 因此, 适度地使用它好了.

综上所述, 在实践中大部分的操作(action)都归结于使用redirect()或者render()方法.

顺便说一下
按照上面所说的方式, 但一个重定向发生时, 一旦访问到达服务器时处理就被中断了(服务器返回新URL, 客户端按新URL再次发出请求). 鉴于瞬时对象(flash object)是通过会话的方式实现的, 存活下来的对象可以在其他操作(action)环境下使用.

staticInitialize() 方法

当启动后, 程序只有一个进程. 你可能希望提前从数据库读取一些初始化信息.

这种情况下, 将处理过程写在ApplicationController#staticInitialize()内.

 void ApplicationController::staticInitialize()
 {
  // 处理过程..
 }

当服务器进程启动后, staticInitialize()方法只会调用一次. 然而, 当你选择在MPM)中描述的PreFork后, 你必须要关注它, 不要让每次进程启动时建立太多的加载功能.

控制器(controller)实例的生命周期

控制器(controller)的实例在被调用前创建, 当操作(action)执行完后被销毁. 这意味着控制器的创建和销毁是基于每个HTTP请求的.
这样规定后, 控制器(controller)通常没有实例变量. 请按同样的方式实现ApplicationController.