教程

让我们新建一个Treefrog应用程序.
我们将尝试生成一个简单的博客系统,它可以列出(list), 查看(view)和添加(add)/编辑(edit)/删除(delete)文字.

生成应用程序框架

首先,我们将需要生成一个框架(各种配置文件和目录树)我们将使用”Blogapp”做为应用的名字. 从命令行执行以下命令.(在Windows请从Treefrog命令行窗口执行)

$ tspawn new blogapp
  created   blogapp
  created   blogapp/controllers
  created   blogapp/models
  created   blogapp/ models/ sqlobjects
  created   blogapp/ views
  created   blogapp/ views/ layouts
  created   blogapp/views/mailer
  created   blogapp/views/partial
   :

新建表

现在我们需要在数据库中创建一个表. 我们将创建title和content(body)字段. 这里是MySQL和SQLite的范例.

MySQL范例:
设置字符集为UTF-8. 你也可以在生成数据库的时候定义它(确保它设置正确,见常见问题FAQ), 你可按下面的描述定义数据库的配置文件:也可以使用命令行工具在MySQL中生成数据库.

$ mysql -u root -p
Enter password:
mysql> CREATE DATABASE blogdb DEFAULT CHARACTER SET utf8mb4;
Query OK, 1 row affected (0.01 sec)
mysql> USE blogdb;
Database changed
mysql> CREATE TABLE blog (id INTEGER AUTO_INCREMENT PRIMARY KEY, title  VARCHAR(20), body VARCHAR(200), created_at DATETIME, updated_at DATETIME, lock_revision INTEGER) DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.02 sec)
mysql> DESC blog;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | int(11)      | NO   | PRI | NULL    | auto_increment |
| title         | varchar(20)  | YES  |     | NULL    |                |
| body          | varchar(200) | YES  |     | NULL    |                |
| created_at    | datetime     | YES  |     | NULL    |                |
| updated_at    | datetime     | YES  |     | NULL    |                |
| lock_revision | int(11)      | YES  |     | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+
6 rows in set (0.01 sec)
mysql> quit
Bye

SQLite范例:
我们将把数据库文件放在DB目录内.

$ cd blogapp
$ sqlite3 db/blogdb
SQLite version 3.6.12
sqlite> CREATE TABLE blog (id INTEGER PRIMARY KEY AUTOINCREMENT, title VARCHAR(20), body VARCHAR(200), created_at TIMESTAMP, updated_at TIMESTAMP, lock_revision INTEGER);
sqlite>. quit

表blog被创建, 有以下字段: id, title, body, created_at, updated_at, and lock_revision.

使用字段updated_at和created_at, Treefrog将在每次更新时自动插入日期和时间. lock_revision字段, 用于配合乐观锁使用, 需要使用整形integer创建.

乐观锁(Optimistic Locking)

乐观锁被用来存储数据同时检查信息没有被其他用户更新. 因为没有实际的写锁, 你可以期待处理更快一点. 更多信息可参见O/R映射章节.

设置数据库信息

使用config/database.ini设置数据库信息.
在编辑器中打开文件,在[dev]处为每个配置项输入恰当的值, 然后点击保存.

MySQL范例:

[dev]
DriverType=QMYSQL
DatabaseName=blogdb
HostName=
Port=
UserName=root
Password=pass
ConnectOptions=

SQLite范例:

[dev]
DriverType=QSQLITE
DatabaseName=db/blogdb
HostName=
Port=
UserName= 
Password= 
ConnectOptions=

一旦你正确完成了这些设置, 就可以显示数据库的表.
如果所有项都被正确设置了, 将显示一条信息如下:

$ cd blogapp
$ tspawn --show-tables
DriverType:   QSQLITE
DatabaseName: db\blogdb
HostName:
Database opened successfully
-----
Available tables:
 blog

如果需要的SQL驱动没有包含在Qt SDK中, 下面的错误信息将出现:

QSqlDatabase: QMYSQL driver not loaded

如果收到此消息,则可能未安装Qt SQL驱动程序. 安装RDBM的驱动程序.

