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…

相關文章
相關標籤/搜索