咱們經過實施新的團隊成員註冊功能,展現了基於SwiftNIO構建的新Swift Talk後端。git
今天咱們將首先看一下Swift中Swift Talk後端的實現!咱們兩年前開始重寫它,這個版本已經在線已經有一段時間了。github
咱們想要展現後端是如何工做的,可是從頭開始構建它會有點無聊。相反,咱們將開始實現一個新功能,而且在此過程當中,咱們將解釋後端的不一樣方面。面試
讓咱們看一下網站賬戶部分的團隊成員頁面。當您想要向團隊添加人員時,您必須輸入他們的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
- 包含主頁,劇集頁面和團隊成員頁面等狀況的枚舉。解釋器而後解釋這個枚舉。服務器
咱們能夠將解釋器視爲控制器,而Node
s能夠與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"
}
複製代碼
當咱們運行服務器並在瀏覽器中從新加載頁面時,團隊令牌應該已從數據庫加載到咱們的用戶數據中。可是咱們沒法知道,由於咱們尚未使用令牌。
爲了顯示註冊連接,咱們必須首先爲它建立一個路由,因此咱們看一下Route
enum及其嵌套的枚舉:
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.uuid
helper,它有兩個函數。第一個函數接收解析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自己就是一種高級語言,咱們本身寫得很差。
在即將到來的劇集中,咱們將完成團隊令牌註冊流程,咱們將不得不查詢數據庫。咱們還將添加一個按鈕,經過生成新令牌使註冊連接無效,咱們將在某個時刻編寫一些測試。