可以通过下面的命令检查哪些SQL驱动已经安装;

$ tspawn --show-drivers
Available database drivers for Qt:
 QSQLITE
 QMYSQL3
 QMYSQL
 QODBC3
 QODBC

内建的SQL驱动可以用于SQLite, 虽然也可以通过完成一点点工作来使用SQLite驱动.

定义一个模版系统

在Treefrog框架中, 我们可以定义Otama或者ERB作为模版系统. 我们将在development.ini文件中设置TemplateSystem参数.

TemplateSystem=ERB
  or
TemplateSystem=Otama

自动从表生成代码

从命令行执行生成器(tspawn)命令生成下面的代码. 下面的例子展示了控制器(controller), 模型(model)和视图(view)的生成. 表名作为命令的参数.

$ tspawn scaffold blog
DriverType: QSQLITE
DatabaseName: db/blogdb
HostName:
Database open successfully
  created   controllers/blogcontroller.h
  created   controllers/blogcontroller.cpp
  updated controllers/ controllers. pro
  created models/ sqlobjects/ blogobject. h
  created models/ blog. h
  created models/ blog. cpp
  updated models/ models. pro
  created views/ blog
    :

使用tspawn选项可以生成/更新模型(model)/视图(view).

tspawn命令的帮助:

 $ tspawn --help
 usage: tspawn <subcommand> [args]

 Type 'tspawn --show-drivers' to show all the available database drivers for Qt.
 Type 'tspawn --show-driver-path' to show the path of database drivers for Qt.
 Type 'tspawn --show-tables' to show all tables to user in the setting of 'dev'.
 Type 'tspawn --show-collections' to show all collections in the MongoDB.

 Available subcommands:
   new (n)         <application-name>
   scaffold (s)    <table-name> [model-name]
   controller (c)  <controller-name> action [action ...]
   model (m)       <table-name> [model-name]
   usermodel (u)   <table-name> [username password [model-name]]
   sqlobject (o)   <table-name> [model-name]
   mongoscaffold (ms) <model-name>
   mongomodel (mm) <model-name>
   validator (v)   <name>
   mailer (l)      <mailer-name> action [action ...]
   delete (d)      <table-name or validator-name>

Build源代码

开始Build进程前, 执行下面的命令一次(仅一次), 它将会生成一个Makefile文件.

$ qmake -r "CONFIG+=debug"

一个WARNING信息将会显示,不过事实上没有影响. 接下来, 执行make命令来编译控制器(controller), 模型(model), 视图(view)和工具助手(helper).

$ make     (MSVC 执行'nmake' 命令代替'make')

如果构建成功, 4个共享库(controller, model, view, helper)将出现在lib文件夹. 默认情况下,这些生成的库的debug模式的, 不过, 你可以重新生成Makefile文件, 使用下面的命令来生成release模式的库.

生成release模式的Makefile文件:

$ qmake- r" CONFIG+= release"

启动应用服务器

在启动应用服务器(AP server)前改变应用的根目录. 服务器将会把命令执行的路径当作应用的根目录启动. 按Ctrl+c停止服务器.

$ treefrog -e dev

在Windows下, 使用treefrogd.exe启动.

> treefrogd.exe -e dev

在Windows下, 当你构建debug模式的网页应用时,使用treefrogd.exe启动, 当你构建release模式的网页应用时, 使用treefrog.exe启动.#### Release and debug 模式不能混着使用, 否则不能正常工作</span>.

如果希望在后台运行, 可配合任何其他需要的选项使用-d选项.

$ treefrog -d -e dev

命令选项-e出现在上面的例子中. 在使用命令跟随着一个在database.ini中已定义的节名前, 可以用来它来更改数据库设置. 如果没有定义节名, 系统假设命令参数是product(当项目生成时, 下面三个节名是预定义的).

| 节 | 描述 | | ——–| ———————-| | dev | 用于生成代码,开发 | | test | 用于测试 | | product | 用于官方版本,生产版本 |

‘-e’来源于’environment’的首字母.

停止命令:

$ treefrog- k stop

终止命令(强制终止):

$ treefrog- k abort

重启命令:

