Vapor 框架學(xué)習(xí)記錄(7)表單框架擴(kuò)展

本篇將全部繼續(xù)高級(jí)表單字段構(gòu)建, 我們將創(chuàng)建一組常用的新字段類型。我們將學(xué)習(xí)如何基于抽象表單域類構(gòu)建自定義表單域,我們會(huì)使用一個(gè)名為 Liquid 的全新 Swift package ,它是為 Vapor 制作的文件存儲(chǔ)驅(qū)動(dòng)程序庫(kù)。 通過使用這個(gè)庫(kù),我們將能夠創(chuàng)建一個(gè)用于上傳圖像的表單字段

隱藏表單

隱藏表單對(duì)用戶來說是不可見的,但我們?nèi)匀豢梢允褂盟ㄟ^表單提交數(shù)據(jù)。 這是一個(gè)非常簡(jiǎn)單的字段類型,需要 HiddenFieldContext 對(duì)象中的key和可選的value

// FILE: Sources/App/Framework/Form/Fields/HiddenFieldContext.swift

public struct HiddenFieldContext {
    
    public let key: String
    public var value: String?
    
    public init(key: String, value: String? = nil) {
        self.key = key
        self.value = value
    }
}

對(duì)應(yīng)的 HiddenFieldTemplate 也非常簡(jiǎn)單,只需要設(shè)置Input 的類型為** .hidden** 和使用context的值

///FILE: Sources/App/Framework/Form/Fields/HiddenFieldTemplate.swift


import Vapor
import SwiftHtml

public struct HiddenFieldTemplate: TemplateRepresentable {
    
    var context: HiddenFieldContext
    
    public init(_ context: HiddenFieldContext) {
        self.context = context
    }
    
    @TagBuilder
    public func render(_ req: Request) -> Tag {
        Input()
            .type(.hidden)
            .name(context.key)
            .value(context.value)
    }
}

第三個(gè)組件是實(shí)際的 HiddenField 類,我們會(huì)把接受字符串作為輸入, HiddenFieldTemplate 作為輸出的類型。 在 process 方法中,我們將輸出的context設(shè)置為已處理的輸入值

/// FILE: Sources/App/Framework/Form/Fields/HiddenField.swift


import Vapor

public final class HiddenField: AbstractFormField<String, HiddenFieldTemplate> {
    
    public convenience init(_ key: String) {
        self.init(key: key, input: "", output: .init(.init(key: key)))
    }
    
    public override func process(req: Request) async throws {
        try await super.process(req: req)
        output.context.value = input
    }
}

就是這樣,我們已經(jīng)準(zhǔn)備好使用這個(gè)全新的input field, 這是一個(gè)很簡(jiǎn)單但可能以后很常用的工具。

文字表單

TextareaField 一般作為文本的輸入表單,我們也將遵循相同的模式去搭建。 首先,我們應(yīng)該為 TextareaFieldContext 對(duì)象創(chuàng)建一個(gè)結(jié)構(gòu)體


/// FILE: Sources/App/Framework/Form/Fields/TextareaFieldContext.swift

public struct TextareaFieldContext {
    public let key: String
    public var label: LabelContext
    public var placeholder: String?
    public var value: String?
    public var error: String?
    
    public init(key: String,
                label: LabelContext? = nil,
                placeholder: String? = nil,
                value: String? = nil,
                error: String? = nil) {
        self.key = key
        self.label = label ?? .init(key: key)
        self.placeholder = placeholder
        self.value = value
        self.error = error
    }
}

textarea contextinput context非常相似,但這里我們可以不需要 type 參數(shù),因?yàn)?textarea 沒有類型。 除了這個(gè)不同之外,其他一切都是一樣的。
現(xiàn)在我們還應(yīng)該為 textarea field 創(chuàng)建一個(gè)模板文件。

import Vapor
import SwiftHtml

public struct TextareaFieldTemplate: TemplateRepresentable {
    
    public var context: TextareaFieldContext
    
    public init(_ context: TextareaFieldContext) {
        self.context = context
    }
    
