Web 开发的世界是广阔的,在每天涌现的源源不断的新技术中很容易感到迷失。大多数这些新技术都是使用 JavaScript 或 TypeScript 构建的。但是,在本文中,我将直接在浏览器中向您介绍如何使用原生 Swift 进行 Web 开发,我相信它会给您留下深刻印象。
为了让 Swift 在网页上本地工作,它需要先被编译成 WebAssembly 字节码,然后 JavaScript 可以将该代码加载到页面上。整个编译过程有点棘手,因为我们需要使用特殊的工具链和构建帮助文件,这就是为什么有帮助 CLI 工具可用:Carton 和 Webber。
SwiftWasm社区做了大量工作,通过修补原始 Swift 工具链,使将 Swift 编译成 WebAssembly 成为可能。他们通过每天自动从原始工具链中提取更改并在测试失败时修复他们的分支来更新工具链。他们的目标是成为官方工具链的一部分,他们希望这会在不久的将来发生。
Carton由 SwiftWasm 社区制作,可用于 Tokamak 项目,该框架使您能够使用 SwiftUI 编写网站。
Webber专为 SwifWeb 项目而设计。 SwifWeb 的不同之处在于它包装了整个 HTML 和 CSS 标准,以及所有的 Web API。
尽管您可能更喜欢使用 SwiftUI 来编写 Web 应用程序以实现代码一致性,但我认为这是错误的方法,因为 Web 开发在本质上是不同的,不能以与 SwiftUI 相同的方式进行处理。
这就是我创建 SwifWeb 的原因,它使您能够直接从 Swift 中使用 HTML、CSS 和 Web API 的所有功能,使用其优美的语法以及自动完成和文档。我创建了 Webber 工具,因为 Carton 无法以正确的方式编译、调试和部署 SwifWeb 应用程序,因为它不是为它创建的。
我叫 Mikhail Isaev,是 SwifWeb 的作者。在本文中,我将向您展示如何使用 SwifWeb 开始构建网站。
您需要安装 Swift,最简单的方法是:
其他情况看官网安装说明
我创建 Webber 是为了帮助您构建、调试和部署您的应用程序。
在 macOS 上很容易使用 HomeBrew 安装(从他们的网站安装)
brew install swifweb/tap/webber
稍后更新到最新版本只需运行
brew upgrade webber
在 Ubuntu 或 Windows(WSL2 中的 Ubuntu)上手动克隆和编译 Webber
sudo apt-get install binaryen curl https://get.wasmer.io -sSfL | sh apt-get install npm cd /opt sudo git clone https://github.com/swifweb/webber cd webber sudo swift build -c release sudo ln -s /opt/webber/.build/release/Webber /usr/local/bin/webber
稍后更新到最新版本运行
cd /opt/webber sudo git pull sudo swift build -c release
主分支总是包含稳定的代码,所以可以随意从中提取更新
打开终端并执行
webber new
在交互式菜单中选择pwa
或spa
,然后输入项目名称。
将目录更改为新创建的项目并执行webber serve
。
此命令会将您的项目编译成 WebAssembly,将所有必需的文件打包到一个特殊的.webber
文件夹中,并在默认情况下使用端口8888
在所有接口上开始为您的项目提供服务。
webber serve
的附加参数
应用类型
-t pwa
用于渐进式 Web 应用程序
-t spa
用于单个 Web 应用程序
Service Worker 目标的名称(在 PWA 项目中通常命名为Service
)
-s Service
应用目标的名称(默认为App
)
-a App
在控制台中打印更多信息
-v
Webber 服务器的端口(默认为8888
)
-p 8080
使用-p 443
像真正的 SSL 一样进行测试(使用允许的自签名 SSL 设置)
自动启动的目标浏览器名称
--browser safari
或--browser chrome
具有允许自签名 SSL 设置以调试服务人员的浏览器的附加实例
--browser-self-signed
处于隐身模式的浏览器的其他实例
--browser-incognito
该应用程序开始于Sources/App/App.swift
import Web @main class App: WebApp { @AppBuilder override var app: Configuration { Lifecycle.didFinishLaunching { app in app.registerServiceWorker("service") } Routes { Page { IndexPage() } Page("login") { LoginPage() } Page("article/:id") { ArticlePage() } Page("**") { NotFoundPage() } } MainStyle() } }
它以类似 iOS 的方式工作:
didFinishLaunching
应用刚启动时
willTerminate
当应用程序将要结束时
willResignActive
当窗口将要处于非活动状态时
didBecomeActive
当窗口处于活动状态时
didEnterBackground
当窗口进入后台时
willEnterForeground
当窗口进入前景时
这里最有用的方法是didFinishLaunching
因为它是配置应用程序的好地方。你看它感觉真的像一个 iOS 应用程序! 😀
这里的app
包含有用的便利方法:
registerServiceWorker(“serviceName“)
调用以注册 PWA service worker
addScript(“path/to/script.js“)
调用以添加相关或外部脚本
addStylesheet(“path/to/style.css“)
调用以添加相对或外部样式
addFont(“path/to/font.woff”, type:)
调用添加相对或外部字体,可选择设置类型
addIcon(“path/to/icon“, type:color:)
调用添加图标,可选择设置类型和颜色
此外,它是配置其他库的地方,如Autolayout 、 Bootstrap 、 Materialize等。
需要路由以根据当前 URL 显示适当的页面。
要了解如何使用路由,您必须了解 URL 是什么
https://website.com/hello/world - 这里/hello/world是路径
如您所见,一开始,我们应该在 App 类中声明所有顶级路由。
顶级意味着在这些路由中声明的页面将占据窗口中的整个空间。
好的,例如可以通过三种方式设置根路由
Page("/") { IndexPage() } Page("") { IndexPage() } Page { IndexPage() }
我认为最后一个是最美丽的🙂
可以这样设置登录或注册路由
Page("login") { LoginPage() } Page("registration") { RegistrationPage() }
参数相关路由
Page("article/:id") { ArticlePage() }
上例中的:id是路由的动态部分。我们可以在ArticlePage类中检索此标识符以显示与其关联的文章。
class ArticlePage: PageController { override func didLoad(with req: PageRequest) { if let articleId = req.parameters.get("id") { // Retrieve article here } } }
路径中可以有多个参数。以相同的方式检索所有这些。
路径中下一个有趣的事情是查询,它也非常易于使用。例如,让我们考虑/search路由,它期望具有搜索text
和age
查询参数。
https://website.com/search**?text=Alex&age=19** - 最后一部分是查询
只需声明搜索路线
Page("search") { SearchPage() }
并像这样在SearchPage类中检索查询数据
class SearchPage: PageController { struct Query: Decodable { let text: String? let age: Int? } override func didLoad(with req: PageRequest) { do { let query = try req.query.decode(Query.self) // use optional query.text and query.age // to query search results } catch { print("Can't decode query: \(error)") } } }
您也可以使用*
来声明接受特定路径部分中的任何内容的路由,如下所示
Page("foo", "*", "bar") { SearchPage() }
上面的路由将接受 foo 和 bar 之间的任何内容,例如 /foo/aaa/bar、/foo/bbb/bar 等。
使用**
符号,您可以设置一个特殊的包罗万象的路由,该路由将处理任何未与特定路径上的其他路由匹配的内容。
用它来制作全局 404 路由
Page("**") { NotFoundPage() }
或特定路径,例如当找不到用户时
Page("user", "**") { UserNotFoundPage() }
让我们澄清上面声明的路线的情况
/user/1 - 如果有 /user/:id 的路由,那么它将返回UserPage 。否则,它将陷入……
UserNotFoundPage
/user/1/hello - 如果有 /user/:id/hello 的路由,那么它将落入UserNotFoundPage
/something - 如果没有 /something 的路线那么它将落入NotFoundPage
我们可能不想为下一个路由替换页面上的全部内容,而只是替换某些块。这就是FragmentRouter派上用场的地方!
假设我们在/user页面上有选项卡。每个选项卡都是一个子路径,我们希望使用FragmentRouter对子路径中的更改做出反应。
在App类中声明顶级路由
Page("user") { UserPage() }
并在UserPage类中声明FragmentRouter
class UserPage: PageController { @DOM override var body: DOM.Content { // NavBar is from Materialize library :) Navbar() .item("Profile") { self.changePath(to: "/user/profile") } .item("Friends") { self.changePath(to: "/user/friends") } FragmentRouter(self) .routes { Page("profile") { UserProfilePage() } Page("friends") { UserFriendsPage() } } } }
在上面的示例中, FragmentRouter处理/user/profile和/user/friends子路由并将其呈现在Navbar下,因此页面永远不会重新加载整个内容,而只会重新加载特定的片段。
也可以声明多个具有相同或不同子路径的片段,它们将像魔术一样一起工作!
顺便说一句, FragmentRouter是一个Div ,您可以通过调用来配置它
FragmentRouter(self) .configure { div in // do anything you want with the div }
您可以使用传统的 CSS 文件,但您也拥有使用 Swift 编写的样式表的新的神奇能力!
要使用 Swift 声明 CSS 规则,我们有Rule对象。
它可以通过调用其方法以声明方式构建
Rule(...selector...) .alignContent(.baseline) .color(.red) // or rgba/hex color .margin(v: 0, h: .auto)
或使用 @resultBuilder 的类似 SwiftUI 的方式
Rule(...selector...) { AlignContent(.baseline) Color(.red) Margin(v: 0, h: .auto) }
两种方式都是一样的,但是,我更喜欢第一种方式,因为在我输入.
😀
MDN 上描述的所有 CSS 方法都可用。
不仅如此,它还能自动处理浏览器前缀!
但是,您可以在某些特定情况下以这种方式设置自定义属性
Rule(...selector...) .custom("customKey", "customValue")
要设置规则应该影响哪些元素,我们必须设置一个选择器。我将选择器视为数据库中的查询,但我将该选择器查询的一部分称为指针。
构建指针的最简单方法是使用原始字符串对其进行初始化
Pointer("a")
但是正确的快速方法是通过在需要的 HTML 标记处调用.pointer
来构建它,就像这样
H1.pointer // h1 A.pointer // a Pointer.any // * Class("myClass").pointer // .myClass Id("myId").pointer // #myId
它是关于基本指针的,但它们也有像:hover
:first
:first-child
等修饰符。
H1.pointer.first // h1:first H1.pointer.firstChild // h1:first-child H1.pointer.hover // h1:hover
您可以声明任何现有的修饰符,它们都是可用的。
如果缺少某些内容,请毫不犹豫地进行扩展以添加它!
并且不要忘记在 github 上发送 pull request 来为每个人添加它。
您还可以连接指针
H1.class(.myClass) // h1.myClass H1.id(.myId) // h1#myId H1.id(.myId).disabled // h1#myId:disabled Div.pointer.inside(P.pointer) // div p Div.pointer.parent(P.pointer) // div > p Div.pointer.immediatedlyAfter(P.pointer) // Div + p P.pointer.precededBy(Ul.pointer) // p ~ ul
如何在规则中使用选择器
Rule(Pointer("a")) // or Rule(A.pointer)
如何在规则中使用多个选择器
Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer))
它产生以下 CSS 代码
a, h1#myId, div > p { }
让我们为我们的应用程序声明深色和浅色样式,稍后,我们将能够轻松地在它们之间切换。
import Web @main class App: WebApp { enum Theme { case light, dark } @State var theme: Theme = .light @AppBuilder override var app: Configuration { // ... Lifecycle, Routes ... LightStyle().disabled($theme.map { $0 != .happy }) DarkStyle().disabled($theme.map { $0 != .sad }) } }
LightStyle和DarkStyle可以在单独的文件中或例如在 App.swift 中声明
class LightStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.white) Rule(H1.pointer).color(.black) } } class DarkStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.black) Rule(H1.pointer).color(.white) } }
然后在某个页面的用户界面中的某个地方调用
App.current.theme = .light // to switch to light theme // or App.current.theme = .dark // to switch to dark theme
它将激活或停用相关的样式表!这不是很酷吗? 😎
但是你可能会说用 Swift 描述样式比用 CSS 更难,那有什么意义呢?
重点是反应性!我们可以将 @State 与 CSS 属性一起使用并动态更改值!
看一下,我们可以创建一个具有一些响应式属性的类,并在运行时随时更改它,因此屏幕上使用该类的任何元素都会更新!它比为许多元素切换类要有效得多!
import Web @main class App: WebApp { @State var reactiveColor = Color.cyan @AppBuilder override var app: Configuration { // ... Lifecycle, Routes ... MainStyle() } } extension Class { static var somethingCool: Class { "somethingCool" } } class MainStyle: Stylesheet { @Rules override var rules: Rules.Content { // for all elements with `somethingCool` class Rule(Class.hello.pointer) .color(App.current.$reactiveColor) // for H1 and H2 elements with `somethingCool` class Rule(H1.class(.hello), H2.class(.hello)) .color(App.current.$reactiveColor) } }
稍后从代码中的任何地方调用
App.current.reactiveColor = .yellow // or any color you want
它将更新样式表和所有使用它的元素中的颜色😜
此外,还可以将原始 CSS 添加到样式表中
class MainStyle: Stylesheet { @Rules override var rules: Rules.Content { // for all elements with `somethingCool` class Rule(Class.hello.pointer) .color(App.current.$reactiveColor) // for H1 and H2 elements with `somethingCool` class Rule(H1.class(.hello), H2.class(.hello)) .color(App.current.$reactiveColor) """ /* Raw CSS goes here */ body { margin: 0; padding: 0; } """ } }
您可以根据需要多次混合原始 CSS 字符串
路由器在每条路线上渲染页面。 Page 是从PageController继承的任何类。
PageController具有诸如willLoad
didLoad
willUnload
didUnload
之类的生命周期方法、UI 方法buildUI
和body
以及用于 HTML 元素的属性包装器变量。
从技术上讲, PageController只是一个 Div,您可以在buildUI
方法中设置它的任何属性。
class IndexPage: PageController { // MARK: - Lifecycle override func willLoad(with req: PageRequest) { super.willLoad(with: req) } override func didLoad(with req: PageRequest) { super.didLoad(with: req) // set page title and metaDescription self.title = "My Index Page" self.metaDescription = "..." // also parse query and hash here } override func willUnload() { super.willUnload() } override func didUnload() { super.didUnload() } // MARK: - UI override func buildUI() { super.buildUI() // access any properties of the page's div here // eg self.backgroundcolor(.lightGrey) // optionally call body method here to add child HTML elements body { P("Hello world") } // or alternatively self.appendChild(P("Hello world")) } // the best place to declare any child HTML elements @DOM override var body: DOM.Content { H1("Hello world") P("Text under title") Button("Click me") { self.alert("Click!") print("button clicked") } } }
如果你的页面很小,你甚至可以用这么短的方式声明它
PageController { page in H1("Hello world") P("Text under title") Button("Click me") { page.alert("Click!") print("button clicked") } } .backgroundcolor(.lightGrey) .onWillLoad { page in } .onDidLoad { page in } .onWillUnload { page in } .onDidUnload { page in }
是不是又漂亮又简洁? 🥲
奖金便利方法
alert(message: String)
- 直接 JS alert
方法
changePath(to: String)
- 切换 URL 路径
最后,我将告诉您如何(!)构建和使用 HTML 元素!
所有带有属性的 HTML 元素在 Swift 中都可用,完整的列表在例如MDN上。
只是一个 HTML 元素的简短列表示例:
SwifWeb代码 | 网页代码 |
---|---|
| |
| |
| |
| |
| |
| |
如您所见,在 Swift 中访问任何 HTML 标签都非常容易,因为除了输入之外,它们都以相同的名称表示。这是因为不同的输入类型有不同的方法,我不想混用。
简单的Div
Div()
我们可以像这样访问它的所有属性和样式属性
Div().class(.myDivs) // <div class="myDivs"> .id(.myDiv) // <div id="myDiv"> .backgroundColor(.green) // <div style="background-color: green;"> .onClick { // adds `onClick` listener directly to the DOM element print("Clicked on div") } .attribute("key", "value") // <div key="value"> .attribute("boolKey", true, .trueFalse) // <div boolKey="true"> .attribute("boolKey", true, .yesNo) // <div boolKey="yes"> .attribute("checked", true, .keyAsValue) // <div checked="checked"> .attribute("muted", true, .keyWithoutValue) // <div muted> .custom("border", "2px solid red") // <div style="border: 2px solid red;">
子类化 HTML 元素为其预定义样式,或者使用大量预定义的子元素和一些外部可用的方便方法制作复合元素,或者实现生命周期事件,如didAddToDOM
和didRemoveFromDOM
。
让我们创建一个Divider
元素,它只是一个Div
但具有预定义的.divider
类
public class Divider: Div { // it is very important to override the name // because otherwise it will be <divider> in HTML open class override var name: String { "\(Div.self)".lowercased() } required public init() { super.init() } // this method executes immediately after any init method public override func postInit() { super.postInit() // here we are adding `divider` class self.class(.divider) } }
在子类化时调用超级方法非常重要。
没有它,您可能会遇到意想不到的行为。
该元素可以立即或稍后附加到PageController或HTML 元素的 DOM。
马上
Div { H1("Title") P("Subtitle") Div { Ul { Li("One") Li("Two") } } }
或者稍后使用lazy var
lazy var myDiv1 = Div() lazy var myDiv2 = Div() Div { myDiv1 myDiv2 }
所以你可以提前声明一个HTML 元素,然后随时将它添加到 DOM 中!
lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()
任何 HTML 元素都有一个可选的 superview 属性,如果它被添加到 DOM 中,它可以访问其父元素
Div().superview?.backgroundColor(.red)
我们经常需要只在某些条件下显示元素,所以让我们使用if/else
来实现
lazy var myDiv1 = Div() lazy var myDiv2 = Div() lazy var myDiv3 = Div() var myDiv4: Div? var showDiv2 = true Div { myDiv1 if showDiv2 { myDiv2 } else { myDiv3 } if let myDiv4 = myDiv4 { myDiv4 } else { P("Div 4 was nil") } }
但它不是反应性的。如果您尝试将showDiv2
设置为false
则不会发生任何事情。
反应性例子
lazy var myDiv1 = Div() lazy var myDiv2 = Div() lazy var myDiv3 = Div() @State var showDiv2 = true Div { myDiv1 myDiv2.hidden($showDiv2.map { !$0 }) // shows myDiv2 if showDiv2 == true myDiv3.hidden($showDiv2.map { $0 }) // shows myDiv3 if showDiv2 == false }
我们为什么要使用$showDiv2.map {…}
?
排序:因为它不是 SwiftUI。完全没有。
在下面阅读有关@State
更多信息。
您可能还需要将原始 HTML 添加到页面或 HTML 元素中,这很容易实现
Div { """ <a href="https://google.com">Go to Google</a> """ }
let names = ["Bob", "John", "Annie"] Div { ForEach(names) { name in Div(name) } // or ForEach(names) { index, name in Div("\(index). \(name)") } // or with range ForEach(1...20) { index in Div() } // and even like this 20.times { Div().class(.shootingStar) } }
@State var names = ["Bob", "John", "Annie"] Div { ForEach($names) { name in Div(name) } // or with index ForEach($names) { index, name in Div("\(index). \(name)") } } Button("Change 1").onClick { // this will append new Div with name automatically self.names.append("George") } Button("Change 2").onClick { // this will replace and update Divs with names automatically self.names = ["Bob", "Peppa", "George"] }
与上面的示例相同,但也可以使用BuilderFunction
Stylesheet { ForEach(1...20) { index in CSSRule(Div.pointer.nthChild("\(index)")) // set rule properties depending on index } 20.times { index in CSSRule(Div.pointer.nthChild("\(index)")) // set rule properties depending on index } }
您可以在ForEach
循环中使用BuilderFunction
仅计算一次某个值,例如以下示例中的delay
值
ForEach(1...20) { index in BuilderFunction(9999.asRandomMax()) { delay in CSSRule(Pointer(".shooting_star").nthChild("\(index)")) .custom("top", "calc(50% - (\(400.asRandomMax() - 200)px))") .custom("left", "calc(50% - (\(300.asRandomMax() + 300)px))") .animationDelay(delay.ms) CSSRule(Pointer(".shooting_star").nthChild("\(index)").before) .animationDelay(delay.ms) CSSRule(Pointer(".shooting_star").nthChild("\(index)").after) .animationDelay(delay.ms) } }
它也可以将函数作为参数
BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 }
BuilderFunction也可用于 HTML 元素 :)
@State
是当今声明式编程最需要的东西。
正如我在上面告诉您的:它不是 SwiftUI,因此没有跟踪和重绘所有内容的全局状态机。 HTML 元素不是临时结构,而是类,因此它们是真实的对象,您可以直接访问它们。它更好更灵活,您拥有所有控制权。
它是一个属性包装器,通知所有订阅者它的变化。
enum Countries { case usa, australia, mexico } @State var selectedCounty: Countries = .usa $selectedCounty.listen { print("country changed") } $selectedCounty.listen { newValue in print("country changed to \(newValue)") } $selectedCounty.listen { oldValue, newValue in print("country changed from \(oldValue) to \(newValue)") }
简单的文本示例
@State var text = "Hello world!" H1($text) // whenever text changes it updates inner-text in H1 InputText($text) // while user is typing text it updates $text which updates H1
简单的数字示例
@State var height = 20.px Div().height($height) // whenever height var changes it updates height of the Div
简单的布尔示例
@State var hidden = false Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div
映射示例
@State var isItCold = true H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })
映射两个状态
@State var one = true @State var two = true Div().display($one.and($two).map { one, two in // returns .block if both one and two are true one && two ? .block : .none })
映射两个以上的状态
@State var one = true @State var two = true @State var three = 15 Div().display($one.and($two).map { one, two in // returns true if both one and two are true one && two }.and($three).map { oneTwo, three in // here oneTwo is a result of the previous mapping // returns .block if oneTwo is true and three is 15 oneTwo && three == 15 ? .block : .none })
所有 HTML 和 CSS 属性都可以处理@State
值
您可以向 Div 等具体元素添加一些方便的方法
extension Div { func makeItBeautiful() {} }
或元素组,如果您知道它们的父class
。
父类很少。
BaseActiveStringElement
- 适用于可以用字符串初始化的元素,如a
、 h1
等。
BaseContentElement
- 适用于其中可以包含内容的所有元素,如div
、 ul
等。
BaseElement
- 适用于所有元素
所以所有元素的扩展都可以这样写
extension BaseElement { func doSomething() {} }
颜色类负责颜色。它有预定义的 HTML 颜色,但你可以有自己的
extension Color { var myColor1: Color { .hex(0xf1f1f1) } // which is 0xF1F1F1 var myColor2: Color { .hsl(60, 60, 60) } // which is hsl(60, 60, 60) var myColor3: Color { .hsla(60, 60, 60, 0.8) } // which is hsla(60, 60, 60, 0.8) var myColor4: Color { .rgb(60, 60, 60) } // which is rgb(60, 60, 60) var myColor5: Color { .rgba(60, 60, 60, 0.8) } // which is rgba(60, 60, 60, 0.8) }
然后像H1(“Text“).color(.myColor1)
一样使用它
extension Class { var my: Class { "my" } }
然后像Div().class(.my)
一样使用它
extension Id { var myId: Id { "my" } }
然后像Div().id(.my)
一样使用它
window
对象被完全包装并可通过App.current.window
变量访问。
MDN上提供了完整参考。
让我们在下面做一个简短的概述
您可以在App.swift
的Lifecycle
中收听它,也可以直接通过这种方式收听
App.current.window.$isInForeground.listen { isInForeground in // foreground flag changed }
或者随时随地阅读
if App.current.window.isInForeground { // do somethign }
或者用 HTML 元素对其做出反应
Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white })
它与前景标志相同,但可通过App.current.window.isActive
访问
它检测用户是否仍在窗口内进行交互。
与前景标志相同,但可通过App.current.window.isOnline
访问
它检测用户是否仍然可以访问互联网。
与前景标志相同,但可通过App.current.window.isDark
访问
它检测用户的浏览器或操作系统是否处于暗模式。
窗口内容区域(视口)的大小,包括滚动条
App.current.window.innerSize
是Size对象里面的width
height
值。
也可用作@State
变量。
浏览器窗口的大小,包括工具栏/滚动条。
App.current.window.outerSize
是Size对象里面的width
和height
值。
也可用作@State
变量。
用于检查呈现当前窗口的屏幕属性的特殊对象。通过App.current.window.screen
可用。
最有趣的属性通常是pixelRatio
。
包含用户访问的 URL(在浏览器窗口内)。
通过App.current.window.history
或History.shared
可用。
它可以作为@State
变量访问,因此您可以在需要时监听它的变化。
App.current.window.$history.listen { history in // read history properties }
它也可以作为简单变量访问
History.shared.length // size of the history stack History.shared.back() // to go back in history stack History.shared.forward() // to go forward in history stack History.shared.go(offset:) // going to specific index in history stack
MDN上提供了更多详细信息。
包含有关当前 URL 的信息。
通过App.current.window.location
或Location.shared
可用。
它可以作为@State
变量访问,因此您可以在需要时监听它的变化。
例如,这就是路由器的工作方式。
App.current.window.$location.listen { location in // read location properties }
它也可以作为简单变量访问
Location.shared.href // also $href Location.shared.host // also $host Location.shared.port // also $port Location.shared.pathname // also $pathname Location.shared.search // also $search Location.shared.hash // also $hash
MDN上提供了更多详细信息。
包含有关浏览器的信息。
通过App.current.window.navigator
或Navigator.shared
可用
最有趣的属性通常是userAgent
platform
language
cookieEnabled
。
允许在网络浏览器中保存键/值对。存储没有到期日期的数据。
可作为App.current.window.localStorage
或只是LocalStorage.shared
。
// You can save any value that can be represented in JavaScript LocalStorage.shared.set("key", "value") // saves String LocalStorage.shared.set("key", 123) // saves Int LocalStorage.shared.set("key", 0.8) // saves Double LocalStorage.shared.set("key", ["key":"value"]) // saves Dictionary LocalStorage.shared.set("key", ["v1", "v2"]) // saves Array // Getting values back LocalStorage.shared.string(forKey: "key") // returns String? LocalStorage.shared.integer(forKey: "key") // returns Int? LocalStorage.shared.string(forKey: "key") // returns String? LocalStorage.shared.value(forKey: "key") // returns JSValue? // Removing item LocalStorage.shared.removeItem(forKey: "key") // Removing all items LocalStorage.shared.clear()
跟踪更改
LocalStorage.onChange { key, oldValue, newValue in print("LocalStorage: key \(key) has been updated") }
跟踪所有物品的移除
LocalStorage.onClear { print("LocalStorage: all items has been removed") }
允许在网络浏览器中保存键/值对。仅存储一个会话的数据。
可作为App.current.window.sessionStorage
或只是SessionStorage.shared
。
API 与上述LocalStorage完全相同。
表示浏览器中加载的任何网页,并作为网页内容的入口点。
通过App.current.window.document
可用。
App.current.window.document.title // also $title App.current.window.document.metaDescription // also $metaDescription App.current.window.document.head // <head> element App.current.window.document.body // <body> element App.current.window.documentquerySelector("#my") // returns BaseElement? App.current.window.document.querySelectorAll(".my") // returns [BaseElement]
经典本地化是自动的,取决于用户系统语言
H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
如果您想即时更改屏幕上的本地化字符串(无需重新加载页面)
您可以通过调用更改当前语言
Localization.current = .es
如果您将用户的语言保存在 cookie 或 localstorage 中的某处,那么您必须在应用程序启动时进行设置
Lifecycle.didFinishLaunching { Localization.current = .es }
H1(LString( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
H1(Localization.currentState.map { "Curent language: \($0.rawValue)" }) H2(LString(.en("English string"), .es("Hilo Español"))) Button("change lang").onClick { Localization.current = Localization.current.rawValue.contains("en") ? .es : .en }
import FetchAPI Fetch("https://jsonplaceholder.typicode.com/todos/1") { switch $0 { case .failure: break case .success(let response): print("response.code: \(response.status)") print("response.statusText: \(response.statusText)") print("response.ok: \(response.ok)") print("response.redirected: \(response.redirected)") print("response.headers: \(response.headers.dictionary)") struct Todo: Decodable { let id, userId: Int let title: String let completed: Bool } response.json(as: Todo.self) { switch $0 { case .failure(let error): break case .success(let todo): print("decoded todo: \(todo)") } } } }
import XMLHttpRequest XMLHttpRequest() .open(method: "GET", url: "https://jsonplaceholder.typicode.com/todos/1") .onAbort { print("XHR onAbort") }.onLoad { print("XHR onLoad") }.onError { print("XHR onError") }.onTimeout { print("XHR onTimeout") }.onProgress{ progress in print("XHR onProgress") }.onLoadEnd { print("XHR onLoadEnd") }.onLoadStart { print("XHR onLoadStart") }.onReadyStateChange { readyState in print("XHR onReadyStateChange") } .send()
import WebSocket let webSocket = WebSocket("wss://echo.websocket.org").onOpen { print("ws connected") }.onClose { (closeEvent: CloseEvent) in print("ws disconnected code: \(closeEvent.code) reason: \(closeEvent.reason)") }.onError { print("ws error") }.onMessage { message in print("ws message: \(message)") switch message.data { case .arrayBuffer(let arrayBuffer): break case .blob(let blob): break case .text(let text): break case .unknown(let jsValue): break } } Dispatch.asyncAfter(2) { // send as simple string webSocket.send("Hello from SwifWeb") // send as Blob webSocket.send(Blob("Hello from SwifWeb")) }
简单的print(“Hello world“)
等同于 JavaScript 中的console.log('Hello world')
控制台方法也被爱包裹❤️
Console.dir(...) Console.error(...) Console.warning(...) Console.clear()
要进行实时预览,请在您想要的每个文件中声明 WebPreview 类。
class IndexPage: PageController {} class Welcome_Preview: WebPreview { @Preview override class var content: Preview.Content { Language.en Title("Initial page") Size(640, 480) // add here as many elements as needed IndexPage() } }
请阅读存储库页面上的说明。这是一个棘手但完全有效的解决方案😎
转到VSCode中的扩展并搜索Webber 。
安装后按Cmd+Shift+P
(或 Linux/Windows 上的Ctrl+Shift+P
)
查找并启动Webber Live Preview
。
在右侧,您将看到实时预览窗口,只要您保存包含WebPreview类的文件,它就会刷新。
它可以通过作为SwifWeb基础的JavaScriptKit获得。
在官方存储库中阅读你的方法。
您可以在项目中添加css
、 js
、 png
、 jpg
和任何其他静态资源。
但是要在调试期间或最终发布文件中使用它们,您必须像这样在Package.swift中声明它们
.executableTarget(name: "App", dependencies: [ .product(name: "Web", package: "web") ], resources: [ .copy("css/*.css"), .copy("css"), .copy("images/*.jpg"), .copy("images/*.png"), .copy("images/*.svg"), .copy("images"), .copy("fonts/*.woff2"), .copy("fonts") ]),
稍后您将能够访问它们,例如像这样Img().src(“/images/logo.png“)
通过以下方式启动Webber
webber serve
只是为了快速启动它
webber serve -t pwa -s Service
以 PWA 模式启动它
-v
或--verbose
在控制台中显示更多信息以进行调试
-p 443
或--port 443
在 443 端口而不是默认的 8888 端口上启动 webber 服务器
--browser chrome/safari
自动打开浏览器,默认不打开
--browser-self-signed
需要在本地调试服务工作者,否则它们不起作用
--browser-incognito
以隐身模式打开其他浏览器实例,仅适用于 chrome
因此,要在调试模式下构建您的应用程序,请在 Chrome 中自动打开它并在您更改任何文件时自动刷新浏览器以这种方式启动它
用于水疗
webber serve --browser chrome
用于真正的 PWA 测试
webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito
您可能希望改进初始加载过程。
为此,只需打开项目内的.webber/entrypoint/dev
文件夹并编辑index.html
文件。
它包含带有非常有用的侦听器的初始 HTML 代码: WASMLoadingStarted
WASMLoadingStartedWithoutProgress
WASMLoadingProgress
WASMLoadingError
。
您可以随意编辑该代码以实现您的自定义样式 🔥
当你完成新的实现时,不要忘记将它保存到.webber/entrypoint/release
文件夹中
只需执行webber release
或webber release -t pwa -s Service
for PWA。
然后从.webber/release
文件夹中获取编译后的文件并将它们上传到您的服务器。
您可以将文件上传到任何静态主机。
托管应该为wasm文件提供正确的内容类型!
是的,为wasm文件设置正确的标头Content-Type: application/wasm
非常重要,否则不幸的是浏览器将无法加载您的 WebAssembly 应用程序。
例如,GithubPages没有为wasm文件提供正确的 Content-Type,因此很遗憾,无法在其上托管 WebAssembly 站点。
如果你用的是自己的nginx服务器,那么打开/etc/nginx/mime.types
查看是否包含application/wasm wasm;
记录。如果是,那么您就可以开始了!
我希望我今天给您带来惊喜并且您至少会尝试 SwifWeb 并且最多会开始将它用于您的下一个大型 Web 项目!
请随时为任何SwifWeb 库做出贡献,并为 ⭐️ 全部加注星标!
我在 Discord 中有一个很棒的 SwiftStream 社区,您可以在其中找到巨大的支持,阅读一些教程,并在任何即将到来的更新时第一时间收到通知!很高兴见到你和我们在一起!
这只是一个开始,敬请期待更多关于 SwifWeb 的文章!
告诉你的朋友!