$ treefrog- k restart

如果有防火墙, 确保使用的端口是允许的(默认的端口号是8800).

作为参考,以下命令显示了当前的URL路由信息.

 $ treefrog --show-routes
 Available controllers:
   match   /blog/index  ->  blogcontroller.index()
   match   /blog/show/:param  ->  blogcontroller.show(id)
   match   /blog/create  ->  blogcontroller.create()
   match   /blog/save/:param  ->  blogcontroller.save(id)
   match   /blog/remove/:param  ->  blogcontroller.remove(id)

浏览器访问

我们将使用浏览器访问http://localhost:8800/Blog. 一个列表界面, 如下图将会显示.

起初, 这里没有任何记录.

Listing Blog 1

当录入两条记录后, 选项show, edit 和 remove就可见了. 如你所见, 显示日文是没有问题的(译者说明:中文也没有问题, 数据库和界面都使用UTF-8字符集).

Listing Blog 2

Treefrog使用了一种方法机制(路由系统Routing system)来实现从请求的URL到控制器(controller)的动作(action)(如同其他框架一样).
已开发的源代码可以工作在其他平台上, 只要源代码被重新构建.

你点击这里查看一个简单的网页应用.
你可以在使用一下这个应用,它将以桌面应用的平均速度响应.

控制器(Controller)的源码

让我们看看生成的控制器的内容.
首先, 头文件这里有几个宏代码, 它们在URL发送时需要用到.

public slot的目的是声明希望发送的操作(actions)(methods).CRUD对应的操作(actions)已被定义. 顺便说一句, 关键字slot在Qt扩展的一个功能. 更多详细信息请参考Qt文档.

class T_CONTROLLER_EXPORT BlogController : public ApplicationController
{
    Q_OBJECT
public:
    Q_INVOKABLE
    BlogController(){}
    BlogController( const BlogController& other);
    public slots:
    void index();                     // 列出所有记录
    void show(const QString &id);     // 显示记录
    void create();                    // 新建记录
    void save(const QString &id);     // 更新(保存)
    void remove(const QString &id);   // 删除一条记录
};

T_DECLARE_CONTROLLER(BlogController, blogcontroller)     //宏

接下来, 看看源文件.
源文件代码有点长, 需要一点耐心.

#include "blogcontroller.h"
#include "blog.h"

BlogController::BlogController(const BlogController &)
    : ApplicationController()
{ }

void BlogController::index()
{
    auto blogList = Blog::getAll();  // 取得所有Blog对象的列表
    texport(blogList);               // 传递列表的值到视图(view)
    render();                        // 渲染视图 (模版template)
}

void BlogController::show(const QString &id)
{
    auto blog = Blog::get(id.toInt()) ;  // 通过主键取得Blog模型(model)
    texport(blog); 
    render();
}

void BlogController::create()
{
    switch (httpRequest().method()) { // 检查http请求的方法类型(method type)
	
        case Tf::Get:
                 render();
                 break;
				 
        case Tf::Post: {
                 auto blog = httpRequest().formItems("blog"); // 保存从'QVariantMap'类型来的'blog'变量的表单数据 
                 auto model = Blog::create(blog);             // 从POST新建对象
                 
				 if (!model.isNull()) {
                     QString notice = "Created successfully.";
                     tflash(notice);                      // 设置瞬时信息
                     redirect(urla("show", model.id()));  // 重定向到show action
                 } else {
                     QString error = "Failed to create."; // 对象创建失败
                     texport(error);
                     texport(blog);
                     render();
                 }
                 break;
        }
        default:
               renderErrorResponse(Tf::NotFound);       // 显示一个错误页面
               break;
    }
}

