Vapor 大型教程
数据校验
表单提交的数据不一定靠谱。本文教你用 Vapor 3 的 Validatable 协议校验注册表单:既支持字段级规则,也能自定义「两次密码一致」这样的跨字段校验,并把错误提示回显到页面。
一、先做注册页面
前面我们已经有了一个能创建缩略语、类别的 Web 应用。现在为它加上用户注册界面。先写注册表单模板 register.leaf:
#set("content") {
<h1>#(title)</h1>
<form method="post">
<div class="form-group">
<label for="name">Name</label>
<input type="text" name="name" class="form-control"
id="name"/>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" name="username" class="form-control"
id="username"/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" name="password" class="form-control"
id="password"/>
</div>
<div class="form-group">
<label for="confirmPassword">Confirm Password</label>
<input type="password" name="confirmPassword"
class="form-control" id="confirmPassword"/>
</div>
<button type="submit" class="btn btn-primary">
Register
</button>
</form>
}
#embed("base")
注册表单对应的上下文与数据模型如下。注意 RegisterData 遵循 Content,这样可以直接用 req.content.decode 解码请求体:
struct RegisterContext: Encodable {
let title = "Register"
}
struct RegisterData: Content {
let name: String
let username: String
let password: String
let confirmPassword: String
}
在 boot(router:) 中注册两个路由:一个显示注册页(GET),一个接收提交(POST):
func boot(router: Router) throws {
...
authSessionRoutes.get("register", use: registerHandler)
authSessionRoutes.post(RegisterData.self, at: "register", use: registerPostHandler)
...
}
...
func registerHandler(_ req: Request) throws -> Future<View> {
let context = RegisterContext()
return try req.view().render("register", context)
}
func registerPostHandler(_ req: Request, data: RegisterData) throws -> Future<Response> {
let password = try BCrypt.hash(data.password)
let user = User(name: data.name, username: data.username, password: password)
return user.save(on: req).map(to: Response.self) { user in
try req.authenticateSession(user)
return req.redirect(to: "/")
}
}
导航栏模板 base.leaf 里,未登录时才显示 Register 入口:
...
#if(!userLoggedIn) {
<li class="nav-item #if(title == "Register"){active}">
<a href="/register" class="nav-link">Register</a>
</li>
}
</ul>
...
二、注册用户时的数据有效性验证
基本数据有效性验证
填写表单时,我们要保证数据符合规则,不符合就提示录入错误。Vapor 3 通过 Validatable 与 Reflectable 协议来声明校验规则:
extension RegisterData: Validatable, Reflectable {
static func validations() throws -> Validations<RegisterData> {
var validations = Validations(RegisterData.self)
try validations.add(\.name, .ascii)
try validations.add(\.username, .alphanumeric && .count(3...))
try validations.add(\.password, .count(8...))
return validations
}
}
然后在处理提交时调用 data.validate(),校验失败就重定向回注册页:
...
func registerPostHandler(_ req: Request, data: RegisterData) throws -> Future<Response> {
do {
try data.validate()
} catch {
return req.future(req.redirect(to: "register"))
}
...
}
说明:
.ascii、.alphanumeric、.count(3...) 都是 Vapor 内置的校验器。多个规则之间可以用 && 组合,比如要求用户名既是字母数字、又至少 3 位。自定义数据有效性验证
前面都是针对单个字段的校验,但「两次输入的密码一致」这种跨字段规则 Vapor 没有内建,需要自定义:
extension RegisterData: Validatable, Reflectable {
static func validations() throws -> Validations<RegisterData> {
...
validations.add("passwords match", { (model) in
guard model.password == model.confirmPassword else {
throw BasicValidationError("passwords don't match")
}
})
return validations
}
}
注册页面显示错误信息
校验完成了,但出错时没有途径通知用户修正,所以要在出错时把错误信息显示在页面上。模板里用 #if(message) 渲染提示框:
#set("content") {
<h1>#(title)</h1>
#if(message) {
<div class="alert alert-danger" role="alert">
Please fix the following errors:<br />
#(message)
</div>
}
...
上下文增加可选的 message 字段,并在显示页面时从查询参数里读取:
struct RegisterContext: Encodable {
let title = "Register"
let message: String?
init(message: String? = nil) {
self.message = message
}
}
...
func registerHandler(_ req: Request) throws -> Future<View> {
let context: RegisterContext
if let message = req.query[String.self, at: "message"] {
context = RegisterContext(message: message)
} else {
context = RegisterContext()
}
...
}
提交处理函数捕获校验错误,把错误信息 URL 编码后回带到注册页:
func registerPostHandler(_ req: Request, data: RegisterData) throws -> Future<Response> {
do {
try data.validate()
} catch(let error) {
let redirect: String
if let error = error as? ValidationError,
let message = error.reason.addingPercentEncoding(
withAllowedCharacters: .urlQueryAllowed) {
redirect = "/register?message=\(message)"
} else {
redirect = "/register?message=Unknown+error"
}
return req.future(req.redirect(to: redirect))
}
...
}
}
小提示:错误信息要经过
addingPercentEncoding 做 URL 编码再拼进查询串,否则特殊字符(如空格、&)会破坏 URL。