Swift Talk后端

作者: iOS_小久 | 来源:发表于2019-07-04 19:50 被阅读61次

    我们通过实施新的团队成员注册功能,展示了基于SwiftNIO构建的新Swift Talk后端。

    今天我们将首先看一下Swift中Swift Talk后端的实现!我们两年前开始重写它,这个版本已经在线已经有一段时间了。

    我们想要展示后端是如何工作的,但是从头开始构建它会有点无聊。相反,我们将开始实现一个新功能,并且在此过程中,我们将解释后端的不同方面。

    小编这里推荐一个群:691040931 里面有大量的书籍和面试资料哦有技术的来闲聊 没技术的来学习

    添加团队成员

    让我们看一下网站帐户部分的团队成员页面。当您想要向团队添加人员时,您必须输入他们的GitHub用户名:

    这并不理想,因为团队经理可能不知道用户名,这意味着他们必须在被邀请者之前询问被邀请者。我们想要改变这种情况:我们希望显示一个注册链接,该链接可以与可能加入您团队的人员共享,这将允许被邀请者使用他们自己的GitHub帐户进行注册。

    我们的第一个任务是用注册链接替换团队成员页面上的邀请表单。当我们深入研究代码时,我们发现 teamMembersView函数返回要呈现的视图Node- 表示HTML节点的递归枚举,可以是任何内容,如HTML元素,文本或注释:

    func teamMembersView(addForm: Node, teamMembers: [Row<UserData>]) -> Node {
        // ... }
    

    在这个函数中,我们找到了包含在结果中的内容定义。我们删除表单元素并将其替换为段落节点Node.p,并将字符串作为其单个子节点。我们还为注册链接添加了另一个带占位符的段落节点,我们将这两个段落嵌套在一个div样式中:

    func teamMembersView(addForm: Node, teamMembers: [Row<UserData>]) -> Node {
        // ... 
        let content: [Node] = [
            Node.div(classes: "stack++", [
                Node.div([
                    heading("Add Team Member"),
                    Node.div(classes: "stack", [
                        Node.p(["To add team members, send them the following signup link:"]),
                        Node.p(["TODO link"])
                    ])
                ]),
                Node.div([
                    heading("Current Team Members"),
                    currentTeamMembers
                ])
            ])
        ]
    
        // ... }
    

    当我们重建项目时,我们会看到更改的页面:

    我们可以删除用于传递给teamMembersView函数的团队成员表单 ,以及创建表单的帮助程序。执行此操作后,我们在代码库的另一部分中收到有关调用站点的编译器错误。

    当服务器收到来自浏览器的请求时,我们将该请求转换为Route- 包含主页,剧集页面和团队成员页面等情况的枚举。解释器然后解释这个枚举。

    我们可以将解释器视为控制器,而Nodes可以与iOS应用程序的视图相媲美。通过这种分离,我们可以使用测试解释器替换服务器解释器,后者将跳过所有服务器基础结构。

    在解释代码中,我们有一个辅助函数来创建旧的团队成员表单,但我们不再需要这个:

    extension Route.Account {
        // ...
        private func interpret2<I: Interp>(session sess: Session) throws -> I {
            func teamMembersResponse(_ data: TeamMemberFormData? = nil, errors: [ValidationError] = []) throws -> I {
                let renderedForm = addTeamMemberForm().render(data ?? TeamMemberFormData(githubUsername: ""), errors)
                return I.query(sess.user.teamMembers) { members in
                    I.write(teamMembersView(addForm: renderedForm, teamMembers: members))
                }
            }
            // ...
        }
    

    我们删除了辅助函数,除了它的return语句,我们将内联移动到我们称为帮助器的位置:

    extension Route.Account {
        // ...
        private func interpret2<I: Interp>(session sess: Session) throws -> I {
            switch self {
            // ...
            case .teamMembers:
                let url = Route.teamMemberSignup(token: sess.user.data.teamToken).url
                return I.query(sess.user.teamMembers) { members in
                    I.write(teamMembersView(signupURL: url, teamMembers: members))
                }
            // ...
        }
    }
    

    我们还在删除团队成员的路线中使用了辅助功能。我们不是调用帮助程序来创建响应,而是重定向回团队成员路由:

    extension Route.Account {
        // ...
        private func interpret2<I: Interp>(session sess: Session) throws -> I {
            switch self {
            // ...
            case .deleteTeamMember(let id):
                return I.verifiedPost { _ in
                    I.query(sess.user.deleteTeamMember(id)) {
                        let task = Task.syncTeamMembersWithRecurly(userId: sess.user.id).schedule(at: globals.currentDate().addingTimeInterval(5*60))
                        return I.query(task) {
                            return I.redirect(to: .account(.teamMembers))
                        }
                    }
                }
            }
        }
    }
    

    我们从中返回的对象I是响应类型,其辅助方法之一是redirect。我们使用相同的枚举重定向到另一个路由,该枚举被解释为来自浏览器的请求。通过仅使用枚举表示内部链接,不可能创建不正确的内部链接; 编译器根本不会让我们。

    生成注册令牌

    下一步是为注册链接生成令牌并将此令牌保存到数据库。

    我们已经选择将PostgreSQL用于我们的数据库,并且我们手动编写SQL查询(除了我们用来执行一些简单查询的一些帮助程序)。我们更喜欢在添加大型抽象层时编写一些查询,这些抽象层可能隐藏了SQL的许多有用功能。

    一系列查询构成了我们的数据库迁移,我们添加了一个迁移,它将团队令牌的列添加到users表中:

    fileprivate let migrations: [String] = [
        // ...
        """
        ALTER TABLE users ADD COLUMN IF NOT EXISTS team_token uuid DEFAULT public.uuid_generate_v4();
        """
    ]
    

    由于我们稍后会从数据库中查找令牌,我们还会添加一个令牌索引:

    fileprivate let migrations: [String] = [
        // ...
        """
        CREATE INDEX IF NOT EXISTS team_token_index ON users (team_token);
        """
    ]
    

    每次服务器启动时,都会运行所有迁移。这需要我们注意并以可以安全执行多次的方式编写查询 - 请注意IF NOT EXISTS上面两个示例中的条件。

    我们运行服务器,没有收到任何错误,我们得出结论,迁移已成功执行。因此,我们现在还可以将团队令牌添加到我们的用户模型中。

    更新模型

    我们使用Codable自动生成结构的查询,并将查询结果解析回此结构。每个表都由一个结构表示,我们还有一些特定查询的结构。

    所有这些后,我们现在只需要teamToken在用户结构中添加一个以访问存储在数据库中的令牌:

    struct UserData: Codable, Insertable {
        var email: String
        var githubUID: Int?
        // ...
        var teamToken: UUID
    
        init(email: String, githubUID: Int? = nil, /*...*/, teamToken: UUID = UUID()) {
            self.email = email
            self.githubUID = githubUID
            // ...
            self.teamToken = teamToken
        }
    
        static let tableName = "users"
    }
    

    当我们运行服务器并在浏览器中重新加载页面时,团队令牌应该已从数据库加载到我们的用户数据中。但是我们无法知道,因为我们还没有使用令牌。

    为了显示注册链接,我们必须首先为它创建一个路由,所以我们看一下Routeenum及其嵌套的枚举:

    indirect enum Route: Equatable {
        case home
        case episodes
        case sitemap
        case subscribe
        case collections
        case login(continue: Route?)
        case account(Account)
        // ... 
        enum Account: Equatable {
            case register(couponCode: String?)
            case profile
            case teamMembers
            // ...
        }
    
        // ... }
    

    我们创建的新路线与.subscribe 路线类似,在注册过程中增加了团队令牌。我们添加一个名为的新案例,.teamMemberSignup其中包含一个令牌作为其关联值:

    indirect enum Route: Equatable {
        // ...
        case subscribe,
        case teamMemberSignup(token: UUID),
        // ... }
    

    我们只需将a的参数存储Route在正确的类型中,就像UUID这里一样,只要我们能够将类型转换为请求即可。当我们处于其中一个解释函数时,我们已经拥有了处理请求所需的所有参数。

    我们编写了一个(稍微复杂的)库以支持Route 枚举,我们不会详细介绍,但添加一个新的Route本质上归结为指定如何将请求Route转换为该请求以及如何将Route返回转换为URL

    我们通过为路由器提供这两个转换来实现。我们首先使用常量帮助器,c告诉路由器该路由的URL以字符串开头"join_team"。然后,对于token参数,我们使用/运算符,然后是Router.uuidhelper,它有两个函数。第一个函数接收解析UUID并且必须返回Route,第二个函数接收a 并且必须 Route返回UUID 值,如果它实际上是我们期望的路径:

    private let otherRoutes: [Router<Route>] = [
        // ...
        .c("join_team") / Router.uuid.transform({ .teamMemberSignup(token: $0) }, { route in
            guard case let .teamMemberSignup(token) = route else { return nil }
            return token
        })
    ]
    

    因为库完成了解析请求(包括参数)和生成URL的大部分工作,所以主要焦点已转移到UUID参数和参数之间的转换Route

    添加新内容后Route,我们必须在解释器中处理它。编译器提醒我们这个事实,因为interpret函数中的switch语句不再详尽无遗。我们添加案例,现在,只需在响应中写一个字符串:

    extension Route {
        func interpret<I: Interp>() throws -> I {
            switch self {
            // ...
            case let .teamMemberSignup(token: token):
                return I.write("team signup \(token)")
            // ...
            }
        }
    }
    

    在我们到达路线之前,我们必须在团队成员页面上显示注册URL,因此我们向teamMembersView 帮助者添加一个URL参数:

    func teamMembersView(signupURL: URL, teamMembers: [Row<UserData>]) -> Node {
    // ... }

    我们删除占位符并插入URL。之前,我们使用字符串文字作为段落的子节点,这是允许的,因为节点类型实现了StringLiteralConvertible。但是现在我们想通过将它包装在一个.text节点中来使用字符串属性。我们还指定了一个CSS类来为链接提供等宽字体:

    func teamMembersView(signupURL: URL, teamMembers: [Row<UserData>]) -> Node {
        // ... 
        let content: [Node] = [
            Node.div(classes: "stack++", [
                Node.div([
                    heading("Add Team Member"),
                    Node.div(classes: "stack", [
                        Node.p(["To add team members, send them the following signup link:"]),
                        Node.p(classes: "type-mono", [.text(signupURL.absoluteString)])
                    ])
                ]),
                // ...
            ])
        ]
    
        // ... }
    

    当我们尝试运行服务器时,视图助手抱怨我们还没有传入注册URL这一事实,所以我们从刚刚添加的路由中获取URL:

    extension Route.Account {
        // ...
        private func interpret2<I: Interp>(session sess: Session) throws -> I {
            switch self {
            // ...
            case .teamMembers:
                let url = Route.teamMemberSignup(token: sess.user.data.teamToken).url
                return I.query(sess.user.teamMembers) { members in
                    I.write(teamMembersView(signupURL: url, teamMembers: members))
                }
            // ...
            }
        }
    }
    

    当我们再次运行服务器并刷新时,我们会看到团队成员页面上的注册链接:

    我们复制URL并在浏览器中打开它以查看我们之前写的响应:

    我们可以尝试弄乱URL并从令牌中删除一个字符; 这会导致“找不到页面”错误。这是因为路由器尝试解析字符串"join_team"和UUID,如果不能,则没有与URL匹配的路由。

    首先检查路由是否只适用于有效的UUID。但是,我们尚未检查所请求的UUID实际上是否是数据库中的有效令牌。

    讨论

    到目前为止,我们已经看到了后端基础架构的一些不同部分:我们修改了一个视图,我们添加了一个数据库迁移并更新了我们的数据库模型,我们添加了一个新的路由和一个最小的响应。

    一切都直接建立在 SwiftNIO之上。不使用中间的任何其他框架使得一些部分,如驱动数据库,相当简单。但这也有助于我们保持高效:我们可以准确地编写我们需要的查询。SQL本身就是一种高级语言,我们自己写得不好。

    在即将到来的剧集中,我们将完成团队令牌注册流程,我们将不得不查询数据库。我们还将添加一个按钮,通过生成新令牌使注册链接无效,我们将在某个时刻编写一些测试。


    扫码进交流群 有技术的来闲聊 没技术的来学习

    691040931

    原文转载地址:https://talk.objc.io/episodes/S01E138-the-swift-talk-backend-part-1

    相关文章

      网友评论

        本文标题:Swift Talk后端

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