void BlogController::save(const QString &id)
{
    switch (httpRequest().method()) {
    case Tf::Get: {
        auto model = Blog::get(id.toInt()); // 取得一个要更新的对象
        if (!model.isNull()) {
            session().insert("blog_lockRevision", model.lockRevision()); // 设置锁版本
            auto blog = model.toVariantMap();
            texport(blog);                  // 发送blog到视图(view)
            render();
        }
        break; 
    }

    case Tf::Post: {
        QString error;
        int rev = session().value("blog_lockRevision").toInt(); // 获得lock revision
        auto model = Blog::get(id.toInt(), rev);                // 通过ID获得blog
        
        if (model.isNull()) {
            error = "Original data not found. It may have been updated/removed by another transaction.";
            tflash(error);
            redirect(urla("save", id));
            break;
        }

        auto blog = httpRequest().formItems("blog");
        model.setProperties(blog);              // 设置请求的数据
        if (model.save()) {                     // 保存对象
            QString notice = "Updated successfully.";
            tflash(notice);
            redirect(urla("show", model.id())); // 重定向到 show 操作(action)
        } else {
            error = "Failed to update.";
            texport(error);
            texport(blog);
            render();
        }
        break; 
    }

    default:
        renderErrorResponse(Tf::NotFound);
        break;
    }
}

void BlogController::remove(const QString &pk)
{
    if (httpRequest().method() != Tf::Post) {
        renderErrorResponse(Tf::NotFound);
        return;
    }

    auto blog = Blog::get(id.toInt());   // 取得Blog对象
    blog.remove();                       // 删除对象
    redirect(urla("index"));
}

// Don't remove below this line
T_REGISTER_CONTROLLER(blogcontroller)    // 宏

Lock revision用来实现乐观锁. 参考后续的模型(model)获取更多信息.

如你所见,你可以使用texport方法来传递数据都视图(view)(模版template). texport方法的参数是一个QVariant对象. QVariant可以是任何类型, 所以int, QString, QList和QHash可以传递任何对象. 更多关于QVariant的详细信息, 请参考Qt文档.

视图(View)机制

目前Treefrog已经实现了2种模版系统(template systems). 它们是Otama和ERB. 和Rails类似, ERB用来嵌入C++代码到HTML中.

生成器自动生成的视图(view)默认是ERB文件.因此, 让我们看看index.erb的内容.如你所见, C++代码包含在<%…%>中.当index操作(action)调用render方法时, index.erb的内容作为响应被返回.

<!DOCTYPE HTML>
<%#include "blog.h" %>
<html>
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title><%= controller()->name() + ": " + controller()->activeAction() %></title>
</head>
<body>
<h1>Listing Blog</h1>

<%== linkTo("New entry", urla("entry")) %><br />
<br />
<table border="1" cellpadding="5" style="border: 1px #d0d0d0 solid; border-collapse: collapse;">
  <tr>
    <th>ID</th>
    <th>Title</th>
    <th>Body</th>
  </tr>
<% tfetch(QList<Blog>, blogList); %>
<% for (const auto &i : blogList) { %>
  <tr>
    <td><%= i.id() %></td>
    <td><%= i.title() %></td>
    <td><%= i.body() %></td>
    <td>
      <%== linkTo("Show", urla("show", i.id())) %>
      <%== linkTo("Edit", urla("save", i.id())) %>
      <%== linkTo("Remove", urla("remove", i.id()), Tf::Post, "confirm('Are you sure?')") %>
    </td>
  </tr>
<% } %>
</table>

接下来, 让我们看看Otama模版系统.

Otama模版系统系统将界面逻辑从模版中完全分离出来. 模版写成HTML文件, 掩码元素作为节的开始标识插入到HTML文件中, 掩码元素会被动态改写. 界面逻辑文件, 由C++代码编写, 提供关于掩码的逻辑.

下面的范例是index.html, 当定义为Otama模版系统时由生成器生成. 它可以包含文件数据, 不过你将会看到, 如果你用浏览器直接打开它, 因为它使用了HTML5, 设计在没有数据的情况下完全没有崩溃.

<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
  <title data-tf="@head_title"></title>
