Plot Components:使用Swift构建HTML页面的新方法

200 阅读5分钟

今天,我对我的Swift静态网站生成工具套件进行了一次巨大的更新--特别是Plot的一个全新版本,这个库被用来生成本网站的所有HTML,它增加了一个新的API,用于以非常类似SwiftUI的方式构建HTML组件。

这个新版本已经酝酿了一年多,并且已经在生产中进行了适当的测试。事实上,它被用来渲染你现在正在阅读的这篇文章的HTML!因此,我很高兴现在终于为整个Swift社区公开提供了它。

从节点到组件

到目前为止,Plot一直在使用一个API,它将HTML页面中的所有元素和属性表示为节点,然后可以以各种方式进行组合。

例如,这里是如何将一个BlogPost 的模型阵列变成一个基于<ul> 元素的博客文章的feed:

func makeBlogFeed(containing posts: [BlogPost]) -> Node<HTML.BodyContext> {
    .ul(
        .class("blog-feed"),
        .forEach(posts) { post in
            .li(.article(
                .img(.src(post.imageURL)),
                .h1(.text(post.title)),
                .p(.text(post.description)),
                .a("Continue reading", .href(post.url))
            ))
        }
    )
}

我仍然对上面使用的API的设计感到非常满意,因为它在很多方面与HTML和基于XML的文档的分层性质完美匹配--但与此同时,我也越来越好奇,想看看这个API的 "SwiftUI改造 "会是什么样子。

现在,我的意思并不是真的使用SwiftUI本身来渲染HTML。因为它是一个闭源项目,是专门为在苹果平台上构建原生视图而开发的,对于第三方开发者(比如我)来说,真的没有合理的方法来使用它来渲染任意的HTML字符串。

然而,就像我们在"为SwiftUI的API提供动力的Swift 5.1功能 "等文章中看到的那样SwiftUI提供的公共API都是使用官方语言功能实现的(从Swift 5.4开始,*结果构建器*终于成为语言的一个适当部分)。因此,我没有试图将SwiftUI变成一个HTML渲染器,而是建立了我自己的受SwiftUI启发的API,它是为构建HTML组件的任务量身定做的。

如果我们使用新的基于组件的API来实现它,那么我们之前的博客feed就是这个样子:

struct BlogFeed: Component {
    var posts: [BlogPost]

    var body: Component {
        List(posts) { post in
            Article {
                Image(post.imageURL)
                H1(post.title)
                Paragraph(post.description)
                Link("Continue reading", url: post.url)
            }
        }
        .class("blog-feed")
    }
}

尽管我们现在使用了一种完全不同的语法来渲染我们的HTML,但最终的结果实际上与我们之前的实现是一样的。但真正酷的是,我们不再需要手动构建元素,如用于渲染我们的列表的<ul><li> 标签 - 我们现在可以简单地创建一个List 组件,Plot 将为我们处理所有这些细节。

将SwiftUI的一些核心概念适用于网络

不过,非常重要的一点是,这个项目的目标并不是简单地直接复制SwiftUI的API。毕竟,构建静态生成的网站与原生应用程序的开发有着本质的区别,所以我希望这个新的 API 能够让了解 SwiftUI 的开发者感到熟悉,同时我也希望它能够在 HTML 的背景下感到自如。

这方面的一个具体例子是 Plot 的环境 API,它和SwiftUI 提供的API 一样,可以让你通过向正在渲染的整体环境中输入数值来修改某些组件的行为。

从表面上看,该API的工作方式与你在SwiftUI视图层次结构中应用fontforegroundColor 等修改器的方式完全相同--你可以将这些修改器应用于一个给定的组件,然后它们会自动转发给该组件的子代。

例如,这里我们可以将某种列表样式和链接目标应用于出现在给定层次结构中的每个List/Link

struct ExternalLinks: Component {
    var body: Component {
        Div {
            H2("External Links")
            List {
                Link("My apps on the App Store", url: "...")
                Link("Twitter", url: "...")
                Link("GitHub", url: "...")
            }
        }
        .class("external-links")
        .listStyle(.ordered)
        .linkTarget(.blank)
    }
}

上述做法的结果是,我们的List 将被渲染成有序的(使用<ol> 元素),而为我们的Link 组件生成的每个<a> 元素将被赋予属性target="_blank" ,这将使它们的URL在新标签中打开。

但如果我们现在更深入地研究这个功能,看看我们如何定义我们自己的环境值和键,我们可以看到,与SwiftUI的环境API的设计方式相比,我做了一些不同的决定。

作为一个例子,让我们看看这个简化版的Menu 组件,我用它来渲染这个网站的主菜单--它使用Plot的环境API来检索当前选择的部分:

// In Plot, environment keys are defined by extending a concrete
// EnvironmentKey type, rather than by conforming to a protocol:
extension EnvironmentKey where Value == SwiftBySundell.SectionID? {
    static var selectedSectionID: Self { Self() }
}

struct Menu: Component {
    // Environment values can be retrieved using the EnvironmentValue
    // property wrapper (which is the Plot equivalent of SwiftUI's
    // Environment wrapper):
    @EnvironmentValue(.selectedSectionID) private var selectedSectionID

    var body: Component {
        Navigation {
            List(SwiftBySundell.SectionID.allCases) { sectionID in
                Link(
                    sectionID.menuTitle,
                    url: sectionID.url
                )
                .class(classForSection(withID: sectionID))
            }
        }
    }

    private func classForSection(
        withID id: SwiftBySundell.SectionID
    ) -> String {
        // We can now use our environment value to make decisions
        // within this component — in this case in order to determine
        // whether a given menu item should be marked as selected:
        id == selectedSectionID ? "selected" : ""
    }
}

因此,虽然SwiftUI提供了几种不同的方式来与它的环境API进行交互(通过EnvironmentEnvironmentObject 属性包装器,EnvironmentKey 协议,等等),我选择了实现一个简化的版本,它仍然提供了足够的功能,在静态网站生成的背景下是非常有用的。毕竟,对我来说,实现对可观察对象这样的东西的支持是没有多大意义的,因为(与SwiftUI不同)Plot不会生成任何需要根据状态变化而更新的动态视图。

结论

所以,从本质上讲,这个新版本的Plot是我对一个类似于SwiftUI的API的看法,它是专门用来生成静态HTML的。它并不是要取代动态的客户端Web框架(如React或Vue.js),也没有提供任何API来为其组件应用自定义样式(这仍然需要使用CSS来完成)。

在未来,我确实希望能够继续扩展Plot以支持CSS生成,但这是另一个项目。同时,我非常高兴能与你和其他Swift社区的人分享这个新的基于组件的API。我希望你会发现它很有用,就像我们的苹果朋友总是喜欢说的那样--我迫不及待地想看看你会用它来做什么!"。

谢谢你的阅读!