    @TagBuilder
    public func render(_ req: Request) -> Tag {
        LabelTemplate(context.label).render(req)
        
        Textarea(context.value)
            .placeholder(context.placeholder)
            .name(context.key)
        
        if let error = context.error {
            Span(error)
                .class("error")
        }
        
    }
    
}


就像在 InputFieldTemplate 中我們可以重用常見的 LabelTemplate 來呈現(xiàn)標(biāo)簽的詳細(xì)信息,我們可以使用 Textarea 標(biāo)簽來配置我們的視圖。 最后,如果有任何錯(cuò)誤,我們會(huì)使用帶有錯(cuò)誤的Span 標(biāo)簽來顯示它。
最后,我們還需要?jiǎng)?chuàng)建一個(gè) TextareaField

//FILE: Sources/App/Framework/Form/Fields/TextareaField.swift

import Vapor

public final class TextareaField: AbstractFormField<String, TextareaFieldTemplate> {
    
    
    public convenience init(_ key: String) {
        self.init(key: key, input: "", output: .init(.init(key: key)))
    }
    
    public override func process(req: Request) async throws {
        try await super.process(req: req)
        output.context.value = input
    }
    
    public override func render(req: Request) -> TemplateRepresentable {
        output.context.error = error
        return super.render(req: req)
    }
    
}

處理完輸入值后,我們可以用它更新output context,在渲染模板之前,我們也應(yīng)該將當(dāng)前錯(cuò)誤值分配給output context。

選擇表單

選擇表單字段會(huì)有點(diǎn)復(fù)雜。這個(gè)字段使用具有多個(gè)可用選項(xiàng)。 每個(gè)選項(xiàng)都應(yīng)該有一個(gè)key和一個(gè)label,因?yàn)檫@是一個(gè)經(jīng)常重用的組件,我們將創(chuàng)建一個(gè)獨(dú)立的 OptionContext 來表示它。

// FILE: Sources/App/Framework/Form/Templates/Contexts/OptionContext.swift

public struct OptionContext {
    
    public var key: String
    public var label: String
    
    public init(key: String, label: String) {
        self.key = key
        self.label = label
    }
}

這個(gè)OptionContext結(jié)構(gòu)的好處是你可以定義額外的幫助方法來涵蓋常見情況或選項(xiàng)值,例如是/否選擇或一組數(shù)字。

public extension OptionContext {
    
    static func yesNo() -> [OptionContext] {
        ["yes", "no"].map { .init(key: $0, label: $0.capitalized) }
    }
    
    static func trueFalse() -> [OptionContext] {
        [true, false].map { .init(key: String($0), label: String($0).capitalized) }
    }
    
    static func numbers(_ numbers: [Int]) -> [OptionContext] {
        numbers.map { .init(key: String($0), label: String($0)) }
    }
}

SelectFieldContext 將包含一組選項(xiàng)和一個(gè)可能的值,如果選項(xiàng)鍵和值匹配,則可用于將選項(xiàng)標(biāo)記為選中。 除了這兩個(gè)屬性之外,Context還將具有其他常規(guī)值,例如標(biāo)簽和錯(cuò)誤。

// FILE: Sources/App/Framework/Form/Fields/SelectFieldContext.swift

public struct SelectFieldContext {

    public let key: String
    public var label: LabelContext
    public var options: [OptionContext]
    public var value: String?
    public var error: String?
    
    public init(key: String,
                label: LabelContext? = nil,
                options: [OptionContext] = [],
                value: String? = nil,
                error: String? = nil){
        self.key = key
        self.label = label ?? .init(key: key)
        self.options = options
        self.value = value
        self.error = error
    }
    
}

SelectFieldTemplate 中,我們需要遍歷選項(xiàng)并將它們映射到選項(xiàng)標(biāo)簽中。 我們可以簡(jiǎn)單地將Option的value設(shè)置為item的key并使用Label作為Option的命名。 如果context value與item的key匹配,就設(shè)置為已選擇狀態(tài)。

