本篇將全部繼續(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 context與input 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ù)對象。 我們還將使用 previewUrl 和 accept 屬性來設(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 界面。