贾伊姆 4.1

贾伊姆 4.1

测试测试过
Lang语言 SwiftSwift
许可证 Apache-2.0
发布最新发布2018年4月
SPM支持SPM

Pablo VillarPablo Villar维护。




Logo


Build Status Codecov Swift Cocoapods Twitter


概览

Jayme是一个Swift库,提供了大量工具来极大地减少您需要编写的代码数量,以执行对RESTful API的CRUD操作。它还封装了网络代码,鼓励您将网络代码和业务逻辑代码与视图控制器分离。

Jayme's Architecture In A Nutshell

功能

面向协议Jayme按照面向协议编程的概念构建,尽可能地鼓励组合而非继承。
泛型和关联类型泛型和关联类型在库的每个地方都存在,以提供灵活性。
错误处理错误使用命名为JaymeError的枚举识别。您可以通过在视图控制器中切换JaymeError来分离具有意义的UI流程。如果需要不同的错误定义,库允许您使用自己的错误类型,借助于关联类型。
未来和结果Jayme使用未来模式来编写异步代码。《未来和结果结构在该库中至关重要。请确保您熟悉它们。
日志Jayme包括一个实用的日志机制,它可以快速启用或禁用。它还允许您设置自定义日志函数,如果您在项目中使用第三方日志库,如CocoaLumberjack,这非常有用。
单元测试Jayme项目100%通过了单元测试。您可以通过参考库中实现单元测试的方式来相对容易地对您仓库中定义的自定义方法进行单元测试。
无依赖项Jayme在运行时不需要任何外部依赖。不过,集成JSON解析库(如SwiftyJSON)相当简单。

架构

这个库是围绕仓储模式工作的。在开始使用它之前,您需要熟悉一些关键概念。这些概念的具体描述可能会根据模式的不同实现方式有所不同,因为它有多种工作方式。

以下是每个相关术语的简要描述,这些描述是基于Jayme如何实现这一模式的

1. 仓储

  • 仓储代表了一个集合,它包含特定类型的实体,并能根据您的应用程序的需求过滤并返回这些实体。
  • 您的业务逻辑代码通常会 reside 在仓储中。
  • Jayme附带四个协议,包含默认实现来处理常见的CRUD操作:CreatableReadableUpdatableDeletable。您的仓储可以任选其一,根据需要实现。

2. 后端

  • 后端是一个中介,它接受具体的请求(例如,对/用户/123的DELETE请求)并通过执行网络操作来满足这些请求,返回结果。
  • 您的网络代码通常会 reside 在后端中。
  • Jayme提供了一个名为URLSessionBackend的默认后端,它通过URLSession来执行对服务器的请求。

3. 实体

  • 实体表示在您的应用程序中有意义的 thing,例如用户。
  • 您的模型对象通常会通过实体来表示。

仓库与实体之间的关系

仓库使用名为 关联类型EntityType 来关联特定类型的实体(实体类型示例:UserPostComment 等)。

根据您的仓库符合的协议,其关联的 EntityType 需要遵守一些(或全部)这些协议。

  • Identifiable:实体可以通过 id 字段被识别。
  • DictionaryInitializable:实体可以用字典进行初始化。
  • DictionaryRepresentable:实体可以用字典表示。

以下表格展示了您的 EntityType 应遵守的协议,这取决于仓库符合的协议。

实体类型
可识别可字典初始化可字典表示
仓库可创建xx
可读取xx
可更新xxx
可删除x

默认行为

CreatableReadableUpdatableDeletable 的运作方式遵循 Inaka 的 REST 规范。服务器需要遵循这些规范,以便这些协议能够正常工作。

以下表格显示了这些协议提供的函数集合

单实体仓库
(例如:"profile")
多实体仓库
(例如:"users")
可创建
.create(entity)请求在服务器上创建实体。如果成功,返回创建的实体。
POST /profile → ⚪
.create(entity)请求在仓库中创建实体。如果成功,返回创建的实体。
POST /users → ⚪