//FILE: Sources/App/Framework/Form/Fields/SelectFieldTemplate.swift

import Vapor
import SwiftHtml

public struct SelectFieldTemplate: TemplateRepresentable {
    
    public var context: SelectFieldContext
    
    public init(_ context: SelectFieldContext) {
        self.context = context
    }
    
    @TagBuilder
    public func render(_ req: Request) -> Tag {
        
        LabelTemplate(context.label).render(req)
        
        Select {
            for item in context.options {
                Option(item.label)
                    .value(item.key)
                    .selected(context.value == item.key)
            }
        }
        .name(context.key)
            
        if let error = context.error {
            Span(error)
                .class("error")
        }
        
    }
    
}

最后一步是創(chuàng)建常規(guī)表單字段類,這個(gè)流程應(yīng)該很熟悉。


// FILE: Sources/App/Framework/Form/Fields/SelectField.swift

import Vapor

public final class SelectField: AbstractFormField<String, SelectFieldTemplate> {
    
    public convenience init(_ key: String) {
        self.init(key: key, input: "", output: .init(.init(key: key)))
    }
    
    public override func process(req: Request) async throws {
        try await super.process(req: req)
        output.context.value = input
    }
    
    
    public override func render(req: Request) -> TemplateRepresentable {
        output.context.error = error
        return super.render(req: req)
    }
}

如你所見,創(chuàng)建新的表單字段是一個(gè)非常簡(jiǎn)單的過程。 每次你需要一個(gè)context、一個(gè)template和一個(gè)表單對(duì)象來連接context和template。

圖片&文件上傳

現(xiàn)在我們將處理一些更高級(jí)的表單字段, 我們將構(gòu)建一個(gè)圖像上傳表單,但是為了將文件上傳到服務(wù)器,我們需要一些額外的處理。 可以使用 Vapor 將文件從客戶端移動(dòng)到服務(wù)器,但有一種更好的方法來處理文件上傳。

有一個(gè)名為Liquid 的文件存儲(chǔ)組件,它可以使資源管理變得更加容易。 你可以把它想象成 Fluent,它是一個(gè)支持多個(gè)存儲(chǔ)驅(qū)動(dòng)程序的抽象。 你可以使用本地驅(qū)動(dòng)程序?qū)⑽募苯由蟼鞯侥姆?wù)器,但也可以使用 S3-driver將文件存儲(chǔ)在 AWS S3 bucket中

Liquid 的文件通過一個(gè)唯一的密鑰保存在存儲(chǔ)中, 密鑰通常是包含文件夾結(jié)構(gòu)的相對(duì)文件路徑,例如 foo/bar/baz.jpg。 這樣,無(wú)論存儲(chǔ)驅(qū)動(dòng)程序如何,系統(tǒng)都可以解析文件的完整位置。

為了使用 Liquid,我們首先需要添加相關(guān)的Swift Package 依賴。

let package = Package(
    name: "a-Vapor-Blog",
    platforms: [
       .macOS(.v12)
    ],
    dependencies: [
        // ?? A server-side Swift web framework.
        .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
        .package(url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0"),
        .package(url: "https://github.com/vapor/leaf.git", from: "4.0.0"),
        .package(url: "https://github.com/binarybirds/liquid", from: "1.3.0"),
        .package(url: "https://github.com/binarybirds/liquid-local-driver", from:
        "1.3.0"),
        .package(url: "https://github.com/binarybirds/swift-html", from: "1.2.0")
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "Fluent", package: "fluent"),
                .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
                .product(name: "Leaf", package: "leaf"),
                .product(name: "Liquid", package: "liquid"),
                .product(name: "LiquidLocalDriver", package: "liquid-local-driver"),
                .product(name: "SwiftHtml", package: "swift-html"),
                .product(name: "SwiftSvg", package: "swift-html")
            ],
            swiftSettings: [
                // Enable better optimizations when building in Release configuration. Despite the use of
                // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
                // builds. See <https://github.com/swift-server/guides/blob/main/docs/building.md#building-for-production> for details.
                .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
            ]
        ),
        .executableTarget(name: "Run", dependencies: [.target(name: "App")]),
        .testTarget(name: "AppTests", dependencies: [
            .target(name: "App"),
            .product(name: "XCTVapor", package: "vapor"),
        ])
    ]
)

