咱们经过实施新的团队成员注册功能,展现了基于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自己就是一种高级语言,咱们本身写得很差。
在即将到来的剧集中,咱们将完成团队令牌注册流程,咱们将不得不查询数据库。咱们还将添加一个按钮,经过生成新令牌使注册连接无效,咱们将在某个时刻编写一些测试。