译 Swift Talk -- NetWorking

作者: 没阳光的午后 | 来源:发表于2016-06-22 13:43 被阅读401次

    原文来自于 objc.io

    Transcript

    0:01 我们来讨论下 Swift talk app 的网络层。我们认为这是个有趣的例子因为设计与之前的 Objective-C 项目不同。通常,我们将创建一个有初始化方法的Webservice类来呼叫一个特定的 endpoints 。这些方法返回从 endpoints 通过一个回调函数获得的数据。举个例子,我们可以有个网络请求的loadEpisodes方法,分析结果,初始化一些 Episode对象,并返回一个包含Episode的数组。我们同样可以有一个loadMedia方法,通过同样的步骤来夹在一个特定 episode 的 media:

    final class Webservice {
        func loadEpisodes(completion: ([Episode]?) -> ()) {
            // TODO
        }
    
        func loadMedia(episode: Episode, completion: (Media?) -> ()) {
            // TODO
        }
    }
    

    final可以用来修饰 class,func 或者 var ,修饰过后的内容不允许被重写或者继承。

    0:50 在 Objective-C 中,这个方式的优点是回调结果有个正确的类型。举个例子,我们将获得一个 episodes 的数组而不仅仅是个id类型,因为这是一个从网络加载任何数据的方法。这个方式的优点是每个方法在幕后执行一个复杂任务:网络请求,分析数据,初始化一些 model 对象,最后通过回调返回他们。这里有很多地方会出错,正因为如何,调试是很难的。因为这些方法还是异步的,所以让他们更难调试。此外,我们需要一个网络栈设置或者模拟,这也使调试更复杂。在 Swift 中,有其他的方式来让这事简单化。

    The Resource Struct

    1:51 我们创建一个Resource结构体,这是一个泛型类型。这个结构体有2个属性:endpoint 的 URL和parse函数。parse函数试图将一些数据转化为结果:

    struct Resource<A> {
        let url: NSURL
        let parse: NSData -> A?
    }
    

    2:12 parse函数的返回类型是可选的因为分析可能失败。代替可选值,我们也可以使用Result类型或者使他抛出详细的错误信息。此外,如果我们只想处理 JSON,parse函数可以使用AnyObject来代替NSData。然而,使用AnyObject会阻止我们使用我们的Resource除了 JSON - 例如图片。

    2:59 现在创建episodesResource。这只是一个返回NSData的简单 resource:

    let episodesResource = Resource<NSData>(url: url, parse: { data in
        return data
    })
    

    3:33 最后,这个 resource 应该有一个[Episode]的 result 类型。我们将重构parse函数通过几个步骤将NSData的 result 改成[Episode]的 result 类型。

    The Webservice Class

    3:58 从网上加载资源,我们创建一个Webservice类,他只有一个方法:load。这个方法是通用的,并将 resource 作为第一个参数。这二个参数是个闭包,使用 A?是因为请求有可能失败或者某些东西会出错。在load方法里,我们使用NSURLSession.sharedSession()来做请求。我们创建一个 data task 用从 resource 中获得的 URL。resource 捆绑了我们需要的所有做请求的信息。目前,只包含了 URL,但在将来会有更多的属性。在 data task 的回调里,我们使用 data 作为第一个参数。我们忽略其他2个参数。最后,开始 data task,我们调用resume

    final class Webservice {
        func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
            NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
                if let data = data {
                    completion(resource.parse(data))
                } else {
                    completion(nil)
                }
            }.resume()
        }
    }
    

    5:38 调用闭包,我们不得不通过parse函数来将 data 转为资源的结果类型。因为 data 是可选的,我们使用可选绑定。如果 data 是nil,我们调用闭包使用nil。如果 data 不是nil,我们调用闭包使用parse函数。

    6:22 因为我们运行在 playground,我们必须让他一直执行下去,否则,主线程完成就会停止:

    import XCPlayground
    XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
    

    7:00 我们创建一个Webservice实例然后调用load方法和episodesResource一起。在闭包里,我们输出 result:

    Webservice().load(episodesResource) { result in
        print(result)
    }
    

    7: 18 在控制台中,我们可以看到一些原始的二进制数据。在我们继续之前,我们将重构load方法--我们不喜欢调用2次completion。我们尝试使用guard let。然而,我们还是调用了2次completion,还添加了返回语句:

    final class Webservice {
        func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
            NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
                guard let data = data else {
                    completion(nil)
                    return
                }
                completion(resource.parse(data))
            }.resume()
        }
    }
    

    8:07 使用flatMap是其他的办法。首先,我们尝试map。然而,map给我们了一个A??代替A?。使用flatMap将移除2个可选:

    final class Webservice {
        func load<A>(resource: Resource<A>, completion: (A?) -> ()) {
            NSURLSession.sharedSession().dataTaskWithURL(resource.url) { data, _, _ in
                let result = data.flatMap(resource.parse)
                completion(result)
            }.resume()
        }
    }
    

    flatMap可以去掉空值

    Parsing JSON

    8:58 下一步我们改变episodesResource为了将NSData解析为 JSON 对象。我们使用内置的 JSON 解析。因为 JSON 解析会 throwing operation,我们使用try?来调用 parsing 方法:

    let episodesResource = Resource<AnyObject>(url: url, parse: { data in
        let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
        return json
    })
    

    9:40 在侧边栏,我们可以看到二进制数据被解析。这是个字典数组,所以我们可以让结果类型更加明确。JSON 字典包含一个 String的 key 和AnyObject的 values:

    typealias JSONDictionary = [String: AnyObject]
    
    let episodesResource = Resource<[JSONDictionary]>(url: url, parse: { data in
        let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
        return json as? [JSONDictionary]
    })
    

    10:23 下一步是返回一个Episode数组,所以我们需要将 JSON 字典转化到Episode里。在初始化之前,我们添加一些属性到Episode里:idtitle,都是String。在真实的项目里,这里有更多的属性:

    struct Episode {
        let id: String
        let title: String
        // ...
    }
    

    11:13 我们现在在 extension 里写个可失败构造器。在这个 extension 里,我们保留了默认的成员逐一初始化。在这个构造器里,我们首先需要检查字典是否包含我们需要的数据。我们使用guard来做这件事,然后我们检查字典里的 id是否是Srting类型,取出title做相同的操作。如果 guard 失败,我们马上返回nil。如果成功,我们给 idtitle赋值:

    extension Episode {
        init?(dictionary: JSONDictionary) {
            guard let id = dictionary["id"] as? String,
                title = dictionary["title"] as? String else { return nil }
            self.id = id
            self.title = title
        }
    }
    

    12:48 我们现在重构episodesResource来返回一个Episode数组。首先,我们检查我们是否有个 JSON 字典。否则,我们马上返回nil。字典转化为 episodes,我们可以使用map并使用可失败Episode.init作为我们的转换函数。然而,构造器返回可选值,所以使用map结果是[Episode?]。但是我们不想在这里返回nil,应该是[Episode]。我们使用flatMap来修复这个问题。

    12:48 code

    14:18 在我们的项目里,flatMap的不同版本。flatMap会默认忽略不能解析的字典,我们想一旦字典无效就完全失败:

    extension SequenceType {
        public func failingFlatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]? {
            var result: [T] = []
            for element in self {
                guard let transformed = try transform(element) else { return nil }
                result.append(transformed)
            }
            return result
        }
    }
    

    14:52 我们可以重构我们的parse函数来移除2个return。首先,我们尝试使用guard,但是这个不能移除2个return。然而,guard可以让我们摆脱嵌套:

    let episodesResource = Resource<[Episode]>(url: url, parse: { data in
        let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
        guard let dictionaries = json as? [JSONDictionary] else { return nil }
        return dictionaries.flatMap(Episode.init)
    })
    

    15:28 我们尝试在dictionaries里使用 optional chaining来去除2次return

    let episodesResource = Resource<[Episode]>(url: url, parse: { data in
        let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
        let dictionaries = json as? [JSONDictionary]
        return dictionaries?.flatMap(Episode.init)
    })
    

    15:44 这开始变得难以理解。我们有一个可选的dictionaries然后我们使用 optional chaining 来调用flatMap,将可失败构造器作为参数。在这里,我们也许会用guard的版本,那个更加清晰。

    JSON Resources

    16:07 一旦我们创建更多的 resources,必须复制 JSON 解析到每个 resources。移除这个复制,我们可以创建一个不同的 resources。然而,我们可以扩展现存的 resources 通过其他的构造器。这个构造器页使用 URL,但是 parse 函数类型是AnyObject -> A?。我们在包裹了这个 parse 函数在其他的NSData -> A?函数类型里并在这个闭包里从episodesResource里移除了 JSON 解析。因为解析 JSON 是可选的,我们可以使用flatMap来调用parseJSON:

    extension Resource {
        init(url: NSURL, parseJSON: AnyObject -> A?) {
            self.url = url
            self.parse = { data in
                let json = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
                return json.flatMap(parseJSON)
            }
        }
    }
    

    18:00 现在我们可以使用新的构造器来改变我们的episodesResource

    let episodesResource = Resource<[Episode]>(url: url, parseJSON: { json in
        guard let dictionaries = json as? [JSONDictionary] else { return nil }
        return dictionaries.flatMap(Episode.init)
    })
    

    Naming the Resources

    18:17 另外一件我们不喜欢的事情是episodesResource在公共的命名空间。我们也不喜欢他的命名。我们可以将episodesResource移到Episode的扩展里作为一个类属性。我们将他重命名为allEpisodesResource。然而,我们还是不怎么喜欢这个名字。看看这个类型,很清楚的表明它属于Episode。从类型里也可以明白是一个 resource,所以我们为什么不仅仅命名为call?:

    18:17 code
    Webservice().load(Episode.all) { result in
        print(result)
    }
    

    19:40 其实这是个危险的命名,也许你会和集合混淆。虽然我们不认为这是个问题,因为你试图使用集合会立即失败。

    20:09 在Episode扩展中,我们也可以添加其他依赖于 episode 的属性的resources——例如,一个mediaresource,从指定的 episode 中获得 media。在media resource 中,我们可以使用字符串插入来组成 URL:

    extension Episode {
        var media: Resource<Media> {
            let url = NSURL(string: "http://localhost:8000/episodes/\(id).json")!
            // TODO Return the resource ...
        }
    }
    

    21:18 如果我们在Episode结构体中需要更多的参数是无效的,我们可以改变 resource 属性作为一个方法然后直接传递参数。

    21:27 我们喜欢这个网络请求的方式因为几乎所有的代码都是同步的。这很简单,很容易调试,而且我们也不需要设置网络栈或者调试一些东西。唯一异步的代码是Webservice.load方法。这个架构是个不错的例子对于 Swift 来说;Swift 的泛型和结构体让这样设计变得很简单。同样的事情在 OC 里是做不了的。

    22:21 让我们添加POST支持在下一节。

    相关文章

      网友评论

        本文标题:译 Swift Talk -- NetWorking

        本文链接:https://www.haomeiwen.com/subject/vpfndttx.html