原文作者:Mikhail Svetkin
校审:Richard Lin
大家好。首先,感谢大家对之前博文的评论。
今天,我来谈谈路由,包括它的工作原理以及我们是如何实现它的。
在开始之前,我想澄清一些事情。我们已经在GitHub上看过很多类似的项目,它们使用各种语言开发而不仅仅是C++。
我们发现大多数用C++编写的项目都过于复杂或者过于底层。
有的“Hello world”示例程序都需要20-30行代码。
用C++以外的其他语言编写的项目则非常庞大,有很大的生态系统。因此,我们无法重新实现他们已经完成的所有功能。
这就是为什么我们希望创建既简单、又易于扩展的东西。
注:非常感谢Flask项目,是它给我灵感创建了这套API。
路由是负责对某个特定的请求执行对应的回调函数的进程。
一个路由是某个特定的请求URL对应的特定规则(回调函数)。
让我们来看一个例子:
http://blog.qt.io/dev // blogs for Dev loop
http://blog.qt.io/biz // blogs for Biz Circuit
http://blog.qt.io/blog/2019/ // blogs for 2019 (dev loop + Biz Circuit)
每个地址代表具有特定回调函数的某个请求。
另外,您可能已经注意到其中一个有参数(http://blog.qt.io/blog/2019/).
这个URL请求2019年的整个博客,因此回调能够获取到请求的年份。
这样的路由称为动态路由,另外两个是静态路由。
我认为静态路理解起来非常简单,让我们来看看如何使用QHttpServer来实现它吧。
1
2
3
4
5
6
7
8
9
|
QHttpServer server;
server.route(
"/dev/"
, [] () {
return
"All dev loop blogs"
;
});
server.route(
"/biz/"
, [] () {
return
"All biz loop blogs"
;
});
|
我们只是将每个路径绑定到其回调函数上。我想动态路由会更有意思。
首先,我建议把任务分成子任务。然后,理解我们需要实现什么。
这是最重要的两个要点。我建议把每一项都认真看一遍。
一开始,您可能会问:“需要那么复杂吗?我们能用正则表达式解决问题吗?”
为了更好地理解,我认为我们应该从上面这个例子开始,尝试用正则表达式来解决。
/blog/(\\d+)/
看起来很完美。我们可以捕捉一个参数。如何将参数转换为int类型呢?
最常见的方法是将参数保持为字符串,然后手动转换。
但这种方法有几个缺点:
我们如何改进它?让我们看看使用其他语言的同行们是怎么做的。
现代框架(Django、Flask、Ruby on Rails)都是以同样的方式实现的。
例如,它可以是这样的:
/blog/<int:year>/
看上去很清楚。int用作正则表达式的别名,现在我们也知道了类型。
year用于将捕获的参数绑定到回调参数。
这种方法允许您轻松地添加自定义类型,如下所示:
/blog/<HexInt:year>/
现在,我们对路由知道得够多了,可以开始实现它了。
我建议这样做:
1
2
3
4
5
6
7
|
route(
"/blog/<int:year>"
, [] (
const
QHttpServerRequest &request) {
return
blogs_by_year(request[
"year"
].toInt());
});
route(
"/blog/<int:year>"
, [] (auto year) {
return
blogs_by_year(year.toInt());
});
|
看起来不错,但我们还是可以继续改进的。如果您仔细观察第二种情况,您会发现参数的名称“year”在路径格式中并不是必须的。它被放在那儿是因为在C++中不能将捕获的参数绑定到回调参数中。而且我们也无法对路径格式类型和回调参数进行编译时检查。那么我们该如何改进呢?
我们可以使用静态类型。这样我们就可以将编译器用作为“控制器”,确保我们得到的类型是我们支持的类型。
要解决第二个子任务(类型转换,记得吗?),我们可以使用QVariant,它可以很容易地转换参数。
那我们如何做呢?可以把它们结合起来,就像这样:
1
2
3
4
|
QHttpServer server;
server.route(
"/blog/"
, [] (
int
year) {
return
blogs_by_year(year);
});
|
我们能得到什么?
很酷,不是吗?!
此外,我们还支持多种类型:int, float, double, QString, QByteArray, QUrl,还有好多…
您还可以添加对自定义类型的支持,甚至可以更改正则表达式匹配。如果您想让我另写一篇博文解释这一点,请在评论中告诉我。
您可能会问:“如何捕获多个参数?”例如,我想展示所有在2019年2月发布的博文:
1
2
3
4
|
QHttpServer server;
server.route(
"/blog/<arg>/<arg>"
, [] (
int
year,
int
month) {
return
blogs_by_year_and_month(year, month);
});
|
您可能已经发现了关键字<arg>。它的工作原理与QString::Arg类似,但不支持排序。
这就是为什么我们使用的参数与QSting::arg中的不一样。
除此以外,我们还希望为您提供创建RESTAPI的可能性。为此,我们需要拆分GET/POST/PUT/DELETE请求。
一个小例子:
1
2
3
|
server.route(
"/blog/"
, QHttpServerRequest::Method::Get, [] (
int
year) {
return
blogs_by_year(year);
});
|
或者
1
2
3
4
5
6
|
server.route(
"/blog/"
, [] (
int
year,
const
QHttpServerRequest &req) {
if
(req.method() == QHttpServerRequest::Method::Get)
return
blogs_by_year(year);
return
QHttpServerResponder::StatusCode::NotFound;
});
|
这两种方法都很好,但我更喜欢第一种 —— 它更短一些。
默认情况下,QHttpServer::router只适用于转自URL的Path。如果想要创建特殊的查询规则,您可以继承QHttpServerRouterRule并将其作为模板参数传递给QHttpServer::router.
如果希望自定义HTTP响应的头,则可以使用底层API,QHttpServerResponder.
1
2
3
4
|
QHttpServer server;
server.route(
"/blog/"
, [] (
int
year, QHttpServerResponder &&responder) {
responder.write(blogs_by_year(year),
"text/plain"
);
});
|
备注:QHttpServerResponder和QHttpServerRequest是特殊参数,只能用作回调函数的最后一个参数。
这里有一个简单的Web服务器,您可以很容易地扩展它。如果您对这个项目感兴趣,别忘了下载它。
如果您对如何进一步改进它的建议或想法,您可以考虑在QTBUG-60105上提交您的反馈。
谢谢您的阅读!