</head>
<body>
<h1>Listing Blog</h1>
<a href="#" data-tf="@link_to_entry">New entry</a><br />
<br />
<table border="1" cellpadding="5" style="border: 1px #d0d0d0 solid; border-collapse: collapse;">
  <tr>
    <th>ID</th>
    <th>Title</th>
    <th>Body</th>
    <th></th>
  </tr>
  <tr data-tf="@for">               <-标记'@for'
    <td data-tf="@id"></td>
    <td data-tf="@title"></td>
    <td data-tf="@body"></td>
    <td>
      <a data-tf="@linkToShow">Show</a>
      <a data-tf="@linkToEdit">Edit</a>
      <a data-tf="@linkToRemove">Remove</a>
    </td>
  </tr>
</table>
</body>
</html>

一个自定义的属性’data-tf’用来打开掩码. 这个自定义数据属性是在HTML5中定义的”@”打头的字符串用来当作掩码的值.

接下来, 让我们看看index.otm对应的界面逻辑.
链接到相应逻辑的掩码(mask)在上面的模版中声明(declare), 在空行前持续有效.这个逻辑包含在C++代码中.

还使用了操作符(如 == ~ =). 这些操作符控制不同的行为(更详细的信息参见后续章节)

#include "blog.h"  <-像C++代码一样include blog.h
@head_title ~= controller()->controllerName() + ": " + controller()->actionName()

@for :
tfetch(QList<Blog>, blogList);  /* 声明对象,使用从控制器(controller)传递过来的数据 */
for (QListIterator<Blog> it(blogList); it.hasNext(); ) {
     const Blog &i = it.next();        /* 参考 Blog 对象 */
     %%      /* 通常用于循环语句, 重复子元素  */
}

@id ~= i.id()   /* 将 i.id() 的值分配到内容标记为 @id 的掩码  */

@title ~= i.title()

@body ~= i.body()

@linkToShow :== linkTo("Show", urla("show", i.id()))  /* 用 linkTo() 替换子元素 */

@linkToEdit :== linkTo("Edit", urla("edit", i.id()))

@linkToRemove :== linkTo("Remove", urla("remove", i.id()), Tf::Post, "confirm('Are you sure?')")

Otama操作符(及其它们的组合)是非常简单的:
~ (波浪号) 掩码元素的内容设置成右侧的值, = 输出HTML转义, 因此~=设置元素的内容设置成右侧的值,然后转义, 如果不希望转义HTML, 可以使用~==. : (冒号) 更换掩码以及掩码的内容为右侧的值, 因此:==没有HTML转义地更换元素.

从控制器(controller)传递数据到视图(view)

如果希望在视图(view)中使用控制器(controller)中的输出(textport)对象, 必须在视图(view)中声明tfetch(macro)方法. 参数部分, 定义变量名和变量的类型. 因为它在输出(textport)前后状态几乎是一样的, 可以像使用正常的变量一样使用它. 在上面的界面逻辑中, 像实际的变量一样使用它.

这里是如何使用的例子:

Controller side :
 int hoge;
 hoge = ...
 texport(hoge);
View side :
 tfetch(int, hoge);

Otama系统, 生成基于C++代码的界面文件和模版文件. 在框架内部, tmake用来处理它. 在代码经过编译后, 生成一个view的共享库, 所以运行起来非常快.

HTML术语

一个HTML元素包括三个部分, 开始标签, 内容, 结束标签.例如, 这个典型的HTML元素, “<p>Hello</p>”, <p> 是开始标签, Hello 是内容, </p> 是结束标签.

模型和ORM

因为Treefrog是基于关系型(relationships)的, 模型(model)将会包含一个ORM对象(虽然你可能希望创建有2个或者更多的ORM对象), 这个关系(relationship)是Has-a. 在这个方面, Treefrog不同于其他框架, 因为默认是使用”ORM = Object Model”.你不可以更改它. 在Treefrog, ORM对象包含在模型(model)对象中.

Treefrog默认会包含一个叫SqlObject的O/R映射器(mapper). 因为C++是静态的语言, 类型声明是需要的. 让我们看看生成的SqlObject文件blogobject.h.

代码中有一半是宏代码, 不过这里的字段是声明成public成员变量的. 它接近实际的结构, 但是仅能使用CRUD或者等效的方法(create, findFirst, update, remove). 这些方法定义在TSqlORMapper类和TSqlObject类.

