VaporHX 是 Htmx 和其他扩展的集合,是我在个人项目中开始使用 htmx 时创建的(因此可能带有一些个人偏好)。无论如何,欢迎讨论任何更改,我对新的和有说服力的想法持开放态度。
注意:所有 HTMX 部分都完全可用,并且我正在积极使用它们,即使文档不完整。 对此我深感抱歉,但编写文档确实非常繁琐。
核心思想是,您可以以最小的努力将现有的 API 端点与 HTMX 端点结合起来。 响应将取决于请求的 Accept
标头和请求方法的值。
您所需要做的就是在您的 Content
结构体上调用 hx(template: String)
方法并返回其值。 它将自动选择适当的响应,无论是 JSON 编码的数据、完整的 HTML 页面还是 HTMX 片段。 当返回 HTML (HTMX) 时,您的内容将作为上下文注入到指定的模板中。 默认情况下,它使用 Leaf 模板引擎。
但是,如果您不想使用 Leaf 引擎,则不必这样做。 这个包定义了 HXTemplateable
协议,它有一个单独的 render 方法,该方法将 HTML 页面作为字符串返回。 只要您自己的模板引擎实现了它,您就可以将其类型传递给 hx(template:)
方法。
import VHX
// Do NOT forget to call 'configureHtmx' in your 'configure' method before trying this snippet in your project
// Also, do NOT create a folder called '--page' in your template root without changing the default VHX settings
// as it is used as a prefix for dynamically generated wrapper page templates
// and the custom template provider is the last one to be checked after the default ones are run
// Furthermore, this snippet assumes the default leaf naming conventions as they can be manually overriden
struct MyApi: Content {
let name: String
}
func routes(_ app: Application) throws {
// Combined API and HTMX endpoint
// 'api.leaf' template must exist
app.get("api") { req in
MyApi(name: "name").hx(template: "api")
// The return type of this function call is 'HX<MyApi>'
}
// HTMX only endpoint
// 'index.leaf' template must exist
// It will automatically select whether to return a full page or only a fragment if the generic page template was configured.
// Otherwise, it will simply render the 'index.leaf' template
app.get { req in
try await req.htmx.render("index")
}
// Or an even quicker definition of simple 'static' routes
app.get("static", use: staticRoute(template: "my-static-template"))
}
基本配置 (configure.swift
)
import Vapor
import VHX
// Basic config configures Leaf engine but you are not required to use it
// Basic config assumes 'index-base.leaf' template exists and that it contains '#import("body")' tag
// It will generate a dynamic template that wraps the content of the specified template for NON-htmx calls to 'htmx.render'
// It will simply plug the provided template into the 'body' slot of the base template
public func configure(_ app: Application) async throws {
let config = HtmxConfiguration.basic()
try configureHtmx(app, configuration: config)
}
这是我的个人看法:让您的后端代码成为项目的单一事实来源,放弃大部分前端膨胀,转而支持就地无缝更新您的 HTML。 无需重新加载页面,只需您的服务器端 HTML 模板。 在 htmx.org 了解更多信息。
这是官方介绍
- 为什么只有
<a>
和<form>
才能发出 HTTP 请求?- 为什么只有
click
&submit
事件才能触发它们?- 为什么只有
GET
&POST
方法可用?- 为什么您只能替换整个屏幕?
通过删除这些任意约束,htmx 完成了 HTML 作为超文本。
最后,这是 Fireship
对 HTMX 的快速介绍:htmx in 100 seconds。
SPM 安装
.package(url: "https://github.com/RussBaz/VaporHX.git", from: "0.0.26"),
.product(name: "VHX", package: "VaporHX"),
假设在以下所有示例中都标准使用了 `configure.swift`。
使用 Leaf 引擎的最简单配置(没有本地化助手)
import Vapor
import VHX
// Basic config configures Leaf engine but you are not required to use it
// Basic config assumes 'index-base.leaf' template exists and that it contains '#import("body")' tag
// It will generate a dynamic template that wraps the content of the specified template for NON-htmx calls to 'htmx.render'
// It will simply plug the provided template into the 'body' slot of the base template
public func configure(_ app: Application) async throws {
// other configuration
let config = HtmxConfiguration.basic()
try configureHtmx(app, configuration: config)
// more configuration
}
请注意,默认包装器允许动态更改基本模板名称和槽名称。 请参阅 render
函数。
否则,如果您想指定自己的 htmx 页面包装器(在 NON-HTMX 请求上将提供的模板名称插入到动态生成的页面中)
// The most straightforward configuration
import Vapor
import VHX
// Defining the page dynamic template generator separately
// Check the 'HXBasicLeafSource' later in this section for further details
func pageTemplate(_ template: String) -> String {
"""
#extend("index-base"): #export("body"): #extend("\(template)") #endexport #endextend
"""
}
public func configure(_ app: Application) async throws {
// Other configuration
// HTMX configuration also enables leaf templating language
try configureHtmx(app, pageTemplate: pageTemplate)
// Later configuration and routes registration
}
这是所有签名
func configureHtmx(_ app: Application, pageTemplate template: ((_ name: String) -> String)? = nil) throws
// or
func configureHtmx(_ app: Application, configuration: HtmxConfiguration) throws
// -------------------------------------------- //
// This struct stores globally available (through the Application) htmx configuration
struct HtmxConfiguration {
var pageSource: HXLeafSource
// A header name that will be copied back from the request when HXError is thrown
// The header type must be UInt, otherwise 0 is returned
// Should be used by the client when retrying
var errorAttemptCountHeaderName: String?
// Possible ways to init the configuration structure
init()
init(pagePrefix prefix: String)
init(pagePrefix prefix: String = "--page", pageTemplate template: @escaping (_ name: String) -> String)
init(pageSource: HXLeafSource, errorAttemptCountHeaderName: String? = nil)
// Default basic configuration
static func basic(pagePrefix prefix: String = "--page", baseTemplate: String = "index-base", slotName: String = "body") -> Self
}
HXLeafSource
用于生成一个动态模板,当通过普通浏览器请求访问时,该模板用于将 HTMX 片段与页面的其余内容包装在一起。
它满足以下协议
protocol HXLeafSource: LeafSource {
var pagePrefix: String { get }
}
其中 LeafSource
是一个特殊的 Leaf
协议,专用于自定义 Leaf 模板的发现方式。
然后 VaporHX 实现此专用协议的实现。
struct HXBasicLeafSource: HXLeafSource {
let pagePrefix: String
// This is our custom template generator
let pageTemplate: (_ name: String) -> String
}
为了手动初始化此结构体,请使用以下函数
func hxPageLeafSource(prefix: String = "--page", template: ((_ name: String) -> String)?) -> HXLeafSource
func hxBasicPageLeafSource(prefix: String = "--page", baseTemplate: String = "index-base", slotName: String = "body") -> HXLeafSource
在我们的例子中,默认的 pagePrefix
值为 --page
。 因此,每次您向 leaf
请求以 --page/
为前缀的模板(请不要错过前缀后的 /
,始终需要),默认的 HXBasicLeafSource
将返回由 pageTemplate
闭包生成的模板。 前缀后 /
的所有内容将传递到页面模板生成器中,并且此函数的结果应该是作为字符串的有效 leaf
模板。
传递给 pageTemplate
方法的值不能为空。 如果为空,则 HXBasicLeafSource
将返回“未找到”错误。
最后,此 LeafSource
实现注册为最后一个 Leaf 源,这意味着默认搜索路径已完全保留。
如何检查传入的请求是否为 HTMX 请求?
// Check this extensions property on the 'Request' object
req.htmx.prefered // Bool
// And if you want more more accuracy ...
req.htmx.prefers // Preference
// -------------------------------------------- //
// HTMX case implies an HTMX fragment
// HTML case implies standard browser request
// API case implies json api request
enum Preference {
case htmx, html, api
}
如何自动决定是否需要渲染 HTMX 片段或完整页面?
// Try this method on the 'req.htmx' extension
// This method tries to mimic the 'req.view.render' api
// but it also can accept optional HXResponseHeaders
// Setting the 'page' parameter to true will force the server to always return a full page or a page fragment only otherwise
func render(_ name: String, _ context: some Encodable, page: Bool? = nil, headers: HXResponseHeaders? = nil) async throws -> Response
func render(_ name: String, page: Bool? = nil, headers: HXResponseHeaders? = nil) async throws -> Response
// If you are using a custom templating engine, then you should use this method
func render<T: HXTemplateable>(_ template: T.Type, _ context: T.Context, page: Bool? = nil, headers: HXResponseHeaderAddable? = nil) async throws -> Response
此外,如果您使用的是默认模板生成器,则可以手动覆盖基本模板名称和槽名称。 这是一个如何完成的例子
// Use square brackets at the beginning of the name to override default base template and slot names
routes.get("template") { req in
// Just square brackets without colons to override base template name only
try await req.htmx.render("[index-custom]name")
}
routes.get("slot") { req in
// Use the following format to override a slot name: [template:slot]
// Using multiple colons will result in an error
try await req.htmx.render("[index-custom:extra]name")
}
要将 HTMX 特定标头添加到响应中,您可以更新 response.headers
扩展
req.htmx.response.headers // HXResponseHeaders
否则,您可以为任何 render
方法提供标头。 它将覆盖任何先前指定的标头。
要了解有关 HXResponseHeaders
的更多信息,请参阅 响应标头 部分。
如何使用正确的 HTMX 标头快速重定向?
// The simplest type of redirect
func redirect(to location: String, htmx: HXRedirect.Kind = .redirect, html: Redirect = .normal, refresh: Bool = false) async throws -> Response
// A helper that looks for a query parameter by the 'key' value and redirects to it
func autoRedirect(key: String = "next", htmx: HXRedirect.Kind = .redirect, html: Redirect = .normal, refresh: Bool = false) async throws -> Response
// A helper that looks for a query parameter by the 'key' value
// And then redirects to a 'through location' while preserving the query parameter from the first step during the redirect
// e.g. it can redirect from '/redirect?next=/dashboard/' to '/login?next=/dashboard/'
// by making 'through' equal to '/login'
func autoRedirect(through location: String, key: String = "next", htmx: HXRedirect.Kind = .redirect, html: Redirect = .normal, refresh: Bool = false) async throws -> Response
// A helper that redirects to 'from location' while adding the current url as query parameter with a name specified by the 'key'
// It preserves query parameteres from the original url
// e.g from /dashboard/ to /login?next=/dashboard/
// by making 'from' equal to '/login'
func autoRedirectBack(from location: String, key: String = "next", htmx: HXRedirect.Kind = .redirect, html: Redirect = .normal, refresh: Bool = false) async throws -> Response
// -------------------------------------------- //
// HXRedirect.Kind
enum Kind {
case redirect
case redirectAndPush
case redirectAndReplace
}
// What is a 'Redirect' type?
// It is simply the default 'Vapor' type which you use with the 'req.redirect'
内置的 Content
类型已使用 hx()
方法进行了扩展。 这是为标准响应添加自动 HTMX 支持的秘诀。 此外,还扩展了一些其他类型,例如 HTTPStatus
和 Abort
。
这是它的工作方式
// This is a slightly simplified version of this extension method declaration
extension Content where Self: AsyncResponseEncodable & Encodable {
func hx(template name: String? = nil, page: Bool? = nil, headers: HXResponseHeaders? = nil) -> HX<Self>
// For custom templating engines
func hx<T: HXTemplateable>(template: T.Type, page: Bool? = nil, headers: HXResponseHeaders? = nil) -> HX<Self> where T.Context == Self
}
// And this is how you would use it
app.get("api") { req in
// Where 'MyApi' is some Content
MyApi(name: "name").hx(template: "api")
// The return type of this function call is 'HX<MyApi>'
}
通常不应直接处理 HX
结构体,但如果需要,这是其定义
typealias TemplateRenderer = (_ req: Request, _ context: T, _ page: Bool?, _ headers: HXResponseHeaders?) async throws -> Response
struct HX<T: AsyncResponseEncodable & Encodable> {
let context: T
let template: TemplateRenderer?
let page: Bool?
let htmxHeaders: HXResponseHeaders?
}
如果您想使用自己的模板引擎,则每个渲染器都应实现以下协议
protocol HXTemplateable {
associatedtype Context: AsyncResponseEncodable & Encodable
static func render(req: Request, isPage: Bool, context: Context) -> String
}
如果此协议不足以满足您的用例,请打开一个 issue,我们可以在那里讨论。
// 'HTMX' request header getter on every request
req.htmx.headers
// Request header structure
// For the meaning of value of each header, please refer to the 'HTMX' docs
struct HXRequestHeaders {
let boosted: Bool
let currentUrl: String?
let historyRestoreRequest: Bool
let prompt: Bool
let request: Bool
let target: String?
let triggerName: String?
let trigger: String?
}
HTMX
标头定义为具有简单内置验证的结构。 它们可以作为单独的标头添加到 Response
对象中,也可以通过使用 HXResponseHeaders
结构体作为整体集合添加。 后一个结构体可以作为可选参数传递给 htmx
特定函数,例如 .htmx.render
或 .hx
。
// Cleaned up definition
struct HXResponseHeaders {
var location: HXLocationHeader?
var pushUrl: HXPushUrlHeader?
var redirect: HXRedirectHeader?
var refresh: HXRefreshHeader?
var replaceUrl: HXReplaceUrlHeader?
var reselect: HXReselectHeader?
var reswap: HXReswapHeader?
var retarget: HXRetargetHeader?
var trigger: HXTriggerHeader?
var triggerAfterSettle: HXTriggerAfterSettleHeader?
var triggerAfterSwap: HXTriggerAfterSwapHeader?
}
// Example usages
// With 'hx' extension
app.get("api") { req in
let headers = HXResponseHeaders(retarget: HXRetargetHeader("#content"))
return MyApi(name: "name").hx(template: "api", headers: headers)
}
// With 'htmx.render' function
app.get("example") { req in
let headers = HXResponseHeaders(retarget: HXRetargetHeader("#content"))
return req.htmx.render("example", headers: headers)
}
// With 'add' extension method on 'Response'
// Can be used with the whole container ('HXResponseHeaders') or with individual headers (such as 'HXRetargetHeader')
// Later header values will replace earlier headers
app.get("redirect") { req in
req.htmx.autoRedirect(through: "/login", html: .temporary).add(headers: HXResponseHeaders())
}
HXLocationHeader
是 HX-Location
响应标头的类型安全构造函数。 我认为这是最复杂的响应标头。
struct HXLocationHeader {
let location: HXLocationType
}
enum HXLocationType {
case simple(String) // Header value is just a string
case custom(HXCustomLocation) // Header value is json
}
struct HXCustomLocation: Encodable {
let path: String
let target: String?
let source: String?
let event: String?
let handler: String?
let swap: HXReswapHeader?
let values: [String]?
let headers: [String: String]?
}
由于可能选项的数量庞大,因此存在一个“流畅”的 API 来生成具有更新的属性值的新标头结构,例如
func set(target newTarget: String) -> Self
func set(source newSource: String?) -> Self
func set(event newEvent: String?) -> Self
func set(handler newHandler: String?) -> Self
func set(swap newSwap: HXReswapHeader?) -> Self
func set(values newValues: [String]?) -> Self
func set(headers newHeaders: [String: String]?) -> Self
// Removes all other properties and sets the location property to simple
func makeSimple(_ path: String? = nil) -> Self
一些创建简单和更复杂的标头的示例
let header1 = HXLocationHeader("/test") // reuslts in "HX-Location: /test"
let header2 = HXLocationHeader("/test2", target: "#testdiv") // results in "HX-Location: {"path":"/test2", "target":"#testdiv"}"
// and so on
HXPushUrlHeader
是 HX-Push-Url
响应标头的类型安全构造函数。
struct HXPushUrlHeader {
let url: HXPushType
}
enum HXPushType {
case disable
case enable(String) // where the string should contain the new value to be pushed onto the browser location history stack
}
一些例子
let header1 = HXPushUrlHeader(url: .disable) // results in "HX-Push-Url: false"
let header2 = HXPushUrlHeader("/test") // results in "HX-Push-Url: /test"
HXRedirectHeader
是 HX-Redirect
响应标头的类型安全构造函数。 请不要将此响应标头与 HXRedirect
结构混淆。
它非常简单
struct HXRedirectHeader {
let location: String
}
// sample usage
let header = HXRedirectHeader("/test") // results in "HX-Redirect: /test"
未完待续...
对于 HTMX 的新手,此包捆绑了一个 HTMX 演示,您可以在本地运行。 它包含来自 HTMX.org 网站的一些示例。
这是使用 Leaf 模板引擎和 VaporHX 构建的一些 HTMX 功能的展示。 它不是此库所有功能的展示,而是对 HTMX 的一个很好的介绍。 它也不是一个生产就绪的示例。
VHX
切换到 Demo
(顶部网址栏的左侧)https://:8080
swift run Demo