這里我們添加好了Liquid,為了簡(jiǎn)單起見,我們將使用本地驅(qū)動(dòng)程序。 publicUrl 參數(shù)是你的公開文件的base URL。 它將用于解析文件密鑰。 publicPath 是公用文件夾的位置,workDirectory 將用作公用文件夾下的根目錄來存儲(chǔ)文件。


/// FILE: Sources/App/configure.swift

import Vapor
import Fluent
import FluentSQLiteDriver
import Liquid
import LiquidLocalDriver

// configures your application
public func configure(_ app: Application) throws {
    // uncomment to serve files from /Public folder
    app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
    
    /// extend paths to always contain a trailing slash
    app.middleware.use(ExtendPathMiddleware())
    
    /// setup Fluent with a SQLite database under the Resources directory
    let dbPath = app.directory.resourcesDirectory + "db.sqlite"
    app.databases.use(.sqlite(.file(dbPath)), as: .sqlite)
    
    /// setup Liquid using the local file storage driver
    app.fileStorages.use(.local(publicUrl: "http://localhost:8080",
                                publicPath: app.directory.publicDirectory,
                                workDirectory: "assets"), as: .local)
    
    /// set the max file upload limit
    app.routes.defaultMaxBodySize = "10mb"
    
    /// setup Sessions
    app.sessions.use(.fluent)
    app.migrations.add(SessionRecord.migration)
    app.middleware.use(app.sessions.middleware)
    
    /// setup modules
    let modules: [ModuleInterface] = [
        WebModule(),
        BlogModule(),
        UserModule()
    ]
    for module in modules {
        try module.boot(app)
    }

    /// use automatic database migration
    try app.autoMigrate().wait()
}

為了能夠收集上傳的數(shù)據(jù),我們還必須在App.Routes屬性上設(shè)置DefaultMaxBodysize值。 目前來說,“ 10MB”的上限是足夠的。 請(qǐng)注意,DefaultMaxBodysize是對(duì)全局的修改,實(shí)際上針對(duì)特別的路由對(duì)對(duì)應(yīng)的限制才是合適的做法,這里我們?yōu)榱朔奖憔褪褂萌值膶傩孕薷摹?/p>

在我們開始 InputField 開發(fā)之前,我們還有一些準(zhǔn)備工作。 有時(shí) Vapor 有一些奇怪的命名約定,文件類型的data value實(shí)際上代表一個(gè)ByteBuffer 對(duì)象,所以讓我們快速為該屬性創(chuàng)建一個(gè)別名方便理解。

/// FILE: Sources/App/Framework/Extensions/File+ByteBuffer.swift

import Vapor

public extension File {
    var byteBuffer: ByteBuffer { data }
}

ByteBuffer 類型創(chuàng)建一個(gè)可選的數(shù)據(jù)擴(kuò)展也會(huì)讓我們的使用更方便,這樣我們就可以返回buffer包含的全部數(shù)據(jù)。

/// FILE: Sources/App/Framework/Extensions/ByteBuffer+Data.swift

import Vapor

public extension ByteBuffer {
    var data: Data? { getData(at: 0, length: readableBytes) }
}

那么,當(dāng)我們嘗試上傳圖片時(shí),我們需要什么樣的數(shù)據(jù)呢?

渲染表單的時(shí)候我們需要有原圖,所以我們需要一些東西來表示原圖的key。 我們?yōu)榱舜_定能夠上傳文件,需要一個(gè)臨時(shí)文件存儲(chǔ),我們可以在其中存儲(chǔ)新的key和名稱值。 有時(shí)我們不需要對(duì)應(yīng)圖像,為此我們可以引入一個(gè)簡(jiǎn)單的 Bool 標(biāo)志來標(biāo)記移除。