.create([entity1, entity2, ...])请求在服务器上创建实体。如果成功,返回包含创建的实体的数组。
POST /users → [⚪ ,⚪ , ...]
可读取
.read()请求从服务器获取实体。如果成功,返回获取的实体。
GET /profile → ⚪
.read(id: x)请求从服务器获取与给定 id 匹配的实体。如果成功,返回获取的实体。
GET /users/x → ⚪

.readAll()请求从服务器获取与该端点相关的所有实体。如果成功,返回包含获取的实体的数组。
GET /users → [⚪ ,⚪ , ...]

.read(pageNumber: n, pageSize: N)请求以分页方式从服务器获取实体批次,遵循 Grape 标准。如果成功,返回包含对应于页面编号 n 的 N 个实体的数组,还有一个包含从响应中获取的相关分页信息的对象。
GET /users?page=n&per_page=N → ( [⚪ ,⚪ , ...] , pageInfo )
可更新
.update(entity)请求在服务器上更新实体。如果成功,返回更新的实体。
PUT /profile → ⚪
.update(entity, id: x)请求服务器上带有指定id的实体更新。如果成功,则返回更新后的实体。
PUT /users/x → ⚪

.update([entity1, entity2, ...])请求将传入的实体更新到服务器上。如果成功,则返回一个包含更新实体的数组。
PATCH /users → [⚪ ,⚪ , ...]
可删除
.delete()请求从服务器删除该实体。
DELETE /profile
.delete(id: x)请求从服务器删除与给定id匹配的实体。
DELETE /users/x

请注意,在单实体存储库与多实体存储库之间的区别。通常,你将有一个只执行单个(例如:/profile/me、或 /session)实体操作的存储库,另一方面,将有多实体操作的存储库(如 /users/posts、或 /comments)。尽管方法接口类似,但在两种情况下它们略有不同。多亏了Swift的面向协议的特性,这些方法的默认行为定义在前面提到的协议的扩展中。

您可以根据需要自定义这些方法。

  • 如果您需要为某个特定的存储库自定义某些方法,请在该存储库中重新实现这些方法。
  • 如果您需要为所有存储库自定义某些方法,您可以创建一个中间协议,它遵守上述任意一个,您的存储库最终将遵守它,并通过扩展来重写方法的主体。

异步操作

存储库不会直接返回结果。因为响应是异步的,所以结果不能立即得到。Jayme通过封装结果在称为Futures的对象中解决了这个问题。

Futures完成了与完成块类似的事情,但以一种更方便的方式,允许更高效地编写异步代码。请参阅本次演讲以获取更多信息。

所以,而不是

let users: [User] = UsersRepository().readAll()

您实际上需要编写

UsersRepository().readAll().start() { result in
    switch result {
    case .success(let users):
        // got your users here
    case .failure(let error):
        // deal with it
    }
}

请注意,得到的结果不是直接是users,而是一个包含users或错误(取决于操作如何进行)的Result对象。这种结构的好处在于强制您考虑不愉快的路径,即发生失败时应该怎么做,这是常常被忘记的事。

示例

在这个示例中,您将学习如何设置和使用具有基本CRUD功能的多实体存储库。

1. 配置您的后端

首先,您需要设置应用程序将使用的前端。这里指定您的应用程序如何连接到服务器。Jayme提供了一个可用的URLSessionBackend类。如果您需要使用与URLSession不同的服务器连接,您可以编写自己的Backend

默认情况下,当您初始化一个URLSessionBackend实例时,它会带有一个默认配置对象,该对象使用基本的HTTP头部进行JSON通信,并将localhost:8080作为默认的基本URL路径。您通常会将其更改。以下是更改方法:

