美文网首页Vapor 学习笔记
Vapor 框架学习记录(6)表单组件扩展与验证器

Vapor 框架学习记录(6)表单组件扩展与验证器

作者: lqbk | 来源:发表于2022-05-13 09:37 被阅读0次

    在本篇的第一部分,我们将稍微研究一下表单组件。 我们将实现更多的事件处理方法,将学习到调用它们的最佳方式,以便构建正确的创建或更新的工作流。 本篇的后半部分是关于为抽象表单构建异步验证机制。 我们将构建几个表单字段验证器,然后使用这些验证器显示用户错误以改善整体体验。

    表单事件处理器

    在上一章中,我们创建了一个用户登录表单。 主要想法是,我们将为每个输入字段创建一个带有contextview model 的模板,因此我们可以使用几行代码组成各种表单。

    现在我们有了能够处理用户输入这方面的基础 blocks,但是我们还没有实现 FormFieldComponent 协议的一些其他方法。 让我们通过使用一个通用模式来完善它,我们将为几乎所有单个事件方法遵循该模式。

    /// FILE: Sources/App/Framework/Form/AbstractFormField.swift
    
    import Vapor
    
    open class AbstractFormField<Input: Decodable, Output: TemplateRepresentable>: FormComponent {
        
        public var key: String
        public var input: Input
        public var output: Output
        public var error: String?
        
        //MARK: - event blocks
        
        public typealias FormFieldBlock = (Request, AbstractFormField<Input, Output>) async throws -> Void
        
        private var readBlock: FormFieldBlock?
        private var writeBlock: FormFieldBlock?
        private var loadBlock: FormFieldBlock?
        private var saveBlock: FormFieldBlock?
        
        //MARK: - init & config
        
        public init(key: String,
                    input: Input,
                    output: Output,
                    error: String? = nil) {
            self.key = key
            self.input = input
            self.output = output
            self.error = error
        }
        
        open func config(_ block: (AbstractFormField<Input, Output>) -> Void) -> Self {
            block(self)
            return self
        }
        
        // MARK: - Block setters
        
        public func read(_ block: @escaping FormFieldBlock) -> Self {
            readBlock = block
            return self
        }
        
        public func write(_ block: @escaping FormFieldBlock) -> Self {
            writeBlock = block
            return self
        }
        
        public func load(_ block: @escaping FormFieldBlock) -> Self {
            loadBlock = block
            return self
        }
        
        public func save(_ block: @escaping FormFieldBlock) -> Self {
            saveBlock = block
            return self
        }
        
        
        
        // MARK: - FormComponent
    
        
        public func process(req: Request) async throws {
            if let value = try? req.content.get(Input.self, at: key) {
                input = value
            }
        }
        
        public func validate(req: Request) async throws -> Bool {
            return true
        }
        
        public func read(req: Request) async throws {
            try await readBlock?(req, self)
        }
        
        public func write(req: Request) async throws {
            try await writeBlock?(req, self)
        }
        
        public func load(req: Request) async throws {
            try await loadBlock?(req, self)
        }
        
        public func save(req: Request) async throws {
            try await saveBlock?(req, self)
        }
    
        public func render(req: Request) -> TemplateRepresentable {
            return output
        }
    }
    
    

    我们创建了四个新的可选 FormFieldBlock 变量来处理各种事件。 这些变量是私有的,因此我们需要四个新的 setter 方法才能为它们赋予新值。 setter 方法将像构建器或修改器一样,在设置值之后,我们将返回当前实例。这种模式将允许我们设置表单字段并立即为它们定义事件处理程序,下面是一个例子:

    InputField("name")
              .load {
                  $1.output.context.value = "John Doe"
              }
              .save {
                  print("Hello, my name is \($1.input)!")
              }
    
    

    在这种情况下,我们可以使用 load 方法更新fieldoutput context value,并且在 save 方法中我们还可以执行操作来处理输入。 read / write 方法的用途也一样,不同之处在于执行顺序。

    下面是 FormFieldComponent 事件的建议执行顺序:

    显示表格时:

    • load
    • read
    • render

    处理提交事件时:

    • load
    • process
    • validate
    • render if invalid write
    • write
    • save

    这将是我们在 admin controllers中的工作流,在我们实现 CMS 之前,我们仍然需要处理表单验证。

    异步表单验证

    验证传入的表单字段一个重要的事情。 Vapor 有一个内置的验证 API 来验证所有类型的输入数据,但是这个系统有一些问题:

    • 你不能提供自定义错误消息
    • 验证错误详细信息始终是一个串联的字符串(如果有多个错误)
    • 无法从错误详细信息字符串中获取确定的错误标识
    • 验证是同步的(不能基于数据库查询进行验证)

    这是非常不幸的,因为 Vapor 有一些非常好的验证器方法,但他们更多地关注 API 验证而不是表单验证。

    我们将构建一组统一的异步验证 helpers,可用于表单和 API 验证目的。 我们还将更多地讨论 API 验证,不过现在让我们只关注 HTML 表单和验证输入字段。

    我们首先需要的是带有相关错误消息的对象。 在 Framework 目录下创建一个 Validation 文件夹,并创建一个的 ValidationErrorDetail 文件

    /// FILE: Sources/App/Framework/Validation/ValidationErrorDetail.swift
    
    import Vapor
    
    public struct ValidationErrorDetail: Codable {
        
        public var key: String
        public var message: String
        
        public init(key: String, message: String) {
            self.key = key
            self.message = message
        }
        
    }
    
    extension ValidationErrorDetail: Content {}
    

    我们将使用此对象根据key作为表单字段错误的唯一标识

    现在我们需要一个协议,我们可以用它来以通用的方式验证表单字段。 我们将需要keymessage以及一个异步验证函数,如果出现错误,该函数可以返回一个可选的 ValidationErrorDetail 对象

    请注意,此函数可以抛出,但我们只会在发生系统错误时抛出错误,例如数据库故障或类似情况。

    /// FILE: Sources/App/Framework/Validation/AsyncValidator.swift
    
    
    import Vapor
    
    public protocol AsyncValidator {
        
        var key: String { get }
        var message: String { get }
        
        func validate(_ req: Request) async throws -> ValidationErrorDetail?
    }
    
    public extension AsyncValidator {
        
        var error: ValidationErrorDetail {
            .init(key: key, message: message)
        }
        
    }
    
    
    

    我们要创建一个新的 ValidationAbort 结构体,因为默认的验证响应不会包含有关错误的必要信息,但我们的 ValidationErrorDetail 具有有关有问题的键的更多详细信息,并且还具有适当的错误消息。

    /// FILE: Sources/App/Framework/Validation/ValidationAbort.swift
    
    
    import Vapor
    
    public struct ValidationAbort: AbortError {
        
        public var abort: Abort
        public var message: String?
        public var details: [ValidationErrorDetail]
        
        public var reason: String { abort.reason }
        public var status: HTTPStatus { abort.status }
        
        
        public init(abort: Abort, message: String? = nil, details: [ValidationErrorDetail]) {
            self.abort = abort
            self.message = message
            self.details = details
        }
        
    }
    
    

    ValidationAbort 类型将实现 VaporAbortError 协议,这是一个可以抛出的错误,并且系统可以在需要时将其转换为正确的 HTTP 响应。 我们添加了一个 abort 属性,这样我们就可以返回一个自定义状态代码和一个通用错误消息,就像我们为 AbstractForm 对象所做的那样。 我们还包括详细信息,该数组将包含我们在请求中遇到的所有问题

    RequestValidator 中,我们将调用 AsyncValidator 协议对象数组上的 validate 方法。 我们可以通过检查结果数组中的keys来优化过程,因此如果与给定key关联的字段已经无效,我们不必运行剩余的验证器。 此外,如果请求验证器失败,这意味着结果数组中有错误,我们可以抛出 ValidationAbort

    /// FILE: Sources/App/Framework/Validation/RequestValidator.swift
    
    import Vapor
    
    public struct RequestValidator {
        
        public var validators: [AsyncValidator]
        
        public init(_ validators: [AsyncValidator]) {
            self.validators = validators
        }
    
        public func validate(_ req: Request, message: String? = nil) async throws {
            var result: [ValidationErrorDetail] = []
            
            for validator in validators {
                
                if result.contains(where: { $0.key == validator.key }) {
                    continue
                }
                
                if let res = try await validator.validate(req) {
                    result.append(res)
                }
            }
            
            if !result.isEmpty {
                throw ValidationAbort(abort: Abort(.badRequest, reason: message), details: result)
            }
        }
        
        
        public func isValid(_ req: Request) async -> Bool {
            do {
                try await validate(req, message: nil)
                return true
            }
            catch {
                return false
            }
        }
    }
    
    

    这次我们总是抛出一个错误,而不是返回 ValidationErrorDetail 对象数组,因为我们需要 JSON 相关 API 的中止错误。 我们仍然可以使用此方法并通过尝试 validate 方法来检查请求是否有效。 如果调用失败,我们可以返回 false 值,否则我们返回 true

    现在我们有能力异步验证事物并且我们可以验证整个请求对象,是时候用一个验证器来检查输入值并将错误消息作为输出传递给给定的表单字段。 我们将其称为 FormFieldValidator 对象,它是一个通用结构,具有关联的 Decodable 输入和 TemplateRepresentable 输出(就像 AbstractFormField 一样)类型,当然它符合 AsyncValidator 协议

    /// FILE: Sources/App/Validation/FormFieldValidator.swift
    
    import Vapor
    
    public struct FormFieldValidator<Input: Decodable, Output: TemplateRepresentable>: AsyncValidator {
        
        
        public let field: AbstractFormField<Input, Output>
        public let message: String
        public let validation: ((Request, AbstractFormField<Input, Output>) async throws -> Bool)
        
        public var key: String { field.key }
        
        public init(_ field: AbstractFormField<Input, Output>,
                    _ message: String,
                    _ validation: @escaping ((Request, AbstractFormField<Input, Output>) async throws -> Bool)) {
            self.field = field
            self.message = message
            self.validation = validation
        }
    
        public func validate(_ req: Request) async throws -> ValidationErrorDetail? {
            let isValid = try await validation(req, field)
            if isValid {
                return nil
            }
            
            field.error = message
            return error
        }
        
    }
    
    

    init 方法将接受三个参数,第一个是指向 AbstractFormField 实例的指针,第二个是错误消息,第三个是我们将在必须验证输入时运行的验证闭包。 在 validate 方法中,我们简单地调用存储的验证块。 如果输入有效,我们将返回 nil 值,如果有错误,我们会使用引用在字段上设置错误消息,并返回错误详细信息作为结果

    这种方法的好处是我们仍然可以使用内置的 Vapor 验证器方法并创建辅助方法来根据输入类型验证我们的表单字段。 例如,字符串验证是一个非常常见的case,因此定义扩展是有意义的

    /// FILE: Sources/App/Validation/FormFieldValidator.swift
    
    public extension FormFieldValidator where Input == String {
        
        static func required(_ field: AbstractFormField<Input, Output>, _ message: String? = nil) -> FormFieldValidator<Input, Output> {
            .init(field, message ?? "\(field.key.capitalized) is required") { _, field in field.input.isEmpty }
        }
        
        static func min(_ field: AbstractFormField<Input, Output>, length: Int, message: String? = nil) -> FormFieldValidator<Input, Output> {
            let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)"
            return .init(field, msg) { _, field in field.input.count >= length }
        }
        
        static func max(_ field: AbstractFormField<Input, Output>, length: Int, message: String? = nil) -> FormFieldValidator<Input, Output> {
            let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)"
            return .init(field, msg) { _, field in field.input.count <= length }
        }
        
        static func alphanumeric(_ field: AbstractFormField<Input, Output>, message: String? = nil) -> FormFieldValidator<Input, Output> {
            let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters"
            return .init(field, msg) { _, field in Validator.characterSet(.alphanumerics).validate(field.input).isFailure }
        }
        
        static func email(_ field: AbstractFormField<Input, Output>, message: String? = nil) -> FormFieldValidator<Input, Output> {
            let msg = message ?? "\(field.key.capitalized) should be a valid email address"
            return .init(field, msg) { _, field in Validator.email.validate(field.input).isFailure }
        }
        
    }
    
    

    在我们更改 AbstractFormField 组件之前,我们将添加一个更方便的枚举,我们可以使用它通过结果构建器返回一组 AsyncValidator 对象

    
    /// FILE: Sources/App/Validation/AsyncValidatorBuilder.swift
    
    
    @resultBuilder
    public enum AsyncValidatorBuilder {
        public static func buildBlock(_ components: AsyncValidator...) -> [AsyncValidator] {
            components
        }
    }
    
    

    现在我们就可以回去改造 AbstractFormField

    /// FILE: Sources/App/Framework/Form/AbstractFormField.swift
    
    import Vapor
    
    open class AbstractFormField<Input: Decodable, Output: TemplateRepresentable>: FormComponent {
        
        public var key: String
        public var input: Input
        public var output: Output
        public var error: String?
        
        //MARK: - event blocks
        
        public typealias FormFieldBlock = (Request, AbstractFormField<Input, Output>) async throws -> Void
        public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField<Input, Output>) -> [AsyncValidator])
        
        private var readBlock: FormFieldBlock?
        private var writeBlock: FormFieldBlock?
        private var loadBlock: FormFieldBlock?
        private var saveBlock: FormFieldBlock?
        private var validatorsBlock: FormFieldValidatorsBlock?
        
        //MARK: - init & config
        
        public init(key: String,
                    input: Input,
                    output: Output,
                    error: String? = nil) {
            self.key = key
            self.input = input
            self.output = output
            self.error = error
        }
        
        open func config(_ block: (AbstractFormField<Input, Output>) -> Void) -> Self {
            block(self)
            return self
        }
        
        // MARK: - Block setters
        
        public func read(_ block: @escaping FormFieldBlock) -> Self {
            readBlock = block
            return self
        }
        
        public func write(_ block: @escaping FormFieldBlock) -> Self {
            writeBlock = block
            return self
        }
        
        public func load(_ block: @escaping FormFieldBlock) -> Self {
            loadBlock = block
            return self
        }
        
        public func save(_ block: @escaping FormFieldBlock) -> Self {
            saveBlock = block
            return self
        }
        
        open func validators(@AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock) -> Self {
            validatorsBlock = block
            return self
        }
        
        
        // MARK: - FormComponent
    
        
        public func process(req: Request) async throws {
            if let value = try? req.content.get(Input.self, at: key) {
                input = value
            }
        }
        
        public func validate(req: Request) async throws -> Bool {
            guard let validators = validatorsBlock else {
                return true
            }
            return await RequestValidator(validators(req, self)).isValid(req)
        }
        
        public func read(req: Request) async throws {
            try await readBlock?(req, self)
        }
        
        public func write(req: Request) async throws {
            try await writeBlock?(req, self)
        }
        
        public func load(req: Request) async throws {
            try await loadBlock?(req, self)
        }
        
        public func save(req: Request) async throws {
            try await saveBlock?(req, self)
        }
    
        public func render(req: Request) -> TemplateRepresentable {
            return output
        }
    }
    
    

    现在让我们更新我们的 UserLoginForm 并验证电子邮件地址和密码字段是否正确

    /// FILE: Sources/App/Modules/User/Forms/UserLoginForm.swift
    
    import Vapor
    
    final class UserLoginForm: AbstractForm {
     
        public convenience init() {
            self.init(action: .init(method: .post, url: "/sign-in/"),
                      submit: "Sign in")
            self.fields = createFields()
        }
        
        @FormComponentBuilder
        func createFields() -> [FormComponent] {
            InputField("email")
                .config {
                    $0.output.context.label.required = true
                    $0.output.context.type = .email
                }
                .validators {
                    FormFieldValidator.required($1)
                    FormFieldValidator.email($1)
                }
            InputField("password")
                .config {
                    $0.output.context.label.required = true
                    $0.output.context.type = .password
                }
                .validators {
                    FormFieldValidator.required($1)
                }
        }
    }
    
    

    回到 UserFrontendController ,我们仍然需要调用表单上的 validate 方法。

    /// FILE: Sources/App/Modules/User/Controllers/UserFrontendController.swift
    
    import Vapor
    
    struct UserFrontendController {
            
        func renderSignInView(_ req: Request, _ form: UserLoginForm) -> Response {
            let template = UserLoginTemplate(context: .init(icon: "⬇️",
                                                            title: "Sign in",
                                                            message: "Please log in with your existing account",
                                                            form: form.render(req: req)))
            
            return req.templates.renderHtml(template)
        }
        
        func signInView(_ req: Request) async throws -> Response {
            return renderSignInView(req, .init())
        }
        
        func signInAction(_ req: Request) async throws -> Response {
            /// the user is authenticated, we can store the user data inside the session too
            if let user = req.auth.get(AuthenticatedUser.self) {
                req.session.authenticate(user)
                return req.redirect(to: "/")
            }
            
            let form = UserLoginForm()
            try await form.process(req: req)
            if try await form.validate(req: req) {
                form.error = "Invalid email or password."
            }
            return renderSignInView(req, form)
        }
        
        func signOut(req: Request) throws -> Response {
            req.auth.logout(AuthenticatedUser.self)
            req.session.unauthenticate(AuthenticatedUser.self)
            return req.redirect(to: "/")
        }
        
    }
    
    

    现在,如果你运行该项目,你应该会看到我们有更好的用户体验,如果登录表单缺少输入值,用户将知道它。 如果两个字段均已填写,但凭据不正确,则我们将仅显示一条错误消息

    使用这种方法设置验证器非常简单,你可以在 AbstractFormField 类上添加更多验证器函数作为扩展,也可以使用自定义验证器

    总结

    本篇继续搭建了我们表单框架的基础,现在有了更多的事件处理程序和表单验证的设计了。可以支撑我们接入更多的表单字段了。

    相关文章

      网友评论

        本文标题:Vapor 框架学习记录(6)表单组件扩展与验证器

        本文链接:https://www.haomeiwen.com/subject/rihfyrtx.html