讓我們創(chuàng)建一個(gè)表示此結(jié)構(gòu)的新 FormImageData 類型,我們應(yīng)該使其符合 Codable 協(xié)議,因?yàn)槲覀兿胍獙?duì)其進(jìn)行編碼或解碼

/// FILE: Sources/App/Framework/Form/FormImageData.swift

import Foundation

public struct FormImageData: Codable {
    
    public struct TemporaryFile: Codable {
        public let key: String
        public let name: String
        
        public init(key: String, name: String) {
            self.key = key
            self.name = name
        }
        
    }
    
    public var originalKey: String?
    public var temporaryFile: TemporaryFile?
    public var shouldRemove: Bool
    
    public init(originalKey: String? = nil,
                temporaryFile: TemporaryFile? = nil,
                shouldRemove: Bool = false) {
        
        self.originalKey = originalKey
        self.temporaryFile = temporaryFile
        self.shouldRemove = shouldRemove
    }
}

除了常規(guī)的key、label和error之外,我們將使用這個(gè) FormImageData 作為 ImageFieldContext 結(jié)構(gòu)中的數(shù)據(jù)對(duì)象。 我們還將使用 previewUrlaccept 屬性來設(shè)置模板。

/// FILE: Sources/App/Framework/Form/Fields/ImageFieldContext.swift

public struct ImageFieldContext {
    
    public let key: String
    public var label: LabelContext
    public var data: FormImageData
    public var previewUrl: String?
    public var accept: String?
    public var error: String?
    
    public init(key: String,
                label: LabelContext? = nil,
                data: FormImageData = .init(),
                previewUrl: String? = nil,
                accept: String? = nil,
                error: String? = nil) {
        
        self.key = key
        self.label = label ?? .init(key: key)
        self.data = data
        self.previewUrl = previewUrl
        self.accept = accept
        self.error = error
        
    }
}

ImageFieldTemplate 會(huì)比之前的模塊更復(fù)雜。在渲染模板的第一部分,如果有 previewUrl 值,我們將嘗試將 previewUrl 顯示為圖像。

接下來我們像往常一樣顯示label,并使用context中的key和accept value添加一個(gè)文件類型的input field。使用 accept 值可以限制用戶在上傳過程中可以選擇的文件類型,該值應(yīng)該是有效的媒體類型,例如 image/png

當(dāng)提交過程中表單出現(xiàn)錯(cuò)誤時(shí),我們需要臨時(shí)文件。如果在驗(yàn)證過程中出現(xiàn)問題,如果我們不重新提交文件key和name作為輸入值,我們可能會(huì)丟失上傳的圖片。這樣即使其他字段不正確,我們也不會(huì)丟失上傳的圖像文件,我們只需將臨時(shí)文件移動(dòng)到其最終位置。這與我們可能會(huì)提交原始密鑰(如果有的話)的原因相同。

最后一個(gè)輸入字段指示用戶是否要?jiǎng)h除上傳的圖像。

/// FILE: Sources/App/Framework/Form/Fields/ImageFieldTemplate.swift


import Vapor
import SwiftHtml

public struct ImageFieldTemplate: TemplateRepresentable {
 
    
    public var context: ImageFieldContext
    
    
    public init(_ context: ImageFieldContext) {
        self.context = context
    }
    
    @TagBuilder
    public func render(_ req: Request) -> Tag {
        
        if let url = context.previewUrl {
            Img(src: url, alt: context.key)
        }
        
        LabelTemplate(context.label).render(req)
        
        Input()
            .type(.file)
            .key(context.key)
            .class("field")
            .accept(context.accept)
        
        if let temporaryFile = context.data.temporaryFile {
            
            Input()
                .key(context.key + "TemporaryFileKey")
                .value(temporaryFile.key)
                .type(.hidden)
            
            Input()
                .key(context.key + "TemporaryFileName")
                .value(temporaryFile.name)
                .type(.hidden)
        }
        
        if let key = context.data.originalKey {
            Input()
                .key(context.key + "OriginalKey")
                .value(key)
                .type(.hidden)
        }
        
        if !context.label.required {
            Input()
                .key(context.key + "ShouldRemove")
                .value(String(true))
                .type(.checkbox)
                .checked(context.data.shouldRemove)
            
            Label("Remove")
                .for(context.key + "Remove")
        }
        
        if let error = context.error {
            
            Span(error)
                .class("error")
            
        }
        
    }
}