extension URLSessionBackend {
    class func myAppBackend() -> URLSessionBackend {
        let basePath = "http://your_base_url_path"
        let headers = [HTTPHeader(field: "Accept", value: "application/json"),
                       HTTPHeader(field: "Content-Type", value: "application/json")]
                       // and any header you need to use
        let configuration = URLSessionBackendConfiguration(basePath: basePath, httpHeaders: headers)
        return URLSessionBackend(configuration: configuration)
    }
} 

然后,您编写的任何存储库都应该使用

let backend = URLSessionBackend.myAppBackend()

2. 创建您的第一个实体类型

让我们创建一个User结构,用于存储基本用户数据

struct User {
    let name: String
    let email: String
}

3. 创建您的第一个存储库

现在,让我们创建一个用户存储库,该存储库能够从后端读取用户信息。

class UsersRepository: Readable {
    typealias EntityType = User                    // 1
    let backend = URLSessionBackend.myAppBackend() // 2 
    let name = "users"                             // 3
}

这是符合Readable的三项要求

  1. 您需要通过EntityType类型别名指定存储库所处理的实体类型。
  2. 您需要实例化一个URLSessionBackend。通常情况下,您将使用您之前设置的。
  3. 您需要为存储库提供一个name。这个字符串是CreatableReadableUpdatableDeletable协议在内部用于构建请求路径的字符串。例如:如果您提供"users"的名称,那么.delete(id: "123")将导致"DELETE /users/123"

这段代码目前无法编译。为什么?因为您的EntityType必须符合IdentifiableDictionaryInitializable协议,就像Readable协议一样。

因此,您必须修改您的User结构体

struct User: Identifiable {
    let id: String
    let name: String
    let email: String
}

extension User: DictionaryInitializable {
    init(dictionary: [AnyHashable: Any]) throws {
        // Parse the entity here
        guard 
            let id = dictionary["id"] as? String,
            let name = dictionary["name"] as? String,
            let email = dictionary["email"] as? String
            else { throw JaymeError.parsingError }
        self.id = id
        self.name = name
        self.email = email
    }
}

4. 执行 READ 操作

现在您可以前往视图控制器读取用户了!

UsersRepository().readAll().start() { result in
    switch result {
    case .success(let users):
        // You've got all your users fetched in this array!
    case .failure(let error):
        // You've got a discrete JaymeError indicating what happened
    }
}
UsersRepository().read(id: "1").start() { result in
    switch result {
    case .success(let user):
        // You've got the user with id = "1"
    case .failure(let error):
        // You've got a discrete JaymeError indicating what happened
    }
}

请注意,所有的魔法都在幕后发生:构建请求、处理响应、解析JSON对象等。您可能想要检查以下类和协议,以了解这些魔法的实现方式:URLSessionBackendDataParserEntityParserCreatableReadableUpdatableDeletable。可能需要编写自己的实现来满足需求,前提是它们与Jayme使用的标准不同。

5. 我想能够创建用户

将创建用户的功能添加到您仓库中就像使其符合Creatable一样简单。

class UsersRepository: Readable, Creatable {
    typealias EntityType = User
    let backend = URLSessionBackend.myAppBackend()
    let name = "users"       
}

请注意,Creatable协议要求您的EntityType也必须符合DictionaryRepresentable。让我们来做这件事。

extension User: DictionaryRepresentable {
    var dictionaryValue: [AnyHashable: Any] {
        return ["id": self.id, 
                "name": self.name,
                "email": self.email]
    }
}

现在您可以从您的视图中创建用户了。

let user = User(id: "1", name: "Laura", email: "[email protected]")
UsersRepository().create(user).start() { result in
    switch result {
    case .success(let user):
        // User created!
    case .failure(let error):
        // JaymeError indicating what happened
    }
}

6. 很好,但我不应该用ID创建用户

好吧,假设您项目的业务规则表明ID是在服务器端创建的,而不是像我们刚才那样在客户端创建。这就是您需要开始
自定义默认行为的地方。

