SwiftHtml
An awesome Swift HTML DSL library using result builders.
首先我们来认识一下SwiftHtml,因为之后会使用到SwiftHtml 这个库去搭建html,所以这里先介绍一个这个库的功能和使用。下面先看一段官方的demo代码:
import SwiftHtml
let doc = Document(.html) {
Html {
Head {
Title("Hello Swift HTML DSL")
Meta().charset("utf-8")
Meta().name(.viewport).content("width=device-width, initial-scale=1")
Link(rel: .stylesheet).href("./css/style.css")
}
Body {
Main {
Div {
Section {
Img(src: "./images/swift.png", alt: "Swift Logo")
.title("Picture of the Swift Logo")
H1("Lorem ipsum")
.class("red")
P("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
.class(["green", "blue"])
.spellcheck(false)
}
A("Download SwiftHtml now!")
.href("https://github.com/binarybirds/swift-html/")
.target(.blank)
.download()
Abbr("WTFPL")
.title("Do What The Fuck You Want To Public License")
}
}
.class("container")
Script().src("./js/main.js").async()
}
}
}
let html = DocumentRenderer(minify: false, indent: 2).render(doc)
print(html)
如果使用过或熟悉html 的话,应该能看明白上面的demo,其实就是通过swift 的 DSL 代码来代替直接写html
文件,好处就是写起来比较省事,不用写开始和结束标签,同时会因为使用了DSL,编译器可以在构建时对所有内容进行类型检查,这样就可以 100% 确保我们的 HTML 代码不会出现语法问题。
添加SwiftHtml依赖
跟上一节一样,我们通过Swift package manager
添加SwiftHtml
库。
- 打开
Package.swift
文件, 在dependencies
添加:
.package(url: "https://github.com/binarybirds/swift-html", from: "1.2.0")
- 在
App
target下添加:
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "SwiftSvg", package: "swift-html")
添加完后就可以cmd + s
保存,等待Xcode 拉取完成就可以了。
但是在这里我遇到了Xcode 报错: SwiftHtml
The repository could not be found.
的问题,看起来是访问SwiftHtml
的git仓库失败,但是我在浏览器是能打开SwiftHtml
的github 网页的。查了一下,发现是Xcode
设置不了代理,就算是设置全局代理
也没有效果。这也解答了我心里的一个疑虑,就是Xcode
自动更新 Swift page Manager
会比通过命令行(命令行有设置代理)更新 Swift page Manager
慢很多。
要解决这个问题我找到有2个方法:
方法1
去到 DerivedData
对应项目的文件夹里面删除对应项目文件,然后回到Xcode
重新更新SPM
可以设置git的全局代理加速拉取过程。
$ git config --global http.proxy http://127.0.0.1:7890
$ git config --global https.proxy http://127.0.0.1:7890
使用完记得取消全局代理,防止拉取私有库出错:
$ git config --global --unset http.proxy
$ git config --global --unset https.proxy
不过我尝试配置感觉拉取速度没提升。。。
方法2
首先要配置好终端代理,然后终端cd
到项目目录,最后输入命令更新 swift package
$ swift package resolve
等待处理完后,可以看到项目目录.build
已经有对应的依赖了。
然后去到
Xcode
项目的DerivedData
文件夹(做iOS开发的同学应该很熟悉这个文件夹),找到对应的当前项目,进入项目可以看到一个SourcePackages
文件夹,这时候将之前.build
目录下的文件全部copy到这个文件下面。这样就手动地更新了Xcode
管理的 swift package
仓库了。这个替换我理解的原理就是,我们通过终端输入的swift package resolve
处理的依赖仓库会在.build
目录里面,但是Xcode
管理的依赖在DerivedData
的SourcePackages
里面。所以Xcode
拉取失败的话,可以通过手动方式替换。
如果上面两个方案都一直拉取失败的话,可以 clean
一下项目,移除对应的DerivedData
文件让Xcode
重新拉取依赖
使用
install
完成后,我们开始敲代码吧。切换到 configure.swift
文件,在vapor框架中, configure.swift
文件负责注册诸如路由、数据库、模型、设置中间件等。
/// FILE: Sources/App/configure.swift import Vapor
// configures your application
public func configure(_ app: Application) throws {
// uncomment to serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.routes.get("lqbk") { req -> Response in
let doc = Document(.html) {
Html {
Head {
Title("Hello, World!")
}
Body {
H1("i am here!")
Text("start with vapor")
}
}
}
let body = DocumentRenderer(minify: false, indent: 4).render(doc)
return Response(status: .ok, headers: ["content-type": "text/html"], body: .init(string: body))
}
// register routes
try routes(app)
}
上面的代码主要是向应用路由注册了lqbk
这个路径,这个路径的响应是通过生成Document
类描述的HTML 页面,然后DocumentRenderer
转化成String类型的Html字符串。最后通过Response
对象返回出去。
可以看到Response
对象需要这里需要3个参数分别是status
headers
body
,分别是http协议的状态码,请求头和请求体。这里可以根据对应情况设置。
现在我们可以Run
起来项目,然后在浏览器输入http://127.0.0.1:8080/lqbk
可以看到浏览器出现下面的样式:
Address already in use 报错
Xcode
停止运行项目时,会自动关闭http
服务器, 但是有时候会失败(我就遇到过一两次),关闭失败的话,再次运行项目出现 Thread 1: Fatal error: Error raised at top level: bind(descriptor:ptr:bytes:): Address already in use (errno: 48)
这个报错的时候,说明之前的服务器未关闭,还占有着默认的地址。这时候可以手动在终端输入:
lsof -i :8080 -sTCP:LISTEN |awk 'NR > 1 {print $2}'|xargs kill -15
这个命令的意思是找到当前使用8080端口的应用然后关闭掉。
当然每次出错都要输入这个命令就太麻烦了,所以我们可以在Xcode
通过Edit Schems
添加到Run
的 Pre actions
中,这样每次Run
项目的时候就会自动地提前关闭当前8080端口的应用一次。
TemplateRenderer
接下来我们定义一个TemplateRenderer
类将html模版转换的流程抽象出来,方便使用。
/// FILE: Sources/App/Template/TemplateRenderer.swift
import Vapor
import SwiftSvg
import SwiftSgml
public protocol TemplateRepresentable {
@TagBuilder
func render(_ req: Request) -> Tag
}
public struct TemplateRenderer {
var req: Request
init(_ req: Request) {
self.req = req
}
public func renderHtml(_ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4) -> Response {
let doc = Document(.html) {
template.render(req)
}
let body = DocumentRenderer(minify: minify, indent: indent).render(doc)
return Response(status: .ok, headers: ["content-type": "text/html"], body: .init(string: body))
}
}
这样模版的渲染流程就隐藏在TemplateRenderer
中了,只需要实现 TemplateRepresentable
协议关注Tag
的拼装。TemplateRenderer
有一个内部的 init 方法,我们不应该直接创建这个结构体,我们可以将扩展 Request 对象以获取渲染器的实例。
extension Request {
var templates: TemplateRenderer { .init(self) }
}
现在,如果我们回到configuration.swift
文件,我们可以创建一个新模板并使用 req.templates
渲染它。
struct MyTemplate: TemplateRepresentable {
let title: String
let text1: String
let text2: String
func render(_ req: Request) -> Tag {
Html {
Head {
Title(title)
}
Body {
H1(text1)
Text(text2)
}
}
}
}
public func configure(_ app: Application) throws {
// ...
app.routes.get("lqbk") { req -> Response in
return req.templates.renderHtml(MyTemplate(title: "Hello, World!", text1: "i am here!", text2: "start with vapor"))
}
// ...
}
可以看到现在我们使用生成MyTemplate
实例去代替之前的代码,这样可以达到复用模版的效果。
模板和上下文
到这里,我们已经学会使用 template renderer
去渲染模版了,现在让我们创建一个可复用的index
模版,这个模版可以作为之后我们使用的到web 页面的基础模版。由于我们将使用模块化方法来管理所有内容,因此我们应该创建一个新的 Modules
文件夹和一个 Web
子目录。
我们将把所有模板放在 Templates/Html
目录中,每个template
都有一个关联的Contexts
对象,我们将把它们存储在 Templates/Contexts
目录中。
/// FILE: Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift
public struct WebIndexTemplate: TemplateRepresentable {
public var context: WebIndexContext
public init(_ context: WebIndexContext) {
self.context = context
}
@TagBuilder
public func render(_ req: Request) -> Tag {
Html {
Head {
Meta()
.charset("utf-8")
Meta()
.name(.viewport)
.content("width=device-width, initial-scale=1")
Link(rel: .shortcutIcon)
.href("/image/favicon.ico")
.type("image/x-icon")
Link(rel: .stylesheet)
.href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css")
Link(rel: .stylesheet)
.href("/css/web.css")
Title(context.title)
}
Body {
Main {
Section {
H1(context.message)
}
.class("wrapper")
}
}
}
.lang("en-US")
}
}
/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift
public struct WebIndexContext {
public let title: String
public let message: String
}
我们实现了一个新的模版WebIndexTemplate
, 作为index 的基础模版,然后通过WebIndexContext
对象去驱动内容。在WebIndexTemplate
使用到外部的样式表Feather CSS
来设置基础组件。关于feather css 可以看这个文档
可以看到设置.href("/image/favicon.ico")
, 其中/image/favicon.ico
是 Public
文件夹里面对应路径的文件,没有的话我们可以创建一个 Public
文件夹
最后一步,回到我们的 router.swift
文件, 使用 req.templates.renderHtml
去渲染我们的新模版试试吧。
// FILE: Sources/App/routes.swift
import Vapor
func routes(_ app: Application) throws {
app.routes.get { req -> Response in
req.templates.renderHtml(WebIndexTemplate.init(WebIndexContext(title: "lqbk.space", message: "Hi there, welcome to my page!")))
}
}
模版层次结构
因为我们打算建立一个多页面网站,拆分模板将是必不可少的。 我们可以创建可重用的部分,你可以稍后在其他模板文件中共享和渲染它们。 在下面的示例中,我们将创建三个单独的页面。
首先,我们必须更新索引模板,因为它将被整个网站重用。
/// FILE: Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift
extension Svg {
static func menuIcon() -> Svg {
Svg {
Line(x1: 3, y1: 12, x2: 21, y2: 12)
Line(x1: 3, y1: 6, x2: 21, y2: 6)
Line(x1: 3, y1: 18, x2: 21, y2: 18)
}
.width(24)
.height(24)
.viewBox(minX: 0, minY: 0, width: 24, height: 24)
.fill("none")
.stroke("currentColor")
.strokeWidth(2)
.strokeLinecap("round")
.strokeLinejoin("round")
}
}
public struct WebIndexTemplate: TemplateRepresentable {
public var context: WebIndexContext
var body: Tag
public init(_ context: WebIndexContext, @TagBuilder _ builder: () -> Tag) {
self.context = context
self.body = builder()
}
@TagBuilder
public func render(_ req: Request) -> Tag {
Html {
Head {
Meta()
.charset("utf-8")
Meta()
.name(.viewport)
.content("width=device-width, initial-scale=1")
Link(rel: .shortcutIcon)
.href("/image/favicon.ico")
.type("image/x-icon")
Link(rel: .stylesheet)
.href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css")
Link(rel: .stylesheet)
.href("/css/web.css")
Title(context.title)
}
Body {
Header {
Div {
A {
Img(src: "/img/logo.png", alt: "Logo")
}
.id("site-logo")
.href("/")
Nav {
Input()
.type(.checkbox)
.id("primary-menu-button")
.name("menu-button")
.class("menu-button")
Label {
Svg.menuIcon()
}.for("primary-menu-button")
Div {
A("Home")
.href("/")
.class("selected", req.url.path == "/")
A("Blog")
.href("/blog/")
.class("selected", req.url.path == "/blog/")
A("About")
.href("#")
.onClick("javascript:about();")
}
.class("menu-items")
}
.id("primary-menu")
}
.id("navigation")
}
Main {
body
}
Footer {
Section {
P {
Text("This site is powered by ")
A("Swift")
.href("https://swift.org")
.target(.blank)
Text(" & ")
A("Vapor")
.href("https://vapor.codes")
.target(.blank)
Text(".")
}
P("lqbk.space © 2020-2022")
}
}
Script()
.type(.javascript)
.src("/js/web.js")
}
}
.lang("en-US")
}
}
这里的一个重要的变化是可以为模板文件传递的一个builder
参数, 是一个 @TagBuilder
的构建器,这个构建器可以为我们创建 body
,这样就能和index 模版组合使用了。
现在我们的WebIndexContext
也不需要message参数了,我们可以删除它。
/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift
public struct WebIndexContext {
public let title: String
}
作为 index 模板的最后一个部分,我们将从 Public/js
目录中加入一些基本的 javascript 文件。现在我们可以提前创建对应的文件夹和一个空的 web.js 文件,之后就会使用到。
HomePage
主页会比较简单,首先我们需要一个 WebHomeContext
struct 来表示想要呈现的数据。
public struct WebHomeContext {
public let title: String
public let message: String
}
接下来我们搭建主页的模版 WebHomeTemplate
。
/// FILE: Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift
import Vapor
import SwiftHtml
import SwiftSgml
public struct WebHomeTemplate: TemplateRepresentable {
var context: WebHomeContext
public init(_ context: WebHomeContext) {
self.context = context
}
@TagBuilder
public func render(_ req: Request) -> Tag {
WebIndexTemplate(.init(title: context.title)) {
Div {
Section {
H1(context.title)
P(context.message)
}
.class("lead")
}
.id("home")
.class("container")
}
.render(req)
}
}
在 WebHomeTemplate
的 render
方法, 我们组合了WebIndexTemplate
模版一起使用。类似这样,我们能将重复使用的模版代码抽出一个独立的模版,然后组合使用它们。
在之前,我们将很多代码直接放在了 configure
or routes
文件里面,这不是一个好的管理代码的方式,所以现在我们应该分模版得管理代码,我们将渲染模版的功能放在WebFrontendController
对象中。
/// FILE: Sources/App/Modules/Web/Controllers/WebFrontendController.swift
import Vapor
struct WebFrontendController {
func homePage(req: Request) throws -> Response {
req.templates.renderHtml(WebHomeTemplate(.init(title: "HomePage", message: "你好,欢迎来到我的主页")))
}
}
接下来我们需要一个专门的Router
去管理页面路由。
/// FILE: Sources/App/Modules/Web/WebRouter.swift
import Vapor
struct WebRouter: RouteCollection {
let frontendController = WebFrontendController()
func boot(routes: RoutesBuilder) throws {
routes.get(use: frontendController.homeView)
}
}
RoutesBuilder
除了使用get
方法,同时也支持其他HTTP 方法。
现在我们可以在 configure
中配置 WebRouter
, 通过 app.routes
去作为 RoutesBuilder
。
/// FILE: Sources/App/configure.swift
import Vapor
public func configure(_ app: Application) throws {
//...
/// setup web routes
let router = WebRouter()
try router.boot(routes: app.routes)
}
运行程序,你可以看到现在由两个模版组合的效果了。
渲染子模版
之前提到了可以在模版中渲染模版,所以我们现在来做这个事情。因为之后会用到很多链接,所以我们需要一个 WebLinkContext
携带 label
与url
。
/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift
public struct WebLinkContext {
public let label: String
public let url: String
}
使用相应的 WebLinkTemplate
我们可以渲染我们的 WebLinkContext
对象,当然我们可以添加更多属性,例如样式类或布尔值来确定链接是否为空白链接,但为了简单起见,我们先从 label
和url
开始。 后续有对应的需求,也可以进行扩展。
/// FILE: Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift
import Vapor
import SwiftHtml
import SwiftSgml
public struct WebLinkTemplate: TemplateRepresentable {
var context: WebLinkContext
init(_ context: WebLinkContext) {
self.context = context
}
@TagBuilder
public func render(_ req: Request) -> Tag {
A(context.label)
.href(context.url)
}
}
我们还应该改变 WebHomeContext
结构,这样我们就可以利用新创建的WebLinkContext
。 我们还将添加一个新的图标属性,以使我们的主页更漂亮一点。
/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift
public struct WebHomeContext {
public let icon: String
public let title: String
public let message: String
public let paragraphs: [String]
public let link: WebLinkContext
}
相应地我们也需要对WebHomeTemplate
做对应的改进。
public struct WebHomeTemplate: TemplateRepresentable {
var context: WebHomeContext
public init(_ context: WebHomeContext) {
self.context = context
}
@TagBuilder
public func render(_ req: Request) -> Tag {
WebIndexTemplate(.init(title: context.title)) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}
.class("lead")
for paragraph in context.paragraphs {
P(paragraph)
}
WebLinkTemplate(context.link).render(req)
}
.id("home")
.class("container")
}
.render(req)
}
}
如你所见,我们可以将 WebLinkTemplate
与 link context
一起使用,并使用模板上的 render 方法返回一个标签。 返回的 Tag 对象就像我们可以手动创建的任何其他标签一样,因此将模板嵌入到另一个模板中是安全的。
请注意,我们仍然可以在模板文件中使用常规的 for 循环(也可以使用 if-else)。 这很棒,因为我们可以遍历段落值并使用 P Tag呈现它们。
struct WebFrontendController {
func homeView(req: Request) throws -> Response {
req.templates.renderHtml(WebHomeTemplate(.init(icon: "👋 ",
title: "i am lqbk",
message: "你好,欢迎来到我的主页",
paragraphs: ["我在学习Vapor框架",
"希望能搭建一个自己喜欢的Blog",
"提升自己的技能"],
link: WebLinkContext(label: "Read my blog →", url: "/blog/"))))
}
}
最后回到WebFrontendController
, 对之前的修改做内容得填充。
Blog List
由于我们正在使用模块化架构构建应用程序,因此我们不能简单地将与博客相关的内容放入 Web
模块中。 Web
模块在我们的案例中有些特殊,因为它为我们提供了呈现网站的主要元素。 它还包含 Index
模板、Web
样式表和 javascript
文件。
我们将创建一个名为 Blog
的新模块。 每个模块都将遵循我们之前创建的相同模式。 这意味着我们将拥有专用的router
和controller
。 在开始之前,我们将创建一个 BlogPost
struct来表示我们的文章。 在 Sources/App/Modules/Blog 目录下新建一个 Swift 文件。
/// FILE: Sources/App/Modules/Blog/BlogPost.swift
import Foundation
public struct BlogPost: Codable {
public let title: String //标题
public let slug: String //标识符
public let image: String //突破路径
public let excerpt: String //摘要
public let date: Date// 日期
public let category: String? //文章分类
public let content: String //文章内容
}
现在我们定义了 BlogPost
的内容,不过因为当前我们还没有数据层去获取存放这些数据,所以当前我们暂时用随机生成的数据去调试页面。 这块逻辑交给 BlogFrontendController
处理。
/// FILE: Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift
import Vapor
struct BlogFrontendController {
var post: [BlogPost] = {
stride(from: 1, to: 9, by: 1).map { index in
BlogPost(title: "Sample post #\(index)",
slug: "sample-post-\(index)",
image: "/img/posts/\(String(format: "%02d", index + 1)).jpg",
excerpt: "Lorem ipsum",
date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))),
category: Bool.random() ? "Sample category" : nil,
content: "Lorem ipsum dolor sit amet.")
}.sorted {
$0.date > $1.date
}
}()
}
BlogFrontendController 负责处理所有在网络上公开的与博客相关的路由。 这就是为什么它被称为前端控制器。 稍后我们将使用相同的逻辑来创建其他类型的内容通道,例如admin
控制器和 API
控制器。
现在对于我们的博客文章页面,我们将需要一个新的 BlogPostsContext
结构,我们可以使用它来呈现页面。
/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift
struct BlogPostsContext {
let icon: String
let title: String
let message: String
let posts: [BlogPost]
}
然后一样的需要一个BlogPostsTemplate
模版来渲染数据
/// FILE: Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift
import Vapor
import SwiftHtml
import SwiftSgml
struct BlogPostsTemplate: TemplateRepresentable {
var context: BlogPostsContext
init(_ context: BlogPostsContext) {
self.context = context
}
@TagBuilder
func render(_ req: Request) -> Tag {
WebIndexTemplate(.init(title: context.title)) {
Div {
Section {
P(context.icon)
H1(context.title)
P(context.message)
}
.class("lead")
Div {
for post in context.posts {
Article {
A {
Img(src: post.image, alt: post.title)
H2(post.title)
P(post.excerpt)
}
.href("/\(post.slug)/")
}
}
}
.class("grid-221")
}
.id("blog")
}
.render(req)
}
}
使用第三方 Feather CSS
框架的好处是我们可以直接使用大部分组件。 例如,我们的列表将是响应式的,因为我们使用的是 grid-221
类。
这意味着网格将在台式机和平板设备上使用 2 列布局,而在移动设备上将使用单列。 当我们在列表中显示帖子时,我们必须调整帖子的标准标题元素,我们将在web.css
文件中添加一个小改动。
/* FILE: Public/css/web.css */
#blog h2 {
margin: 0.5rem 0;
}
现在我们回到 BlogFrontendController
去处理请求后的这个模版的渲染
struct BlogFrontendController {
//...
func blogView(req: Request) throws -> Response {
let ctx = BlogPostsContext(icon: "🔥 ", title: "Blog", message: "Hot news and stories about everything.", posts: posts)
return req.templates.renderHtml(BlogPostsTemplate(ctx))
}
}
同样的Blog
模块也应该有专门的路由类管理页面,我们在对应模块下创建BlogRouter.swift
文件,处理对应的路由跳转。
/// FILE: Sources/App/configure.swift
import Vapor
struct BlogRouter: RouteCollection {
let controller = BlogFrontendController()
func boot(routes: RoutesBuilder) throws {
routes.get("blog", use: controller.blogView)
}
}
最后一步我们回到configure
文件,将BlogRouter
添加到App
中。
// configures your application
public func configure(_ app: Application) throws {
//...
//setup web routes
let routers: [RouteCollection] = [
WebRouter(),
BlogRouter()
]
for router in routers {
try router.boot(routes: app.routes)
}
}
最后运行应用,点击 Blog
tab 查看效果吧。
如果出现 Pubilc
有对应图片,但是图片展示不出来的问题,你需要配置一下 working directory
可以参考:https://theswiftdev.com/custom-working-directory-in-xcode/
Blog 详情页
列表页已经搭建完成了,现在我们将进一步完成详情页的搭建。我们将首先在 Templates 文件夹中创建一个新的 BlogPostTemplate
文件 和 BlogPostContext
文件。
/// FILE: Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift
import Vapor
import SwiftHtml
import SwiftSgml
struct BlogPostTemplate: TemplateRepresentable {
var context: BlogPostContext
init(_ context: BlogPostContext) {
self.context = context
}
var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .short
return formatter
}()
@TagBuilder
func render(_ req: Request) -> Tag {
WebIndexTemplate(.init(title: context.post.title)) {
Div {
Section {
P(dateFormatter.string(from: context.post.date))
H1(context.post.title)
P(context.post.excerpt)
}
.class(["lead", "container"])
Img(src: context.post.image, alt: context.post.title)
Article {
Text(context.post.content)
}
.class("container")
}
.id("post")
}
.render(req)
}
}
/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift
struct BlogPostContext {
let post: BlogPost
}
接着我们回到BlogFrontendController
处理详情页跳转的逻辑,我们通过路径中的 slug
标识去找到对应的BlogPost
数据,如果没有找到就重定向回主页。
struct BlogFrontendController {
//...
func postView(req: Request) throws -> Response {
let slug = req.url.path.trimmingCharacters(in: .init(charactersIn: "/"))
guard let post = posts.first(where: { $0.slug == slug }) else {
return req.redirect(to: "/")
}
return req.templates.renderHtml(BlogPostTemplate(.init(post: post)))
}
}
现在我们剩下的问题就是: 如何修改路由通过url
的path去匹配对应的控制器。
因为每个详情页的路径应该是不同的,所以这里我们可以使用 .anything
匹配。
import Vapor
struct BlogRouter: RouteCollection {
let controller = BlogFrontendController()
func boot(routes: RoutesBuilder) throws {
routes.get("blog", use: controller.blogView)
routes.get(.anything, use: controller.postView)
}
}
现在你可以运行应用,可以点击文章详情去查看内容了。
自定义中间件
现在我们可以使用同一路径的两个版本(例如 /blog/ vs /blog)访问每个 URL,我们将实现一个中间件去统一这2个url 的表现。
/// FILE: Sources/App/Middlewares/ExtendPathMiddleware.swift
import Vapor
struct ExtendPathMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
if !request.url.path.hasSuffix("/"), !request.url.path.contains(".") {
return request.redirect(to: request.url.path + "/", type: .permanent)
}
return try await next.respond(to: request)
}
}
然后在configure
文件配置上这个中间件。
/// FILE: Sources/App/configure.swift
// 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())
//...
}
这样输入没有带"/"后缀的的url时,中间件会自动带上"/"后缀。类似这样我们通过使用中间件做一些串行的处理。因为 Async
和 Await
需要macOS 12才支持,所以需要改一下Package.swift
的platforms
为.macOS(.v12)
最后一个菜单项呢? 让我们使用在教程开始时创建的那个空的 web.js 文件。 我们将简单地显示一个alert,但当然您可以使用此模板通过一些精美的动画来为网站增添趣味。
/* FILE: Public/js/web.js */
function about() {
alert("myPage\n\nversion 1.0.0");
}
总结
这次我们学习到了通过 SwiftHtml 去搭建 Web 页面的模版,模块化得管理代码,了解到路由的基础知识。下一步,我们会开始使用Fluent 实现我们的数据层。