現(xiàn)在我們可以渲染image field,我們?nèi)匀恍枰韱巫侄巫宇悂硖幚硭⑽募蟼鞯椒?wù)器。 在我們進(jìn)入該部分之前,我們將再定義一個(gè)輔助對(duì)象,它將作為抽象表單字段的輸入類型。

FormImageInput 結(jié)構(gòu)將有一個(gè)key、一個(gè)file value,它將表示上傳的文件數(shù)據(jù)和一個(gè)FormImageData 類型的數(shù)據(jù)對(duì)象。

/// FILE: Sources/App/Framework/Form/FormImageInput.swift

import Vapor

public struct FormImageInput: Codable {
    
    public var key: String
    public var file: File?
    public var data: FormImageData
    
    public init(key: String, file: File? = nil, data: FormImageData? = nil) {
        self.key = key
        self.file = file
        self.data = data ?? .init()
    }
}

現(xiàn)在我們可以在創(chuàng)建 ImageField 時(shí)使用 FormImageInput 作為輸入值,使用 ImageFieldTemplate 作為輸出類型。 我們將使用一個(gè)公共 imageKey 變量來存儲(chǔ)當(dāng)前密鑰,并使其也可供其他人訪問。 path 變量將是圖像鍵的前綴,它只是我們保存上傳文件的目錄路徑。

process函數(shù)將比以前用于其他字段更有趣。 首先,我們嘗試根據(jù)我們?cè)?strong>template文件中使用的key對(duì)Input進(jìn)行解碼。 在我們擁有完整的輸入數(shù)據(jù)后,我們檢查是否應(yīng)該刪除文件,并根據(jù)其他輸入值執(zhí)行相應(yīng)的操作。

如果文件應(yīng)該被刪除并且有一個(gè)原始密鑰,這意味著我們必須使用 req.fs.delete(key:) 方法刪除原始文件。

如果有用戶提交的某種圖片數(shù)據(jù),我們首先要檢查臨時(shí)文件,然后根據(jù)key刪除,因?yàn)槲覀円葘⑿聰?shù)據(jù)上傳到服務(wù)器,并作為臨時(shí)文件存儲(chǔ)。

您可以通過調(diào)用 try await req.fs.upload(key: key, data: data) 方法使用 Liquid 上傳文件。 默認(rèn)情況下,它會(huì)返回上傳文件的完整 URL,但我們現(xiàn)在不關(guān)心這個(gè)。

作為最后一步,我們可以使用當(dāng)前輸入數(shù)據(jù)更新out context數(shù)據(jù),我們就完成了。

/// FILE: Sources/App/Framework/Form/Fields/ImageField.swift

import Vapor

public final class ImageField: AbstractFormField<FormImageInput, ImageFieldTemplate> {
    
    public var imageKey: String? {
        didSet {
            output.context.data.originalKey = imageKey
        }
    }
    
    public var path: String
    
    public init(_ key: String, path: String) {
        self.path = path
        super.init(key: key, input: .init(key: key), output: .init(.init(key: key)))
    }
    
