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

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

隱藏表單

隱藏表單對用戶來說是不可見的琴昆,但我們?nèi)匀豢梢允褂盟ㄟ^表單提交數(shù)據(jù)氓鄙。 這是一個非常簡單的字段類型,需要 HiddenFieldContext 對象中的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
    }
}

對應(yīng)的 HiddenFieldTemplate 也非常簡單业舍,只需要設(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)
    }
}

第三個組件是實(shí)際的 HiddenField 類,我們會把接受字符串作為輸入升酣, 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)備好使用這個全新的input field下面, 這是一個很簡單但可能以后很常用的工具。

文字表單

TextareaField 一般作為文本的輸入表單绩聘,我們也將遵循相同的模式去搭建沥割。 首先,我們應(yīng)該為 TextareaFieldContext 對象創(chuàng)建一個結(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 沒有類型。 除了這個不同之外衅谷,其他一切都是一樣的椒拗。
現(xiàn)在我們還應(yīng)該為 textarea field 創(chuàng)建一個模板文件。

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)簽來配置我們的視圖蚀苛。 最后,如果有任何錯誤玷氏,我們會使用帶有錯誤的Span 標(biāo)簽來顯示它堵未。
最后,我們還需要創(chuàng)建一個 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)前錯誤值分配給output context

選擇表單

選擇表單字段會有點(diǎn)復(fù)雜拙徽。這個字段使用具有多個可用選項(xiàng)刨沦。 每個選項(xiàng)都應(yīng)該有一個key和一個label,因?yàn)檫@是一個經(jīng)常重用的組件膘怕,我們將創(chuàng)建一個獨(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
    }
}

這個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)和一個可能的值来破,如果選項(xiàng)鍵和值匹配,則可用于將選項(xiàng)標(biāo)記為選中忘古。 除了這兩個屬性之外徘禁,Context還將具有其他常規(guī)值,例如標(biāo)簽和錯誤髓堪。

// 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)簽中。 我們可以簡單地將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ī)表單字段類争群,這個流程應(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)建新的表單字段是一個非常簡單的過程换薄。 每次你需要一個context玉雾、一個template和一個表單對象來連接context和template。

圖片&文件上傳

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

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

Liquid 的文件通過一個唯一的密鑰保存在存儲中, 密鑰通常是包含文件夾結(jié)構(gòu)的相對文件路徑革半,例如 foo/bar/baz.jpg碑定。 這樣流码,無論存儲驅(qū)動程序如何,系統(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碘赖,為了簡單起見驾荣,我們將使用本地驅(qū)動程序。 publicUrl 參數(shù)是你的公開文件的base URL普泡。 它將用于解析文件密鑰播掷。 publicPath 是公用文件夾的位置,workDirectory 將用作公用文件夾下的根目錄來存儲文件撼班。


/// 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”的上限是足夠的件炉。 請注意,DefaultMaxBodysize是對全局的修改矮湘,實(shí)際上針對特別的路由對對應(yīng)的限制才是合適的做法妻率,這里我們?yōu)榱朔奖憔褪褂萌值膶傩孕薷摹?/p>

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

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

import Vapor

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

ByteBuffer 類型創(chuàng)建一個可選的數(shù)據(jù)擴(kuò)展也會讓我們的使用更方便,這樣我們就可以返回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ù)據(jù)呢说搅?

渲染表單的時候我們需要有原圖炸枣,所以我們需要一些東西來表示原圖的key。 我們?yōu)榱舜_定能夠上傳文件弄唧,需要一個臨時文件存儲适肠,我們可以在其中存儲新的key和名稱值。 有時我們不需要對應(yīng)圖像候引,為此我們可以引入一個簡單的 Bool 標(biāo)志來標(biāo)記移除侯养。

讓我們創(chuàng)建一個表示此結(jié)構(gòu)的新 FormImageData 類型,我們應(yīng)該使其符合 Codable 協(xié)議澄干,因?yàn)槲覀兿胍獙ζ溥M(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之外柠傍,我們將使用這個 FormImageData 作為 ImageFieldContext 結(jié)構(gòu)中的數(shù)據(jù)對象。 我們還將使用 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 會比之前的模塊更復(fù)雜惧笛。在渲染模板的第一部分,如果有 previewUrl 值逞泄,我們將嘗試將 previewUrl 顯示為圖像患整。

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

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

最后一個輸入字段指示用戶是否要刪除上傳的圖像斟览。

/// 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)入該部分之前苛茂,我們將再定義一個輔助對象已烤,它將作為抽象表單字段的輸入類型。

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

/// 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 時使用 FormImageInput 作為輸入值躁绸,使用 ImageFieldTemplate 作為輸出類型裕循。 我們將使用一個公共 imageKey 變量來存儲當(dāng)前密鑰,并使其也可供其他人訪問净刮。 path 變量將是圖像鍵的前綴剥哑,它只是我們保存上傳文件的目錄路徑。

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

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

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

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

作為最后一步士败,我們可以使用當(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)在可以安全地將上傳的文件移動到最終目的地。首先重慢,我們必須檢查是否有刪除操作饥臂,如果我們必須執(zhí)行此操作,我們只需根據(jù)原始密鑰刪除文件似踱。

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

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

我們將最終密鑰存儲在 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 }

類似上面這樣簡單的代碼,我們就可以使用ImageField完成圖片上傳假勿。

最后

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末晕换,一起剝皮案震驚了整個濱河市午乓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌闸准,老刑警劉巖益愈,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異恕汇,居然都是意外死亡腕唧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門瘾英,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枣接,“玉大人,你說我怎么就攤上這事缺谴〉蹋” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵湿蛔,是天一觀的道長膀曾。 經(jīng)常有香客問我,道長阳啥,這世上最難降的妖魔是什么添谊? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮察迟,結(jié)果婚禮上斩狱,老公的妹妹穿的比我還像新娘。我一直安慰自己扎瓶,他們只是感情好所踊,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著概荷,像睡著了一般秕岛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天继薛,我揣著相機(jī)與錄音修壕,去河邊找鬼。 笑死惋增,一個胖子當(dāng)著我的面吹牛叠殷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播诈皿,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼林束,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了稽亏?” 一聲冷哼從身側(cè)響起壶冒,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎截歉,沒想到半個月后胖腾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘪松,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年咸作,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宵睦。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡记罚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出壳嚎,到底是詐尸還是另有隱情桐智,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布烟馅,位于F島的核電站说庭,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏郑趁。R本人自食惡果不足惜刊驴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寡润。 院中可真熱鬧缺脉,春花似錦、人聲如沸悦穿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栗柒。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瞬沦,已是汗流浹背太伊。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逛钻,地道東北人僚焦。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像曙痘,于是被迫代替她去往敵國和親芳悲。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容