Swift Talk后端
我们通过实施新的团队成员注册功能,展示了基于SwiftNIO构建的新Swift Talk后端。
今天我们将首先看一下Swift中Swift Talk后端的实现!我们两年前开始重写它,这个版本已经在线已经有一段时间了。
我们想要展示后端是如何工作的,但是从头开始构建它会有点无聊。相反,我们将开始实现一个新功能,并且在此过程中,我们将解释后端的不同方面。
小编这里推荐一个群:691040931 里面有大量的书籍和面试资料哦有技术的来闲聊 没技术的来学习
添加团队成员
让我们看一下网站帐户部分的团队成员页面。当您想要向团队添加人员时,您必须输入他们的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
- 包含主页,剧集页面和团队成员页面等情况的枚举。解释器然后解释这个枚举。
我们可以将解释器视为控制器,而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<UserData>]) -> 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本身就是一种高级语言,我们自己写得不好。
在即将到来的剧集中,我们将完成团队令牌注册流程,我们将不得不查询数据库。我们还将添加一个按钮,通过生成新令牌使注册链接无效,我们将在某个时刻编写一些测试。