    public override func process(req: Request) async throws {
        /// process input
        input.file = try? req.content.get(File.self, at: key)
        input.data.originalKey = try? req.content.get(String.self, at: key + "OriginalKey")
        
        if let temporaryFileKey = try? req.content.get(String.self, at: key + "TemporaryFileKey"), let temporaryFileName = try? req.content.get(String.self, at: key + "TemporaryFileName") {
            input.data.temporaryFile = .init(key: temporaryFileKey, name: temporaryFileName)
            
        }
        
        input.data.shouldRemove = (try? req.content.get(Bool.self, at: key + "ShouldRemove")) ?? false
        
        /// remove & upload file
        if input.data.shouldRemove {
            if let originalKey = input.data.originalKey {
                try? await req.fs.delete(key: originalKey)
            }
        }
        else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty {
            if let tmpKey = input.data.temporaryFile?.key {
                try? await req.fs.delete(key: tmpKey)
            }
            
            let key = "tmp/\(UUID().uuidString).tmp"
            
            _ = try await req.fs.upload(key: key, data: data)
            
            /// update the temporary image
            
            input.data.temporaryFile = .init(key: key, name: file.filename)
            
            
        }
        
        /// update output values
        output.context.data = input.data
    }
    
    public override func write(req: Request) async throws {
        
        imageKey = input.data.originalKey
        
        if input.data.shouldRemove {
            if let key = input.data.originalKey {
                try? await req.fs.delete(key: key)
            }
            imageKey = nil
        }
        else if let file = input.data.temporaryFile {
            
            var newKey = path + "/" + file.name
            if await req.fs.exists(key: newKey) {
                let formatter = DateFormatter()
                formatter.dateFormat="y-MM-dd-HH-mm-ss-"
                let prefix = formatter.string(from: .init())
                newKey = path + "/" + prefix + file.name
            }
            
            _ = try await req.fs.move(key: file.key, to: newKey)
            input.data.temporaryFile = nil
            if let key = input.data.originalKey {
                try? await req.fs.delete(key: key)
            }
            imageKey = newKey
        }
        
        try await super.write(req: req)
        
    }
    
    public override func render(req: Request) -> TemplateRepresentable {
        output.context.error = error
        return super.render(req: req)
    }
    
}

write 函數(shù)調(diào)用發(fā)生在驗(yàn)證步驟成功后,因此現(xiàn)在可以安全地將上傳的文件移動(dòng)到最終目的地。首先,我們必須檢查是否有刪除操作,如果我們必須執(zhí)行此操作,我們只需根據(jù)原始密鑰刪除文件。

否則我們可以確定當(dāng)前上傳的文件已經(jīng)作為臨時(shí)文件存儲(chǔ)在服務(wù)器上,我們可以將其移動(dòng)到 assets 目錄。如果已經(jīng)存在具有給定key的文件,我們將在文件名前加上當(dāng)前時(shí)間戳。

然后我們可以使用 req.fs.move 將臨時(shí)文件移動(dòng)到 assets 目錄,如果存在則刪除原始密鑰,因?yàn)槲覀儎倓傆眯旅荑€替換了它。

我們將最終密鑰存儲(chǔ)在 imageKey 屬性中,并調(diào)用 super.write(req:) 來處理進(jìn)一步的操作。


ImageField("image", path: "blog/post") .read {
if let key = model.imageKey {
$1.output.context.previewUrl = $0.fs.resolve(key: key)
}
($1 as! ImageField).imageKey = model.imageKey }
.write { model.imageKey = ($1 as! ImageField).imageKey }

類似上面這樣簡(jiǎn)單的代碼,我們就可以使用ImageField完成圖片上傳。

最后

本章主要介紹新的表單字段。 我們?yōu)樘峤徊豢梢姷膋ey value創(chuàng)建了一個(gè)隱藏表單字段,并為多行的用戶輸入添加了一個(gè) textarea 字段。 選擇表單字段是一種更復(fù)雜的類型,能夠從選項(xiàng)數(shù)組中選擇給定值。 最后,我們?cè)陧?xiàng)目中添加了 Liquid 文件存儲(chǔ)驅(qū)動(dòng)程序,這使我們可以輕松地將文件上傳到服務(wù)器。 通過利用 Liquid,我們能夠定義一個(gè)全新的 ImageField,它將幫助我們上傳圖像文件,在我們不再需要它們時(shí)替換或刪除它們。 在下一篇中,我們將利用這些新的組件,并為我們的博客模塊創(chuàng)建一個(gè)基本的 CMS 界面。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容