Web 开发的世界是广阔的,在每天涌现的源源不断的新技术中很容易感到迷失。大多数这些新技术都是使用 JavaScript 或 TypeScript 构建的。但是,在本文中,我将直接在浏览器中向您介绍如何使用原生 Swift 进行 Web 开发,我相信它会给您留下深刻印象。 这怎么可能? 为了让 Swift 在网页上本地工作,它需要先被编译成 WebAssembly 字节码,然后 JavaScript 可以将该代码加载到页面上。整个编译过程有点棘手,因为我们需要使用特殊的工具链和构建帮助文件,这就是为什么有帮助 CLI 工具可用:Carton 和 Webber。 可以使用第三方工具链吗? 社区做了大量工作,通过修补原始 Swift 工具链,使将 Swift 编译成 WebAssembly 成为可能。他们通过每天自动从原始工具链中提取更改并在测试失败时修复他们的分支来更新工具链。他们的目标是成为官方工具链的一部分,他们希望这会在不久的将来发生。 SwiftWasm 纸箱还是韦伯? 由 SwiftWasm 社区制作,可用于 Tokamak 项目,该框架使您能够使用 SwiftUI 编写网站。 Carton 专为 SwifWeb 项目而设计。 SwifWeb 的不同之处在于它包装了整个 HTML 和 CSS 标准,以及所有的 Web API。 Webber 尽管您可能更喜欢使用 SwiftUI 来编写 Web 应用程序以实现代码一致性,但我认为这是错误的方法,因为 Web 开发在本质上是不同的,不能以与 SwiftUI 相同的方式进行处理。 这就是我创建 SwifWeb 的原因,它使您能够直接从 Swift 中使用 HTML、CSS 和 Web API 的所有功能,使用其优美的语法以及自动完成和文档。我创建了 Webber 工具,因为 Carton 无法以正确的方式编译、调试和部署 SwifWeb 应用程序,因为它不是为它创建的。 我叫 Mikhail Isaev,是 SwifWeb 的作者。在本文中,我将向您展示如何使用 SwifWeb 开始构建网站。 所需工具 迅速 您需要安装 Swift,最简单的方法是: 在 macOS 上是安装 Xcode 在 Linux 或 Windows(WSL2) 上是使用来自 的脚本 swiftlang.xyz 其他情况 看官网安装说明 韦伯 CLI 我创建 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 应用类型 用于渐进式 Web 应用程序 -t pwa 用于单个 Web 应用程序 -t spa Service Worker 目标的名称(在 PWA 项目中通常命名为 ) Service -s Service 应用目标的名称(默认为 ) App -a App 在控制台中打印更多信息 -v Webber 服务器的端口(默认为 ) 8888 -p 8080 使用 像真正的 SSL 一样进行测试(使用允许的自签名 SSL 设置) -p 443 自动启动的目标浏览器名称 或 --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 这里最有用的方法是 因为它是配置应用程序的好地方。你看它感觉真的像一个 iOS 应用程序! 😀 didFinishLaunching 这里的 包含有用的便利方法: app 调用以注册 PWA service worker registerServiceWorker(“serviceName“) 调用以添加相关或外部脚本 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 }) } } 和 可以在单独的文件中或例如在 App.swift 中声明 LightStyle DarkStyle 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 具有诸如 之类的生命周期方法、UI 方法 和 以及用于 HTML 元素的属性包装器变量。 PageController willLoad didLoad willUnload didUnload buildUI body 从技术上讲, 只是一个 Div,您可以在 方法中设置它的任何属性。 PageController 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 } 是不是又漂亮又简洁? 🥲 奖金便利方法 - 直接 JS 方法 alert(message: String) alert - 切换 URL 路径 changePath(to: String) 元素 最后,我将告诉您如何(!)构建和使用 HTML 元素! 所有带有属性的 HTML 元素在 Swift 中都可用,完整的列表在例如 上。 MDN 只是一个 HTML 元素的简短列表示例: SwifWeb代码 网页代码 Div() <div></div> H1(“text“) <h1>text</h1> A(“Click me“).href(““).target(.blank) <a href=”” target=”_blank”>Click me</a> Button(“Click“).onClick { print(“click“) } <button onclick=”…”>Click</button> InputText($text).placeholder("Title") <input type=”text” placeholder=”title”> InputCheckbox($checked) <input type=”checkbox”> 如您所见,在 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) } } 在子类化时调用超级方法非常重要。 没有它,您可能会遇到意想不到的行为。 追加到 DOM 该元素可以立即或稍后附加到 或 的 DOM。 PageController HTML 元素 马上 Div { H1("Title") P("Subtitle") Div { Ul { Li("One") Li("Two") } } } 或者稍后使用 lazy var lazy var myDiv1 = Div() lazy var myDiv2 = Div() Div { myDiv1 myDiv2 } 所以你可以提前声明一个 ,然后随时将它添加到 DOM 中! 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 添加到页面或 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"] } CSS 与上面的示例相同,但也可以使用 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 } 也可用于 HTML 元素 :) BuilderFunction 与@State 的反应性 是当今声明式 最需要的东西。 @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)") } HTML 元素如何对变化做出反应? 简单的文本示例 @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 扩展 扩展 HTML 元素 您可以向 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) 声明id extension Id { var myId: Id { "my" } } 然后像 一样使用它 Div().id(.my) 网络 API 窗户 对象被完全包装并可通过 变量访问。 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)") } } } } XMLHttpRequest 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() WebSocket 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")) } 安慰 简单的 等同于 JavaScript 中的 print(“Hello world“) 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() } } Xcode 请阅读 上的说明。这是一个棘手但完全有效的解决方案😎 存储库页面 VS代码 转到 中的 并搜索 。 VSCode 扩展 Webber 安装后按 (或 Linux/Windows 上的 ) Cmd+Shift+P Ctrl+Shift+P 查找并启动 。 Webber Live Preview 在右侧,您将看到实时预览窗口,只要您保存包含 类的文件,它就会刷新。 WebPreview 访问 JavaScript 它可以通过作为 基础的 获得。 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 以 PWA 模式启动它 webber serve -t pwa -s Service 附加参数 或 在控制台中显示更多信息以进行调试 -v --verbose 或 在 443 端口而不是默认的 8888 端口上启动 webber 服务器 -p 443 --port 443 自动打开浏览器,默认不打开 --browser chrome/safari 需要在本地调试服务工作者,否则它们不起作用 --browser-self-signed 以隐身模式打开其他浏览器实例,仅适用于 chrome --browser-incognito 因此,要在调试模式下构建您的应用程序,请在 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 建筑发布 只需执行 或 for PWA。 webber release webber release -t pwa -s Service 然后从 文件夹中获取编译后的文件并将它们上传到您的服务器。 .webber/release 如何部署 您可以将文件上传到任何静态主机。 托管应该为 文件提供正确的内容类型! wasm 是的,为 文件设置正确的标头 非常重要,否则不幸的是浏览器将无法加载您的 WebAssembly 应用程序。 wasm Content-Type: application/wasm 例如 没有为 文件提供正确的 Content-Type,因此很遗憾,无法在其上托管 WebAssembly 站点。 ,GithubPages wasm Nginx 如果你用的是自己的nginx服务器,那么打开 查看是否包含 记录。如果是,那么您就可以开始了! /etc/nginx/mime.types application/wasm wasm; 结论 我希望我今天给您带来惊喜并且您至少会尝试 SwifWeb 并且最多会开始将它用于您的下一个大型 Web 项目! 请随时为任何 做出贡献,并为 ⭐️ 全部加注星标! SwifWeb 库 我 您可以在其中找到巨大的支持,阅读一些教程,并在任何即将到来的更新时第一时间收到通知!很高兴见到你和我们在一起! 在 Discord 中有一个很棒的 SwiftStream 社区, 这只是一个开始,敬请期待更多关于 SwifWeb 的文章! 告诉你的朋友!