Swift Talk后端

咱们经过实施新的团队成员注册功能,展现了基于SwiftNIO构建的新Swift Talk后端。git

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

咱们想要展现后端是如何工做的,可是从头开始构建它会有点无聊。相反,咱们将开始实现一个新功能,而且在此过程当中,咱们将解释后端的不一样方面。面试

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

添加团队成员

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

这并不理想,由于团队经理可能不知道用户名,这意味着他们必须在被邀请者以前询问被邀请者。咱们想要改变这种状况:咱们但愿显示一个注册连接,该连接能够与可能加入您团队的人员共享,这将容许被邀请者使用他们本身的GitHub账户进行注册。swift

咱们的第一个任务是用注册连接替换团队成员页面上的邀请表单。当咱们深刻研究代码时,咱们发现 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函数的团队成员表单 ,以及建立表单的帮助程序。执行此操做后,咱们在代码库的另外一部分中收到有关调用站点的编译器错误。bash

当服务器收到来自浏览器的请求时,咱们将该请求转换为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自己就是一种高级语言,咱们本身写得很差。

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


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

691040931

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

相关文章
相关标签/搜索