Swift Talk后端

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

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

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

点击此处进交流群 有技术的来闲聊 没技术的来学习

添加团队成员

让我们看一下网站帐户部分的团队成员页面。当您想要向团队添加人员时,您必须输入他们的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]) -> 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本身就是一种高级语言,我们自己写得不好。

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


小编这里有大量的书籍和面试资料哦(点击下载

原文转载地址:talk.objc.io/episodes/S0…

转载于:https://juejin.im/post/5d1ed9d7f265da1bce3df056

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值