Elementary:纯 Swift 中的 HTML 模板

一个现代且高效的 HTML 渲染库 - 灵感来源于 SwiftUI,专为 Web 构建。

示例 | 动机 | 讨论

struct MainPage: HTMLDocument {
    var title = "Elementary"

    var head: some HTML {
        meta(.name(.description), .content("Typesafe HTML in modern Swift"))
    }

    var body: some HTML {
        main {
            h1 { "Features" }

            FeatureList(features: [
                "HTML in pure Swift",
                "SwiftUI-inspired composition",
                "Lightweight and fast",
                "Framework agnostic and unopinionated",
            ])

            a(.href("https://github.com/sliemeobn/elementary"), .class("fancy-style")) {
                "Learn more"
            }
        }
    }
}

struct FeatureList: HTML {
    var features: [String]

    var content: some HTML {
        ul {
            for feature in features {
                li { feature }
            }
        }
    }
}

使用它

将依赖项添加到 Package.swift

.package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2")
.product(name: "Elementary", package: "elementary")

Hummingbird 集成

.package(url: "https://github.com/hummingbird-community/hummingbird-elementary.git", from: "0.3.0")
.product(name: "HummingbirdElementary", package: "hummingbird-elementary")

Vapor 集成

.package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0")
.product(name: "VaporElementary", package: "vapor-elementary")

试用一下

查看 Hummingbird + Tailwind 示例应用

有关 ElementaryHTMX 的演示,请参阅此 Hummingbird + HTMX 演示

有关 Vapor 示例,请参阅 Vapor + HTMX 演示

轻量且快速

Elementary 直接渲染为文本,针对从 HummingbirdVapor 服务器应用提供生成的 HTML 进行了优化。

任何符合 HTML 类型的类型都可以单独渲染,非常适合测试或使用 htmx 发送片段。

默认的渲染机制生成 HTML 块以实现高效的响应流式传输,因此浏览器可以在服务器仍在生成其余部分时开始加载页面。 Swift 并发用于处理背压,因此即使对于大型页面,您的内存占用量也保持较低。

// Stream HTML, optimized for responsiveness and back pressure-aware
try await MainPage().render(into: responseStreamWriter)

或者,您可以简单地将渲染的 HTML 收集到一个字符串中。

let html: String = div(.class("pretty")) { "Hello" }.render()
// <div class="pretty">Hello</div>

let fragment: String = FeatureList(features: ["Anything conforming to HTML can be rendered"]).render()
// <ul><li>Anything conforming to HTML can be rendered</li></ul>

// For testing purposes, there is also a formatted version
print(
    div {
        p(.class("greeting")) { "Hi mom!" }
        p { "Look how pretty." }
    }.renderFormatted()
)

// <div>
//   <p class="greeting">Hi mom!</p>
//   <p>Look how pretty.</p>
// </div>

Elementary 零依赖(甚至不依赖 Foundation),并且不使用运行时反射或存在类型容器(代码库中没有一个 any)。

按照设计,它不附带布局引擎、响应式状态跟踪或内置 CSS 样式:它只渲染 HTML。

简洁且可组合

使用受 SwiftUI 启发的组合 API 构建您的 HTML 结构。

struct List: HTML {
    var items: [String]
    var importantIndex: Int

    var content: some HTML {
        // conditional rendering
        if items.isEmpty {
            p { "No items" }
        } else {
            ul {
                // list rendering
                for (index, item) in items.enumerated() {
                    // seamless composition of elements
                    ListItem(text: item, isImportant: index == importantIndex)
                }
            }
        }
    }
}

struct ListItem: HTML {
    var text: String
    var isImportant: Bool = false

    var content: some HTML {
        // conditional attributes
        li { text }
            .attributes(.class("important"), when: isImportant)
    }
}

一流的属性处理

Elementary 利用 Swift 强大的泛型来提供一个属性系统,该系统知道什么属性应该放在哪里。每个元素都知道它用于哪个标签。

与 HTML 中一样,属性紧跟在“开始标签”之后。

// staying close to HTML syntax really helps
div(.data("hello", value: "there")) {
    a(.href("/swift"), .target(.blank)) {
        img(.src("/swift.png"))
        span(.class("fancy")) { "Click Me" }
    }
}

属性也可以通过使用修饰符语法来更改,这允许轻松处理条件属性。

div {
    p { "Hello" }
        .attributes(.id("maybe-fancy"))
        .attributes(.class("fancy"), when: isFancy)
}

通过公开 content 的标签类型,属性将向下传递并被正确应用。

struct Button: HTML {
    var text: String

    // by exposing the HTMLTag type information...
    var content: some HTML<HTMLTag.input> {
        input(.type(.button), .value(text))
    }
}

div {
    // ... Button will know it really is an <input> element ...
    Button(text: "Hello")
        .attributes(.autofocus) // ... and pass on any attributes
}

作为合理的默认设置,classstyle 属性会被合并(分别使用空格或分号)。所有其他属性默认情况下会被覆盖。

无缝异步支持

Elementary 在 HTML 内容中支持 Swift 并发。只需在您的 HTML 中 await 某些内容,同时第一个字节已经飞向浏览器。

div {
    let text = await getMyData()
    p { "This totally works: \(text)" }
    MyComponent()
}

struct MyComponent: HTML {
    var content: some HTML {
        AsyncContent {
            "So does this: \(await getMoreData())"
        }
    }
}

通过使用 AsyncForEach 元素,任何 AsyncSequence 都可以高效地直接渲染为 HTML。

ul {
    // the full result never needs to be stored in memory...
    let users = try await db.users.findAll()
    // ...as each async sequence element...
    AsyncForEach(users) { user in
        // ...is immediately streamed out as HTML
        li { "\(user.name) \(user.favoriteProgrammingLanguage)" }
    }
}

环境变量值

Elementary 利用 TaskLocal 来提供轻量级的环境系统。

enum MyValues {
    // task-locals act as keys, ...
    @TaskLocal static var userName = "Anonymous"
}

struct MyComponent: HTML {
    // ... their values can be accessed ...
    @Environment(MyValues.$userName) var userName

    var content: some HTML {
        p { "Hello, \(userName)!" }
    }
}

div {
    // ... and provided in a familiar way
    MyComponent()
        .environment(Values.$userName, "Drax the Destroyer")
}

🚧 正在进行中 🚧

内置属性列表远未完成,但添加它们非常简单(也可以在外部包中完成)。

欢迎提交 PR,添加模型中缺少的其他属性。

动机和其他包

PlotHTMLKitSwim 都是用于执行类似操作的优秀包。

我创建 Elementary 的主要动机是创造类似于这些包的体验(Swift 论坛帖子 了解更多背景信息),但是

Tokamak 是一个很棒的项目,非常鼓舞人心。它可以生成 HTML,但它的主要重点是一个非常不同的领域。去看看!

swift-htmlswift-dom 也能很好地生成 HTML,但它们使用不同的语法来组合 HTML 元素。

未来方向