完成此任务的一个简单方法是向UsersRepository添加一个新方法,该方法请求除了ID之外的所有用户属性,并将这些属性发送到服务器。

您可以从Creatable协议扩展中借用默认的create函数,并根据需要修改它。

extension UsersRepository {
    func create(name: String, email: String) -> Future<User, JaymeError> {
        let path = self.name
        let parameters = ["name": name, "email": email] // see? no id!
        return self.backend.future(path: path, method: .POST, parameters: parameters)
            .andThen { DataParser().dictionary(from: $0.0) }
            .andThen { EntityParser().entity(from: $0) }
    }
}

请注意,由于您不再使用任何Creatable功能,因此您的UsersRepository不再需要符合Creatable。因此,User也不需要符合DictionaryRepresentable

现在,从您的控制台

let future = UsersRepository().create(name: "Laura", email: "[email protected]")
future.start() { result in
    switch result {
    case .success(let user):
        // User created! 
        // Here, you have a full `user` with the `id` that comes from the server
    case .failure(let error):
        // JaymeError indicating what happened
    }
}

7. 添加更新和删除功能

添加基本更新删除功能很简单:使您的仓库符合UpdatableDeletable

extension UsersRepository: Updatable, Deletable {
}

Updatable.swiftDeletable.swift扩展中提供了方法。您的EntityType已符合IdentifiableDictionaryInitializableDictionaryRepresentable,因此没有其他注意事项。

现在,您可以从您的视图中更新和删除用户了。

UsersRepository().update(updatedUser, id: "1").start() { result in
    switch result {
    case .success(let user):
        // user updated!
    case .failure(let error):
        // JaymeError indicating what happened
    }
}
UsersRepository().delete(id: "1").start() { result in
    switch result {
    case .success:
        // user deleted!
    case .failure(let error):
        // JaymeError indicating what happened
    }
}

8. 为自定义端点添加功能

假设您的服务器能够在调用 /users/active 端点并使用 GET 方法时返回活跃用户

这是在您的存储库中为自定义端点添加功能的完美示例。在开发时通常会发现很多这样的示例。

这就是如何去做

extension UsersRepository {
    func readActive() -> Future<[User], JaymeError> {
        let path = "\(self.name)/active"
        return self.backend.future(path: path, method: .GET, parameters: nil)
            .andThen { DataParser().dictionary(from: $0.0) }
            .andThen { EntityParser().entity(from: $0) }
    }
}

您会经常为自己的一些存储库编写这类自定义函数。因此,建议您了解如何使用 andThen 函数将这些示例中 Future 对象串联起来。鼓励您查看 附录 A:编写您自己的自定义函数

9. 已设置完毕!

现在您已经了解了如何使用 Jayme 编写代码的基本知识。您会遇到更复杂的情况,这将需要您思考如何应对。凭借创建实体类型、存储库以及在使用视图控制器中使用它们的基本知识,您应该能够顺利地进行。

我们鼓励您阅读以下额外文档

示例项目

您可以查看这个示例项目,它集成了 Jayme 和 GitHub API

此外,REPO 内还有一个 示例 文件夹,其中包含了一些集成了视图控制器的存储库的更基本的实现。此示例项目需要一个本地服务器才能工作,您可以快速通过以下方式设置:

$ cd Jayme/Example/Server
$ python -m SimpleHTTPServer 8080

设置

  • Jayme 可通过 CocoaPods 获取。
    • 安装它,请将 pod 'Jayme' 添加到您的 Podfile 文件,并运行 pod install 命令。
    • 然后,在需要使用此库的任何源文件中添加 import Jayme 语句。

迁移指南

支持的 Swift 版本

联系我们

有关此库使用的问题或一般评论,请使用我们的公开 HipChat 房间

如果您发现任何 bug,在使用此库时遇到 问题,或有可以使其变得更好的 建议,请在这 repo 中 提交一个问题(或发送 pull request)。

您还可以在 inaka.github.io 查看我们所有的开源项目。