class T_MODEL_EXPORT BlogObject : public TSqlObject, public QSharedData
{
public:
    int id {0};
    QString title;
    QString body;
    QDateTime created_at;
    QDateTime updated_at;
    int lock_revision {0};

    enum PropertyIndex {
        Id = 0,
        Title,
        Body,
        CreatedAt,
        UpdatedAt,
        LockRevision,
    };

    int primaryKeyIndex() const override { return Id; }
    int autoValueIndex() const override { return Id; }
    QString tableName() const override { return QLatin1String("blog"); }

private:    /*** Don't modify below this line ***/
    Q_OBJECT
    Q_PROPERTY(int id READ getid WRITE setid)
    T_DEFINE_PROPERTY(int, id)
    Q_PROPERTY(QString title READ gettitle WRITE settitle)
    T_DEFINE_PROPERTY(QString, title)
    Q_PROPERTY(QString body READ getbody WRITE setbody)
    T_DEFINE_PROPERTY(QString, body)
    Q_PROPERTY(QDateTime created_at READ getcreated_at WRITE setcreated_at)
    T_DEFINE_PROPERTY(QDateTime, created_at)
    Q_PROPERTY(QDateTime updated_at READ getupdated_at WRITE setupdated_at)
    T_DEFINE_PROPERTY(QDateTime, updated_at)
    Q_PROPERTY(int lock_revision READ getlock_revision WRITE setlock_revision)
    T_DEFINE_PROPERTY(int, lock_revision)
};

Treefrog的O/R映射器(mapper)有查询和更新主键的方法, 不过SqlObject只有一个返回primarykeyIndex()的方法. 因此, 任何有多主键的表应该更改为单主键.还能够通过Tcriteria类的设定条件进行复杂的查询. 详细信息请参加后续章节.

接下来, 让我们看看模型(model).
对象每个属性的setter/getter和生成获取的静态方法已定义好. 父类TAbstractModel定义了save, remove等方法, 这样Blog类就有了CRUD方法(create, get, save, remove).

class T_MODEL_EXPORT Blog : public TAbstractModel
{
public:
    Blog();
    Blog(const Blog &other);
    Blog(const BlogObject &object); // 从 ORM 对象创建模型
    ~Blog();

    int id() const;      // The following lines are the setter/getter 
    QString title() const;
    void setTitle(const QString &title);
    QString body() const;
    void setBody(const QString &body);
    QDateTime createdAt() const;
    QDateTime updatedAt() const;
    int lockRevision() const;
    Blog &operator=(const Blog &other);

    bool create() { return TAbstractModel::create(); }
    bool update() { return TAbstractModel::update(); }
    bool save()   { return TAbstractModel::save(); }
    bool remove() { return TAbstractModel::remove(); }

    static Blog create(const QString &title, const QString &body); // 创建对象
    static Blog create(const QVariantMap &values);                 // 从Hash创建对象
    static Blog get(int id);                   // 通过id获得对象
    static Blog get(int id, int lockRevision); // 通过id 和 lockRevision 获得对象
    static int count();                 // 返回blog的记录数
    static QList<Blog> getAll();        // 获得所有模型对象                              
    static QJsonArray getAllJson();     // 获得JSON方式的所有模型对象

private:
    QSharedDataPointer<BlogObject> d;   // ORM对象的指针

    TModelObject *modelData();
    const TModelObject *modelData() const;
};

Q_DECLARE_METATYPE(Blog)
Q_DECLARE_METATYPE(QList<Blog>)

自动生成代码的步骤并不多, 所有基本的功能已经涵盖.

当然自动生成的代码不是完美的, 真实的应用可能会更加复杂一些. 生成的代码可能不一定合适, 因此可能需要一些修改工作. 无论如何, 这个生成器可以节省一点代码编写的时间和工作.

除了以上描述的代码, 后台还提供了结合cookies篡改检查的CSRF(Cross-site request forgery跨站请求伪造) measures, 乐观锁(optimistic locking), SQL注入的令牌授权(token authentication). 如果有兴趣, 请浏览源代码.

视频Demo - 简单的博客应用的创建

视频Demo - 简单的博客应用的创建