Vapor中使用Future和Promise

几乎任何一个有可能带来请求延迟的操作,Vapor都会采用Future来处理,例如:

  • 返回渲染过的页面,会用Future<View>;
  • 返回HTTP状态码,会用Future<HTTPStatus>;
  • 返回HTTP请求,会用Future<Response>;

在Vapor里,只要返回来自Model中的内容,它就一定是一个Future<T>

修改Episode

Vapor中的做法。为了表示某个在未来会返回的值,我们要使用Future<T>来表示。例如,在未来会得到一个Int就是Future<Int>,在未来会得到一个String,就是Future<String>,以此类推。

因此,对于上一节的/episodes/id这个API,如果Episode对象是通过数据库查询而来,我们最先要修改的,是遵从protocol Parameter时实现的resolveParameter,它返回的,应该是一个Future<Episode?>,而不是Episode。

static func resolveParameter(_ parameter: String,
on container: Container)
throws -> Future<Episode?> {

}

尽管之前Episode的init方法我们处理了字符串id无法转换成整数的情况,但为了更接近实际情况,这里,我们还是返回了Future<Episode?>而不是Future<Episode>,并且这样做也方便我们稍后演示更多Future的用法。

接下来,该怎么实现它呢?由于现在我们知识有限,因此,不会引入真正的异步操作,而只是创建一个Future对象来体会下它的用法:

static func resolveParameter(_ parameter: String,
on container: Container)
throws -> Future<Episode?> {
return Future.map(on: container) {
Episode(id: parameter)
}
}

这里Future.map就是完成映射的方法,它的第一个参数on表示执行任务的线程,我们暂时先不用太多关心它。而后面的closure则是生成Future要封装的值的过程。可以看到,我们还是硬编码了一个Episode对象。不过这次,概念上,我们就得到了一个“在未来的某个时候是Episode?”的值。

接下来,我们修改下Episode的定义:

struct Episode: Content {
var id: Int
var desc: String

init(id: Int, desc: String) {
self.id = id
self.desc = desc
}

init?(id: String) {
if let eid = Int(id) {
self.init(id: eid,
desc: "Description of episode \(eid)")
}
else {
return nil
}
}
}

这次,我们定义了memberwise initializer,并且让之前的init(id: String)变成了failable initializer。这样比之前用一个默认值的实现更接近实际的情况。

修改路由

当我们再调用req.parameters.next(Episode.self)尝试把URL中的参数自动转换成Episode对象的时候,我们就会得到一个Future<Episode?>对象,而不再是之前的Episode对象。但给前端返回一个Optional并不是个好主意,我们应该把这个Future<Episode?>变成Future<Episode>。为此,我们可以使用另外一个版本的Future.map方法。

综合上面这些修改,我们把/episodes的路由改成这样:

router.get("episodes", Episode.parameter) {
req -> Future<Episode> in

let episode = try req.parameters.next(Episode.self)

return episode.map(to: Episode.self) {
episode in
guard let episode = episode else {
throw Abort(.badRequest)
}

return episode
}
}

可以看到,这次,/episodes已经返回了Future<Episode>。在它的实现里,episode的类型是Future<Episode?>。然后,我们使用Future.map:

它的第一个参数to表示要映射的值的类型;
第二个参数表示映射的过程。如果Future包装的Episode对象不为nil,我们就直接返回这个对象。否则就抛出一个Abort异常,这是Vapor提供的一个错误类型。实际上,它可以接受很多参数表示HTTP错误的相关信息,但只有一个参数是必选的。就是我们这里传递的.badRequest。这是一个enum HTTPResponseStatus中的一个case,表示HTTP 400错误。
这样,路由这边的修改就完成了

当我们访问/episodes/5,就会得到对应记录的JSON:

用Future处理单一任务的常用方法

map和flatMap

正因为Future在Vapor中极为普遍,Vapor也提供了很多辅助函数来帮助我们使用Future。这一节,我们就来总结一下这些套路,适应并且掌握它们非常重要。因为在接下来的Vapor各种组件中,我们会频繁使用这些方法。
首先,是最基础的两个方法map和flatMap,它们都返回一个Future对象,唯一的区别就是当它们的closure生成的值自身是一个Future的时候,用flatMap,否则用map。实际上,上一节最后,我们已经使用了map方法:

episode.map(to: Episode.self) {
episode in
guard let episode = episode else {
throw Abort(.badRequest)
}

return episode
}

可以看到,在map的closure参数里,我们返回的是一个Episode对象,它不是一个Future对象。因此,这里直接用map就可以把closure的返回值封装成一个Future<Episode>了。

接下来,为了演示flatMap以及后续的例子,我们直接在routes.swift中添加两个辅助方法:

func getEpisode(from req: Request) -> Future<Episode> {
return req.future(
Episode(id: 1, desc: "Just for demo.")
)
}

func save(_ episode: Episode, for req: Request)
-> Future<Response> {
return episode.encode(status: .created, for: req)
}

其中:

  • getEpisode模拟根据用户请求生成Episode对象,通常这个过程要解码上传数据,因此我们返回了Future<Episode&gt。另外,这里为了避免引入更多的内容,我们还是使用了硬编码的方法;
  • save模拟把生成的Episode对象返回给客户端;这里,我们使用了encode方法把episode的值编码成JSON(这是Episode遵从了protocol Content之后的免费福利),并通过status参数指定了返回的HTTP状态码。同样,在Vapor里,编码过程也会被认为是个可能带来延迟的操作,因此,encode返回的不是表示HTTP返回信息的Response,而是Future<Response?>;

有了这两个方法之后,我们添加一个POST /episode路由,模拟处理用户上传数据:

router.post("episode") {
req in
return getEpisode(from: req)
.flatMap(to: Response.self) {
episode in
return save(episode, for: req)
}
}

在它的实现里可以看到,我们用getEpisode得到了用户上传的内容,并生成了Episode对象。此时,我们得到的,是一个Future<Response>。接下来,为了保存这个这个Episode对象,我们使用了flatMap方法,在它的closure中,我们调用了save。这就是我们在一开始说的,用于类型转换的closure自身也返回一个Future的情况。如果我们使用map就会得到Future<Future<Response>>,这显然不是我们想要的。得到的JSON和HTTP状态码,就是我们在save函数中返回的值。

transform

但有时,我们并不关心一个Future中具体的值。例如刚才那个保存episode的操作,我们只是想告诉客户端:行,我知道了,你放心吧,我保证完成任务。然后就不再发送其它细节信息了。这种情况,我们就可以直接使用transform方法,直接把Future中的数据进行变换:

router.post("episode") {
req in

return getEpisode(from: req)
.flatMap(to: Response.self) {
episode in
return save(episode, for: req)
}
.transform(to: HTTPStatus.noContent)
}

这样,flatMap返回的Future就会被直接变成Future,重新请求下,得到的结果就会变成这样:没有了JSON,HTTP状态码也变成了204。

用Future处理多个任务的常用方法