SwiftHtml
An awesome Swift HTML DSL library using result builders.
首先我們來認(rèn)識(shí)一下SwiftHtml庐氮,因?yàn)橹髸?huì)使用到SwiftHtml 這個(gè)庫去搭建html,所以這里先介紹一個(gè)這個(gè)庫的功能和使用业簿。下面先看一段官方的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 的話,應(yīng)該能看明白上面的demo批狱,其實(shí)就是通過swift 的 DSL 代碼來代替直接寫html
文件裸准,好處就是寫起來比較省事,不用寫開始和結(jié)束標(biāo)簽赔硫,同時(shí)會(huì)因?yàn)槭褂昧薉SL炒俱,編譯器可以在構(gòu)建時(shí)對(duì)所有內(nèi)容進(jìn)行類型檢查,這樣就可以 100% 確保我們的 HTML 代碼不會(huì)出現(xiàn)語法問題爪膊。
添加SwiftHtml依賴
跟上一節(jié)一樣权悟,我們通過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 報(bào)錯(cuò): SwiftHtml
The repository could not be found.
的問題,看起來是訪問SwiftHtml
的git倉庫失敗耘成,但是我在瀏覽器是能打開SwiftHtml
的github 網(wǎng)頁的拇派。查了一下,發(fā)現(xiàn)是Xcode
設(shè)置不了代理凿跳,就算是設(shè)置全局代理
也沒有效果件豌。這也解答了我心里的一個(gè)疑慮,就是Xcode
自動(dòng)更新 Swift page Manager
會(huì)比通過命令行(命令行有設(shè)置代理)更新 Swift page Manager
慢很多控嗜。
要解決這個(gè)問題我找到有2個(gè)方法:
方法1
去到 DerivedData
對(duì)應(yīng)項(xiàng)目的文件夾里面刪除對(duì)應(yīng)項(xiàng)目文件茧彤,然后回到Xcode
重新更新SPM
可以設(shè)置git的全局代理加速拉取過程。
$ git config --global http.proxy http://127.0.0.1:7890
$ git config --global https.proxy http://127.0.0.1:7890
使用完記得取消全局代理疆栏,防止拉取私有庫出錯(cuò):
$ git config --global --unset http.proxy
$ git config --global --unset https.proxy
不過我嘗試配置感覺拉取速度沒提升曾掂。。壁顶。
方法2
首先要配置好終端代理珠洗,然后終端cd
到項(xiàng)目目錄,最后輸入命令更新 swift package
$ swift package resolve
等待處理完后若专,可以看到項(xiàng)目目錄.build
已經(jīng)有對(duì)應(yīng)的依賴了许蓖。
然后去到
Xcode
項(xiàng)目的DerivedData
文件夾(做iOS開發(fā)的同學(xué)應(yīng)該很熟悉這個(gè)文件夾),找到對(duì)應(yīng)的當(dāng)前項(xiàng)目调衰,進(jìn)入項(xiàng)目可以看到一個(gè)SourcePackages
文件夾膊爪,這時(shí)候?qū)⒅?code>.build目錄下的文件全部copy到這個(gè)文件下面。這樣就手動(dòng)地更新了Xcode
管理的 swift package
倉庫了嚎莉。這個(gè)替換我理解的原理就是米酬,我們通過終端輸入的swift package resolve
處理的依賴倉庫會(huì)在.build
目錄里面,但是Xcode
管理的依賴在DerivedData
的SourcePackages
里面趋箩。所以Xcode
拉取失敗的話赃额,可以通過手動(dòng)方式替換加派。
如果上面兩個(gè)方案都一直拉取失敗的話,可以 clean
一下項(xiàng)目跳芳,移除對(duì)應(yīng)的DerivedData
文件讓Xcode
重新拉取依賴
使用
install
完成后芍锦,我們開始敲代碼吧。切換到 configure.swift
文件筛严,在vapor框架中醉旦, configure.swift
文件負(fù)責(zé)注冊(cè)諸如路由饶米、數(shù)據(jù)庫桨啃、模型、設(shè)置中間件等檬输。
/// 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)
}
上面的代碼主要是向應(yīng)用路由注冊(cè)了lqbk
這個(gè)路徑照瘾,這個(gè)路徑的響應(yīng)是通過生成Document
類描述的HTML 頁面,然后DocumentRenderer
轉(zhuǎn)化成String類型的Html字符串丧慈。最后通過Response
對(duì)象返回出去析命。
可以看到Response
對(duì)象需要這里需要3個(gè)參數(shù)分別是status
headers
body
,分別是http協(xié)議的狀態(tài)碼,請(qǐng)求頭和請(qǐng)求體逃默。這里可以根據(jù)對(duì)應(yīng)情況設(shè)置鹃愤。
現(xiàn)在我們可以Run
起來項(xiàng)目,然后在瀏覽器輸入http://127.0.0.1:8080/lqbk
可以看到瀏覽器出現(xiàn)下面的樣式:
Address already in use 報(bào)錯(cuò)
Xcode
停止運(yùn)行項(xiàng)目時(shí)完域,會(huì)自動(dòng)關(guān)閉http
服務(wù)器, 但是有時(shí)候會(huì)失斎硗隆(我就遇到過一兩次),關(guān)閉失敗的話吟税,再次運(yùn)行項(xiàng)目出現(xiàn) Thread 1: Fatal error: Error raised at top level: bind(descriptor:ptr:bytes:): Address already in use (errno: 48)
這個(gè)報(bào)錯(cuò)的時(shí)候凹耙,說明之前的服務(wù)器未關(guān)閉,還占有著默認(rèn)的地址肠仪。這時(shí)候可以手動(dòng)在終端輸入:
lsof -i :8080 -sTCP:LISTEN |awk 'NR > 1 {print $2}'|xargs kill -15
這個(gè)命令的意思是找到當(dāng)前使用8080端口的應(yīng)用然后關(guān)閉掉肖抱。
當(dāng)然每次出錯(cuò)都要輸入這個(gè)命令就太麻煩了,所以我們可以在Xcode
通過Edit Schems
添加到Run
的 Pre actions
中异旧,這樣每次Run
項(xiàng)目的時(shí)候就會(huì)自動(dòng)地提前關(guān)閉當(dāng)前8080端口的應(yīng)用一次意述。
TemplateRenderer
接下來我們定義一個(gè)TemplateRenderer
類將html模版轉(zhuǎn)換的流程抽象出來,方便使用吮蛹。
/// 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
中了欲险,只需要實(shí)現(xiàn) TemplateRepresentable
協(xié)議關(guān)注Tag
的拼裝。TemplateRenderer
有一個(gè)內(nèi)部的 init 方法匹涮,我們不應(yīng)該直接創(chuàng)建這個(gè)結(jié)構(gòu)體天试,我們可以將擴(kuò)展 Request 對(duì)象以獲取渲染器的實(shí)例。
extension Request {
var templates: TemplateRenderer { .init(self) }
}
現(xiàn)在然低,如果我們回到configuration.swift
文件喜每,我們可以創(chuàng)建一個(gè)新模板并使用 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"))
}
// ...
}
可以看到現(xiàn)在我們使用生成MyTemplate
實(shí)例去代替之前的代碼,這樣可以達(dá)到復(fù)用模版的效果带兜。
模板和上下文
到這里枫笛,我們已經(jīng)學(xué)會(huì)使用 template renderer
去渲染模版了,現(xiàn)在讓我們創(chuàng)建一個(gè)可復(fù)用的index
模版刚照,這個(gè)模版可以作為之后我們使用的到web 頁面的基礎(chǔ)模版刑巧。由于我們將使用模塊化方法來管理所有內(nèi)容,因此我們應(yīng)該創(chuàng)建一個(gè)新的 Modules
文件夾和一個(gè) Web
子目錄无畔。
我們將把所有模板放在 Templates/Html
目錄中啊楚,每個(gè)template
都有一個(gè)關(guān)聯(lián)的Contexts
對(duì)象,我們將把它們存儲(chǔ)在 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
}
我們實(shí)現(xiàn)了一個(gè)新的模版WebIndexTemplate
, 作為index 的基礎(chǔ)模版恭理,然后通過WebIndexContext
對(duì)象去驅(qū)動(dòng)內(nèi)容。在WebIndexTemplate
使用到外部的樣式表Feather CSS
來設(shè)置基礎(chǔ)組件郭变。關(guān)于feather css 可以看這個(gè)文檔
可以看到設(shè)置.href("/image/favicon.ico")
, 其中/image/favicon.ico
是 Public
文件夾里面對(duì)應(yīng)路徑的文件颜价,沒有的話我們可以創(chuàng)建一個(gè) 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!")))
}
}
模版層次結(jié)構(gòu)
因?yàn)槲覀兇蛩憬⒁粋€(gè)多頁面網(wǎng)站周伦,拆分模板將是必不可少的。 我們可以創(chuàng)建可重用的部分未荒,你可以稍后在其他模板文件中共享和渲染它們专挪。 在下面的示例中,我們將創(chuàng)建三個(gè)單獨(dú)的頁面茄猫。
首先狈蚤,我們必須更新索引模板,因?yàn)樗鼘⒈徽麄€(gè)網(wǎng)站重用划纽。
/// 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")
}
}
這里的一個(gè)重要的變化是可以為模板文件傳遞的一個(gè)builder
參數(shù), 是一個(gè) @TagBuilder
的構(gòu)建器脆侮,這個(gè)構(gòu)建器可以為我們創(chuàng)建 body
,這樣就能和index 模版組合使用了勇劣。
現(xiàn)在我們的WebIndexContext
也不需要message參數(shù)了靖避,我們可以刪除它。
/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift
public struct WebIndexContext {
public let title: String
}
作為 index 模板的最后一個(gè)部分比默,我們將從 Public/js
目錄中加入一些基本的 javascript 文件』媚螅現(xiàn)在我們可以提前創(chuàng)建對(duì)應(yīng)的文件夾和一個(gè)空的 web.js 文件,之后就會(huì)使用到命咐。
HomePage
主頁會(huì)比較簡單篡九,首先我們需要一個(gè) WebHomeContext
struct 來表示想要呈現(xiàn)的數(shù)據(jù)。
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
模版一起使用榛臼。類似這樣伊佃,我們能將重復(fù)使用的模版代碼抽出一個(gè)獨(dú)立的模版,然后組合使用它們沛善。
在之前航揉,我們將很多代碼直接放在了 configure
or routes
文件里面,這不是一個(gè)好的管理代碼的方式金刁,所以現(xiàn)在我們應(yīng)該分模版得管理代碼帅涂,我們將渲染模版的功能放在WebFrontendController
對(duì)象中。
/// 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: "你好尤蛮,歡迎來到我的主頁")))
}
}
接下來我們需要一個(gè)專門的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
方法,同時(shí)也支持其他HTTP 方法抵屿。
現(xiàn)在我們可以在 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)
}
運(yùn)行程序捅位,你可以看到現(xiàn)在由兩個(gè)模版組合的效果了轧葛。
渲染子模版
之前提到了可以在模版中渲染模版,所以我們現(xiàn)在來做這個(gè)事情艇搀。因?yàn)橹髸?huì)用到很多鏈接尿扯,所以我們需要一個(gè) WebLinkContext
攜帶 label
與url
。
/// FILE: Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift
public struct WebLinkContext {
public let label: String
public let url: String
}
使用相應(yīng)的 WebLinkTemplate
我們可以渲染我們的 WebLinkContext
對(duì)象焰雕,當(dāng)然我們可以添加更多屬性衷笋,例如樣式類或布爾值來確定鏈接是否為空白鏈接,但為了簡單起見矩屁,我們先從 label
和url
開始辟宗。 后續(xù)有對(duì)應(yīng)的需求,也可以進(jìn)行擴(kuò)展吝秕。
/// 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)
}
}
我們還應(yīng)該改變 WebHomeContext
結(jié)構(gòu)泊脐,這樣我們就可以利用新創(chuàng)建的WebLinkContext
。 我們還將添加一個(gè)新的圖標(biāo)屬性烁峭,以使我們的主頁更漂亮一點(diǎn)容客。
/// 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
}
相應(yīng)地我們也需要對(duì)WebHomeTemplate
做對(duì)應(yīng)的改進(jìn)。
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 方法返回一個(gè)標(biāo)簽。 返回的 Tag 對(duì)象就像我們可以手動(dòng)創(chuàng)建的任何其他標(biāo)簽一樣鬓梅,因此將模板嵌入到另一個(gè)模板中是安全的供置。
請(qǐng)注意,我們?nèi)匀豢梢栽谀0逦募惺褂贸R?guī)的 for 循環(huán)(也可以使用 if-else)绽快。 這很棒芥丧,因?yàn)槲覀兛梢员闅v段落值并使用 P Tag呈現(xiàn)它們悲关。
struct WebFrontendController {
func homeView(req: Request) throws -> Response {
req.templates.renderHtml(WebHomeTemplate(.init(icon: "?? ",
title: "i am lqbk",
message: "你好,歡迎來到我的主頁",
paragraphs: ["我在學(xué)習(xí)Vapor框架",
"希望能搭建一個(gè)自己喜歡的Blog",
"提升自己的技能"],
link: WebLinkContext(label: "Read my blog →", url: "/blog/"))))
}
}
最后回到WebFrontendController
, 對(duì)之前的修改做內(nèi)容得填充娄柳。
Blog List
由于我們正在使用模塊化架構(gòu)構(gòu)建應(yīng)用程序,因此我們不能簡單地將與博客相關(guān)的內(nèi)容放入 Web
模塊中赤拒。 Web
模塊在我們的案例中有些特殊秫筏,因?yàn)樗鼮槲覀兲峁┝顺尸F(xiàn)網(wǎng)站的主要元素。 它還包含 Index
模板挎挖、Web
樣式表和 javascript
文件这敬。
我們將創(chuàng)建一個(gè)名為 Blog
的新模塊。 每個(gè)模塊都將遵循我們之前創(chuàng)建的相同模式蕉朵。 這意味著我們將擁有專用的router
和controller
崔涂。 在開始之前,我們將創(chuàng)建一個(gè) BlogPost
struct來表示我們的文章始衅。 在 Sources/App/Modules/Blog 目錄下新建一個(gè) Swift 文件冷蚂。
/// FILE: Sources/App/Modules/Blog/BlogPost.swift
import Foundation
public struct BlogPost: Codable {
public let title: String //標(biāo)題
public let slug: String //標(biāo)識(shí)符
public let image: String //突破路徑
public let excerpt: String //摘要
public let date: Date// 日期
public let category: String? //文章分類
public let content: String //文章內(nèi)容
}
現(xiàn)在我們定義了 BlogPost
的內(nèi)容,不過因?yàn)楫?dāng)前我們還沒有數(shù)據(jù)層去獲取存放這些數(shù)據(jù)汛闸,所以當(dāng)前我們暫時(shí)用隨機(jī)生成的數(shù)據(jù)去調(diào)試頁面蝙茶。 這塊邏輯交給 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 負(fù)責(zé)處理所有在網(wǎng)絡(luò)上公開的與博客相關(guān)的路由诸老。 這就是為什么它被稱為前端控制器隆夯。 稍后我們將使用相同的邏輯來創(chuàng)建其他類型的內(nèi)容通道,例如admin
控制器和 API
控制器别伏。
現(xiàn)在對(duì)于我們的博客文章頁面蹄衷,我們將需要一個(gè)新的 BlogPostsContext
結(jié)構(gòu),我們可以使用它來呈現(xiàn)頁面厘肮。
/// FILE: Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift
struct BlogPostsContext {
let icon: String
let title: String
let message: String
let posts: [BlogPost]
}
然后一樣的需要一個(gè)BlogPostsTemplate
模版來渲染數(shù)據(jù)
/// 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
框架的好處是我們可以直接使用大部分組件愧口。 例如,我們的列表將是響應(yīng)式的轴脐,因?yàn)槲覀兪褂玫氖?grid-221
類调卑。
這意味著網(wǎng)格將在臺(tái)式機(jī)和平板設(shè)備上使用 2 列布局,而在移動(dòng)設(shè)備上將使用單列大咱。 當(dāng)我們?cè)诹斜碇酗@示帖子時(shí)恬涧,我們必須調(diào)整帖子的標(biāo)準(zhǔn)標(biāo)題元素,我們將在web.css
文件中添加一個(gè)小改動(dòng)碴巾。
/* FILE: Public/css/web.css */
#blog h2 {
margin: 0.5rem 0;
}
現(xiàn)在我們回到 BlogFrontendController
去處理請(qǐng)求后的這個(gè)模版的渲染
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
模塊也應(yīng)該有專門的路由類管理頁面溯捆,我們?cè)趯?duì)應(yīng)模塊下創(chuàng)建BlogRouter.swift
文件,處理對(duì)應(yīng)的路由跳轉(zhuǎn)。
/// 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)
}
}
最后運(yùn)行應(yīng)用,點(diǎn)擊 Blog
tab 查看效果吧劳跃。
如果出現(xiàn) Pubilc
有對(duì)應(yīng)圖片谎仲,但是圖片展示不出來的問題,你需要配置一下 working directory
可以參考:https://theswiftdev.com/custom-working-directory-in-xcode/
Blog 詳情頁
列表頁已經(jīng)搭建完成了刨仑,現(xiàn)在我們將進(jìn)一步完成詳情頁的搭建郑诺。我們將首先在 Templates 文件夾中創(chuàng)建一個(gè)新的 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
處理詳情頁跳轉(zhuǎn)的邏輯杉武,我們通過路徑中的 slug
標(biāo)識(shí)去找到對(duì)應(yīng)的BlogPost
數(shù)據(jù)辙诞,如果沒有找到就重定向回主頁。
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)))
}
}
現(xiàn)在我們剩下的問題就是: 如何修改路由通過url
的path去匹配對(duì)應(yīng)的控制器轻抱。
因?yàn)槊總€(gè)詳情頁的路徑應(yīng)該是不同的蝇恶,所以這里我們可以使用 .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)
}
}
現(xiàn)在你可以運(yùn)行應(yīng)用豺憔,可以點(diǎn)擊文章詳情去查看內(nèi)容了证舟。
自定義中間件
現(xiàn)在我們可以使用同一路徑的兩個(gè)版本(例如 /blog/ vs /blog)訪問每個(gè) URL释树,我們將實(shí)現(xiàn)一個(gè)中間件去統(tǒng)一這2個(gè)url 的表現(xiàn)叙量。
/// 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
文件配置上這個(gè)中間件烛卧。
/// 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時(shí)晃痴,中間件會(huì)自動(dòng)帶上"/"后綴曹铃。類似這樣我們通過使用中間件做一些串行的處理缰趋。因?yàn)?Async
和 Await
需要macOS 12才支持,所以需要改一下Package.swift
的platforms
為.macOS(.v12)
最后一個(gè)菜單項(xiàng)呢陕见? 讓我們使用在教程開始時(shí)創(chuàng)建的那個(gè)空的 web.js 文件秘血。 我們將簡單地顯示一個(gè)alert,但當(dāng)然您可以使用此模板通過一些精美的動(dòng)畫來為網(wǎng)站增添趣味评甜。
/* FILE: Public/js/web.js */
function about() {
alert("myPage\n\nversion 1.0.0");
}
總結(jié)
這次我們學(xué)習(xí)到了通過 SwiftHtml 去搭建 Web 頁面的模版灰粮,模塊化得管理代碼,了解到路由的基礎(chǔ)知識(shí)忍坷。下一步粘舟,我們會(huì)開始使用Fluent 實(shí)現(xiàn)我們的數(shù)據(jù)層。