paint-brush
So verwenden Sie Swift für die Webentwicklungvon@imike
12,401 Lesungen
12,401 Lesungen

So verwenden Sie Swift für die Webentwicklung

von Mikhail Isaev33m2023/03/20
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

SwifWeb ist ein Framework, das Ihnen die Möglichkeit gibt, Websites mit SwiftUI zu schreiben. Es umfasst die gesamten HTML- und CSS-Standards sowie alle Web-APIs. In diesem Artikel zeige ich Ihnen, wie Sie mit dem SwifWeb-Framework mit der Erstellung einer Website beginnen.

People Mentioned

Mention Thumbnail
featured image - So verwenden Sie Swift für die Webentwicklung
Mikhail Isaev HackerNoon profile picture
0-item
1-item

Die Welt der Webentwicklung ist riesig und man fühlt sich leicht im ständigen Strom neuer Technologien, die jeden Tag auftauchen, verloren. Die meisten dieser neuen Technologien werden mit JavaScript oder TypeScript erstellt. In diesem Artikel stelle ich Ihnen jedoch die Webentwicklung mit nativem Swift direkt in Ihrem Browser vor und bin zuversichtlich, dass es Sie beeindrucken wird.


Wie ist es möglich?

Damit Swift nativ auf der Webseite funktioniert, muss es zuerst in WebAssembly-Bytecode kompiliert werden, und dann könnte JavaScript diesen Code auf die Seite laden. Der gesamte Kompilierungsprozess ist etwas knifflig, da wir die spezielle Toolchain verwenden und Hilfsdateien erstellen müssen. Deshalb stehen Hilfs-CLI-Tools zur Verfügung: Carton und Webber.

Ist es in Ordnung, eine Toolchain eines Drittanbieters zu verwenden?

Die SwiftWasm- Community hat enorm viel Arbeit geleistet, um die Kompilierung von Swift in WebAssembly zu ermöglichen, indem sie die ursprüngliche Swift-Toolchain gepatcht hat. Sie aktualisieren die Toolchain, indem sie jeden Tag automatisch Änderungen aus der ursprünglichen Toolchain abrufen und ihren Fork reparieren, wenn Tests fehlschlagen. Ihr Ziel ist es, Teil der offiziellen Toolchain zu werden und sie hoffen, dass dies in naher Zukunft geschieht.

Karton oder Webber?

Carton wird von der SwiftWasm-Community erstellt und kann für Tokamak-Projekte verwendet werden. Dabei handelt es sich um ein Framework, das Ihnen die Möglichkeit gibt, Websites mit SwiftUI zu schreiben.


Webber wurde für SwifWeb-Projekte entwickelt. SwifWeb unterscheidet sich dadurch, dass es die gesamten HTML- und CSS-Standards sowie alle Web-APIs umschließt.


Auch wenn Sie es aus Gründen der Codekonsistenz vielleicht vorziehen, Webanwendungen mit SwiftUI zu schreiben, glaube ich, dass dies der falsche Ansatz ist, da die Webentwicklung von Natur aus anders ist und nicht auf die gleiche Weise wie SwiftUI angegangen werden kann.


Aus diesem Grund habe ich SwifWeb erstellt, das Ihnen die Möglichkeit gibt, die gesamte Leistungsfähigkeit von HTML, CSS und Web-APIs direkt von Swift aus zu nutzen und dabei die schöne Syntax mit automatischer Vervollständigung und Dokumentation zu nutzen. Und ich habe das Webber-Tool erstellt, weil Carton SwifWeb-Apps nicht richtig kompilieren, debuggen und bereitstellen kann, weil es nicht dafür erstellt wurde.


Mein Name ist Mikhail Isaev und ich bin der Autor von SwifWeb. In diesem Artikel zeige ich Ihnen, wie Sie mit dem Erstellen einer Website mit SwifWeb beginnen.

Notwendige Werkzeuge


Schnell

Sie müssen Swift installiert haben. Der einfachste Weg, es zu installieren:

  • unter macOS ist die Installation von Xcode
  • unter Linux oder Windows (WSL2) ist die Verwendung eines Skripts von Swiftlang.xyz


In anderen Fällen werfen Sie einen Blick auf die Installationsanweisungen auf der offiziellen Website

Webber-CLI

Ich habe Webber erstellt, um Sie beim Erstellen, Debuggen und Bereitstellen Ihrer Apps zu unterstützen.


Unter macOS ist die Installation mit HomeBrew einfach (installieren Sie es von der Website ).

 brew install swifweb/tap/webber

Um später auf die neueste Version zu aktualisieren, führen Sie einfach aus

 brew upgrade webber


Unter Ubuntu oder Windows (Ubuntu in WSL2) klonen und kompilieren Sie Webber manuell

 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

um später auf die letzte Version zu aktualisieren

 cd /opt/webber sudo git pull sudo swift build -c release

Der Hauptzweig enthält immer stabilen Code, Sie können also jederzeit Updates daraus abrufen

Neues Projekt erstellen

Öffnen Sie das Terminal und führen Sie es aus

 webber new

Wählen Sie im interaktiven Menü pwa oder spa und geben Sie den Projektnamen ein.


Wechseln Sie in das Verzeichnis des neu erstellten Projekts und führen Sie webber serve aus.

Dieser Befehl kompiliert Ihr Projekt in WebAssembly, packt alle erforderlichen Dateien in einen speziellen .webber Ordner und beginnt mit der Bereitstellung Ihres Projekts auf allen Schnittstellen, die standardmäßig Port 8888 verwenden.


Zusätzliche Argumente für webber serve

  • App-Typ

    -t pwa für Progressive Web App

    -t spa für eine einzelne Web-App

  • Name des Service-Worker-Ziels (im PWA-Projekt normalerweise Service genannt)

    -s Service

  • Name des App-Ziels (standardmäßig App )

    -a App

  • Drucken Sie weitere Informationen in der Konsole aus

    -v

  • Port für den Webber-Server (Standard ist 8888 )

    -p 8080

Verwenden Sie -p 443 um wie echtes SSL zu testen (mit erlaubter selbstsignierter SSL-Einstellung).

  • Name des Zielbrowsers, in dem automatisch gestartet wird

    --browser safari oder --browser chrome

  • Zusätzliche Browserinstanz mit erlaubter selbstsignierter SSL-Einstellung zum Debuggen von Servicemitarbeitern

    --browser-self-signed

  • Zusätzliche Instanz des Browsers im Inkognito-Modus

    --browser-incognito

Anwendung

Die App beginnt in 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() } }

Lebenszyklus

Es funktioniert auf eine iOS-ähnliche Weise:

didFinishLaunching , als die App gerade gestartet wurde

willTerminate , wenn die App sterben wird

willResignActive wenn das Fenster inaktiv sein soll

didBecomeActive wenn das Fenster aktiv ist

didEnterBackground , wenn das Fenster in den Hintergrund wechselt

willEnterForeground , wenn das Fenster in den Vordergrund geht


Die nützlichste Methode ist hier didFinishLaunching , da sie sich hervorragend zum Konfigurieren der App eignet. Sie sehen, es fühlt sich wirklich wie eine iOS-App an! 😀


Diese app enthält nützliche Komfortmethoden:

registerServiceWorker(“serviceName“) -Aufruf zum Registrieren des PWA-Servicemitarbeiters

addScript(“path/to/script.js“) Aufruf zum Hinzufügen eines relativen oder externen Skripts

addStylesheet(“path/to/style.css“) Aufruf zum Hinzufügen eines relativen oder externen Stils

addFont(“path/to/font.woff”, type:) Aufruf zum Hinzufügen einer relativen oder externen Schriftart, optional Festlegen des Typs

addIcon(“path/to/icon“, type:color:) Aufruf zum Hinzufügen eines Symbols, optional Festlegung von Typ und Farbe


Außerdem können Sie hier zusätzliche Bibliotheken wie Autolayout , Bootstrap , Materialise usw. konfigurieren.

Routen

Routing ist erforderlich, um die entsprechende Seite basierend auf der aktuellen URL anzuzeigen.


Um zu verstehen, wie Routing verwendet wird, müssen Sie wissen, was eine URL ist

https://website.com/hello/world – hier ist /hello/world der Pfad

Wie Sie gesehen haben, sollten wir zu Beginn in der App-Klasse alle Routen der obersten Ebene deklarieren.

Top-Level bedeutet, dass in diesen Routen deklarierte Seiten den gesamten Platz im Fenster einnehmen.


Ok, die Root-Route kann beispielsweise auf drei Arten festgelegt werden

 Page("/") { IndexPage() } Page("") { IndexPage() } Page { IndexPage() }

Ich denke, das letzte ist das schönste 🙂


Auf diese Weise können Anmelde- oder Registrierungsrouten festgelegt werden

 Page("login") { LoginPage() } Page("registration") { RegistrationPage() }


Parameterbezogene Routen

 Page("article/:id") { ArticlePage() }

Die :id im obigen Beispiel ist ein dynamischer Teil der Route. Wir können diesen Bezeichner in der ArticlePage- Klasse abrufen, um den damit verbundenen Artikel anzuzeigen.

 class ArticlePage: PageController { override func didLoad(with req: PageRequest) { if let articleId = req.parameters.get("id") { // Retrieve article here } } }

Der Pfad kann mehr als einen Parameter enthalten. Rufen Sie sie alle auf die gleiche Weise ab.

Abfragen

Das nächste interessante Element im Pfad ist die Abfrage , die ebenfalls sehr einfach zu verwenden ist. Betrachten wir beispielsweise die Route /search , die die Abfrageparameter „ text “ und age erwartet.

https://website.com/search**?text=Alex&age=19** – der letzte Teil ist die Abfrage


Geben Sie einfach die Suchroute an

 Page("search") { SearchPage() }

Und rufen Sie Abfragedaten in der SearchPage- Klasse wie folgt ab

 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)") } } }

Irgendetwas

Sie können * auch verwenden, um eine Route zu deklarieren, die alles im spezifischen Pfadteil wie diesen akzeptiert

 Page("foo", "*", "bar") { SearchPage() }

Die obige Route akzeptiert alles zwischen foo und bar, z. B. /foo/aaa/bar, /foo/bbb/bar usw.

Alles in allem

Mit dem ** Zeichen können Sie eine spezielle Catch-All-Route festlegen, die alles verarbeitet, was nicht mit anderen Routen auf einem bestimmten Pfad abgeglichen wurde.


Verwenden Sie es, um entweder eine globale 404-Route zu erstellen

 Page("**") { NotFoundPage() }

oder für einen bestimmten Pfad, z. B. wenn der Benutzer nicht gefunden wurde

 Page("user", "**") { UserNotFoundPage() }


Lassen Sie uns Situationen mit den oben angegebenen Routen klären

/user/1 – wenn es eine Route für /user/:id gibt, wird UserPage zurückgegeben. Andernfalls fällt es in ...


UserNotFoundPage

/user/1/hello – wenn es eine Route für /user/:id/hello gibt, wird sie in UserNotFoundPage fallen

/something – wenn es keine Route für /something gibt, wird es in NotFoundPage abgelegt

Verschachteltes Routing

Möglicherweise möchten wir für die nächste Route nicht den gesamten Inhalt der Seite ersetzen, sondern nur bestimmte Blöcke. Hier kommt der FragmentRouter zum Einsatz!


Nehmen wir an, dass wir auf der Seite /user Registerkarten haben. Jede Registerkarte ist eine Unterroute, und wir möchten mit FragmentRouter auf Änderungen in der Unterroute reagieren.


Deklarieren Sie die Route der obersten Ebene in der App- Klasse

 Page("user") { UserPage() }

Und deklarieren Sie FragmentRouter in der UserPage- Klasse

 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() } } } }


Im obigen Beispiel verarbeitet FragmentRouter die Unterrouten /user/profile und /user/friends und stellt sie unter der Navbar dar, sodass die Seite nie den gesamten Inhalt neu lädt, sondern nur bestimmte Fragmente.


Es können auch mehr als ein Fragment mit denselben oder unterschiedlichen Unterrouten deklariert werden, und alle funktionieren einfach wie von Zauberhand zusammen!


Übrigens ist FragmentRouter ein Div und Sie können es durch Aufrufen konfigurieren

 FragmentRouter(self) .configure { div in // do anything you want with the div }

Stylesheets

Sie können herkömmliche CSS-Dateien verwenden, haben aber auch die neue, magische Möglichkeit, ein in Swift geschriebenes Stylesheet zu verwenden!

Grundlagen

Um eine CSS-Regel mit Swift zu deklarieren, haben wir das Rule- Objekt.


Es kann deklarativ durch den Aufruf seiner Methoden erstellt werden

 Rule(...selector...) .alignContent(.baseline) .color(.red) // or rgba/hex color .margin(v: 0, h: .auto)

oder SwiftUI-ähnliche Weise mit @resultBuilder

 Rule(...selector...) { AlignContent(.baseline) Color(.red) Margin(v: 0, h: .auto) }


Beide Möglichkeiten sind gleich, ich bevorzuge jedoch die erste wegen der automatischen Vervollständigung direkt nach der Eingabe . 😀

Alle auf MDN beschriebenen CSS-Methoden sind verfügbar.

Darüber hinaus verarbeitet es Browser-Präfixe automatisch!

In bestimmten Fällen können Sie jedoch benutzerdefinierte Eigenschaften auf diese Weise festlegen

 Rule(...selector...) .custom("customKey", "customValue")

Wähler

Um festzulegen, welche Elemente die Regel beeinflussen soll, müssen wir einen Selektor festlegen. Ich sehe den Selektor als Abfrage in der Datenbank, aber Teile dieser Selektorabfrage nenne ich Zeiger.


Der einfachste Weg, einen Zeiger zu erstellen, besteht darin, ihn mit der Rohzeichenfolge zu initialisieren

 Pointer("a")


Aber der richtige und schnelle Weg besteht darin, es zu erstellen, indem man .pointer auf das benötigte HTML-Tag wie dieses aufruft

 H1.pointer // h1 A.pointer // a Pointer.any // * Class("myClass").pointer // .myClass Id("myId").pointer // #myId

Es geht um grundlegende Zeiger, aber sie haben auch Modifikatoren wie :hover :first :first-child usw.

 H1.pointer.first // h1:first H1.pointer.firstChild // h1:first-child H1.pointer.hover // h1:hover

Sie können jeden vorhandenen Modifikator deklarieren, sie sind alle verfügbar.

Wenn etwas fehlt, zögern Sie nicht, eine Erweiterung vorzunehmen, um es hinzuzufügen!

Und vergessen Sie nicht, eine Pull-Anfrage auf Github zu senden, um sie für alle hinzuzufügen.

Sie können Zeiger auch verketten

 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


So verwenden Sie den Selektor in der Regel

 Rule(Pointer("a")) // or Rule(A.pointer)

So verwenden Sie mehr als einen Selektor in der Regel

 Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer))

Es erzeugt den folgenden CSS-Code

 a, h1#myId, div > p { }

Reaktivität

Lassen Sie uns dunkle und helle Stile für unsere App deklarieren und später können wir problemlos zwischen ihnen wechseln.

 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 und DarkStyle können in separaten Dateien oder z. B. in der App.swift deklariert werden


 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) } }


Und dann rufen Sie einfach irgendwo in der Benutzeroberfläche einer Seite auf

 App.current.theme = .light // to switch to light theme // or App.current.theme = .dark // to switch to dark theme

Und es aktiviert oder deaktiviert die zugehörigen Stylesheets! Ist das nicht cool? 😎


Aber Sie sagen vielleicht, dass die Beschreibung von Stilen in Swift statt in CSS schwieriger ist, wozu also?


Der Hauptpunkt ist die Reaktivität! Wir können @State mit CSS-Eigenschaften verwenden und Werte im Handumdrehen ändern!


Werfen Sie einen Blick darauf, wir können eine Klasse mit einer reaktiven Eigenschaft erstellen und diese jederzeit zur Laufzeit ändern, sodass jedes Element auf dem Bildschirm, das diese Klasse verwendet, aktualisiert wird! Es ist viel effektiver als der Klassenwechsel für viele Elemente!


 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) } }


Rufen Sie später einfach von einer beliebigen Stelle im Code aus an

 App.current.reactiveColor = .yellow // or any color you want

und es aktualisiert die Farbe im Stylesheet und in allen Elementen, die es verwenden 😜


Außerdem ist es möglich, rohes CSS in ein Stylesheet einzufügen

 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; } """ } }

Sie können rohe CSS-Strings so oft wie nötig einmischen

Seiten

Der Router rendert Seiten auf jeder Route. Page ist eine beliebige vom PageController geerbte Klasse.


PageController verfügt über Lebenszyklusmethoden wie willLoad didLoad willUnload didUnload , UI-Methoden buildUI und body sowie eine Eigenschafts-Wrapper-Variable für HTML-Elemente.

Technisch gesehen ist PageController nur ein Div und Sie können alle seine Eigenschaften in buildUI Methode festlegen.


 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") } } }


Wenn Ihre Seite sehr klein ist, können Sie sie auch auf diese kurze Weise deklarieren

 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 }

Ist es nicht schön und lakonisch? 🥲


Bonus-Komfortmethoden

alert(message: String) – direkte JS- alert

changePath(to: String) – URL-Pfad wechseln

HTML-Elemente

Abschließend erkläre ich Ihnen, wie(!) Sie HTML-Elemente erstellen und verwenden!


Alle HTML-Elemente mit ihren Attributen sind in Swift verfügbar, die vollständige Liste gibt es z. B. auf MDN .


Nur eine beispielhafte kurze Liste von HTML-Elementen:

SwifWeb-Code

HTML Quelltext

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”>


Wie Sie sehen, ist es in Swift sehr einfach, auf alle HTML-Tags zuzugreifen, da diese bis auf Eingaben alle unter demselben Namen dargestellt werden. Das liegt daran, dass verschiedene Eingabetypen unterschiedliche Methoden haben und ich sie nicht mischen wollte.


Einfache Div

 Div()

Auf diese Weise können wir auf alle seine Attribute und Stileigenschaften zugreifen

 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;">

Unterklassen

Unterklassen Sie ein HTML-Element, um den Stil dafür vorzudefinieren, oder um ein zusammengesetztes Element mit vielen vordefinierten untergeordneten Elementen und einigen praktischen Methoden außerhalb verfügbar zu machen, oder um Lebenszyklusereignisse wie didAddToDOM und didRemoveFromDOM zu erreichen.

Lassen Sie uns ein Divider Element erstellen, das nur ein Div ist, aber eine vordefinierte .divider Klasse hat

 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) } }

Es ist sehr wichtig, bei der Unterklassenerstellung Supermethoden aufzurufen.

Ohne sie kann es zu unerwartetem Verhalten kommen.

An DOM anhängen

Das Element kann sofort oder später an das DOM des PageControllers oder HTML-Elements angehängt werden.


Sofort

 Div { H1("Title") P("Subtitle") Div { Ul { Li("One") Li("Two") } } }


Oder später mit lazy var

 lazy var myDiv1 = Div() lazy var myDiv2 = Div() Div { myDiv1 myDiv2 }

Sie können also ein HTML-Element im Voraus deklarieren und es später jederzeit zum DOM hinzufügen!

Aus DOM entfernen

 lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()

Greifen Sie auf das übergeordnete Element zu

Jedes HTML-Element verfügt über eine optionale Superview-Eigenschaft, die Zugriff auf sein übergeordnetes Element ermöglicht, wenn es dem DOM hinzugefügt wird

 Div().superview?.backgroundColor(.red)

if/else-Bedingungen

Wir müssen Elemente oft nur unter bestimmten Bedingungen anzeigen, also verwenden wir dafür 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") } }

Aber es ist nicht reaktiv. Wenn Sie versuchen, showDiv2 auf false zu setzen, passiert nichts.


Reaktives Beispiel

 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 }


Warum sollten wir $showDiv2.map {…} verwenden ?

In gewisser Weise: weil es nicht SwiftUI ist. Überhaupt.


Lesen Sie unten mehr über @State .


Rohes HTML

Möglicherweise müssen Sie auch Roh-HTML in die Seite oder das HTML-Element einfügen, was problemlos möglich ist

 Div { """ <a href="https://google.com">Go to Google</a> """ }


Für jede

Statisches Beispiel

 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) } }

Dynamisches Beispiel

 @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

Wie in den obigen Beispielen, aber auch BuilderFunction ist verfügbar

 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 } }


Sie können BuilderFunction in ForEach Schleifen verwenden, um einen Wert nur einmal zu berechnen, wie im folgenden Beispiel einen 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) } }


Es kann auch als Argument fungieren

 BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 }

BuilderFunction ist auch für HTML-Elemente verfügbar :)

Reaktivität mit @State

@State ist heutzutage das wünschenswerteste Element für die deklarative Programmierung .


Wie ich Ihnen oben bereits sagte: Es handelt sich nicht um SwiftUI, daher gibt es keine globale Zustandsmaschine, die alles verfolgt und neu zeichnet. Und HTML-Elemente sind keine temporären Strukturen, sondern Klassen, also echte Objekte, auf die Sie direkt zugreifen können. Es ist viel besser und flexibler, Sie haben die volle Kontrolle.

Was steckt unter der Haube?

Es handelt sich um einen Eigenschafts-Wrapper, der alle Abonnenten über seine Änderungen benachrichtigt.

Wie abonniere ich Änderungen?

 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)") }

Wie können HTML-Elemente auf Änderungen reagieren?

Einfaches Textbeispiel

 @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

Einfaches Zahlenbeispiel

 @State var height = 20.px Div().height($height) // whenever height var changes it updates height of the Div

Einfaches boolesches Beispiel

 @State var hidden = false Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div

Mapping-Beispiel

 @State var isItCold = true H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })

Zwei Zustände abbilden

 @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 })

Kartierung von mehr als zwei Staaten

 @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 })

Alle HTML- und CSS-Eigenschaften können @State Werte verarbeiten

Erweiterungen

Erweitern Sie HTML-Elemente

Sie können konkreten Elementen wie Div einige praktische Methoden hinzufügen

 extension Div { func makeItBeautiful() {} }

Oder Gruppen von Elementen, wenn Sie deren übergeordnete class kennen.


Es gibt nur wenige Elternklassen.

BaseActiveStringElement – ist für Elemente, die mit String initialisiert werden können, wie a , h1 usw.

BaseContentElement – gilt für alle Elemente, die Inhalte enthalten können, wie div , ul usw.

BaseElement – gilt für alle Elemente


Auf diese Weise können Erweiterungen für alle Elemente geschrieben werden

 extension BaseElement { func doSomething() {} }

Farben deklarieren

Die Farbklasse ist für die Farben verantwortlich. Es verfügt über vordefinierte HTML-Farben, Sie können jedoch auch eigene verwenden

 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) }

Dann verwenden Sie es wie H1(“Text“).color(.myColor1)

Klassen deklarieren

 extension Class { var my: Class { "my" } }

Dann verwenden Sie es wie Div().class(.my)

IDs deklarieren

 extension Id { var myId: Id { "my" } }

Dann verwenden Sie es wie Div().id(.my)

Web-APIs

Fenster

window ist vollständig umschlossen und über die Variable App.current.window zugänglich.

Die vollständige Referenz finden Sie auf MDN .


Lassen Sie uns unten einen kurzen Überblick geben

Vordergrundflagge

Sie können es in Lifecycle im App.swift oder direkt hier anhören

 App.current.window.$isInForeground.listen { isInForeground in // foreground flag changed }

oder lesen Sie es einfach jederzeit und überall

 if App.current.window.isInForeground { // do somethign }

oder mit einem HTML-Element darauf reagieren

 Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white })

Aktive Flagge

Es ist dasselbe wie das Foreground-Flag, ist aber über App.current.window.isActive zugänglich

Es erkennt, ob ein Benutzer noch innerhalb des Fensters interagiert.

Online Status

Identisch mit der Vordergrundflagge, aber über App.current.window.isOnline zugänglich

Es erkennt, ob ein Benutzer noch Zugriff auf das Internet hat.

Darkmode-Status

Identisch mit der Foreground-Flagge, aber zugänglich über App.current.window.isDark

Es erkennt, ob sich der Browser oder das Betriebssystem eines Benutzers im Dunkelmodus befindet.

Innere Größe

Die Größe des Inhaltsbereichs (Ansichtsfenster) des Fensters einschließlich Bildlaufleisten

App.current.window.innerSize ist das Größenobjekt innerhalb width und height im Inneren.

Auch als @State Variable verfügbar.

Äußere Größe

Die Größe des Browserfensters, einschließlich Symbolleisten/Bildlaufleisten.

App.current.window.outerSize ist das Größenobjekt innerhalb width und height im Inneren.

Auch als @State Variable verfügbar.

Bildschirm

Spezielles Objekt zum Überprüfen der Eigenschaften des Bildschirms, auf dem das aktuelle Fenster gerendert wird. Verfügbar über App.current.window.screen .

Die interessanteste Eigenschaft ist normalerweise pixelRatio .

Geschichte

Enthält die vom Benutzer besuchten URLs (innerhalb eines Browserfensters).

Verfügbar über App.current.window.history oder einfach History.shared .

Sie ist als @State Variable zugänglich, sodass Sie bei Bedarf auf Änderungen warten können.

 App.current.window.$history.listen { history in // read history properties }

Es ist auch als einfache Variable zugänglich

 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

Weitere Einzelheiten finden Sie auf MDN .

Standort

Enthält Informationen zur aktuellen URL.

Verfügbar über App.current.window.location oder einfach Location.shared .

Sie ist als @State Variable zugänglich, sodass Sie bei Bedarf auf Änderungen warten können.

So funktioniert beispielsweise ein Router.

 App.current.window.$location.listen { location in // read location properties }


Es ist auch als einfache Variable zugänglich

 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

Weitere Einzelheiten finden Sie auf MDN .

Navigator

Enthält Informationen zum Browser.

Verfügbar über App.current.window.navigator oder einfach Navigator.shared

Die interessantesten Eigenschaften sind normalerweise language platform userAgent cookieEnabled .

Lokaler Speicher

Ermöglicht das Speichern von Schlüssel/Wert-Paaren in einem Webbrowser. Speichert Daten ohne Ablaufdatum.

Verfügbar als App.current.window.localStorage oder einfach 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()

Änderungen verfolgen

 LocalStorage.onChange { key, oldValue, newValue in print("LocalStorage: key \(key) has been updated") }

Verfolgung der Entfernung aller Gegenstände

 LocalStorage.onClear { print("LocalStorage: all items has been removed") }

Sitzungsspeicher

Ermöglicht das Speichern von Schlüssel/Wert-Paaren in einem Webbrowser. Speichert Daten nur für eine Sitzung.

Verfügbar als App.current.window.sessionStorage oder einfach SessionStorage.shared .

Die API ist absolut dieselbe wie in LocalStorage, wie oben beschrieben.

Dokumentieren

Stellt jede im Browser geladene Webseite dar und dient als Einstiegspunkt in den Inhalt der Webseite.

Verfügbar über 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]

Lokalisierung

Statische Lokalisierung

Die klassische Lokalisierung erfolgt automatisch und hängt von der Systemsprache des Benutzers ab

Wie benutzt man

 H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))

Dynamische Lokalisierung

Wenn Sie lokalisierte Zeichenfolgen auf dem Bildschirm im laufenden Betrieb ändern möchten (ohne Neuladen der Seite)

Sie können die aktuelle Sprache ändern, indem Sie anrufen

 Localization.current = .es

Wenn Sie die Sprache des Benutzers irgendwo in Cookies oder im lokalen Speicher gespeichert haben, müssen Sie sie beim Start der App festlegen

 Lifecycle.didFinishLaunching { Localization.current = .es }

Wie benutzt man

 H1(LString( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))

Erweitertes Beispiel

 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 }

Bringen

 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")) }

Konsole

Einfaches print(“Hello world“) entspricht console.log('Hello world') in JavaScript


Konsolenmethoden werden ebenfalls mit Liebe verpackt ❤️

 Console.dir(...) Console.error(...) Console.warning(...) Console.clear()

Live Vorschau

Damit die Live-Vorschau funktioniert, deklarieren Sie die WebPreview-Klasse in jeder gewünschten Datei.

 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

Bitte lesen Sie die Anweisungen auf der Repository-Seite . Es ist eine knifflige, aber voll funktionsfähige Lösung 😎


VSCode

Gehen Sie in VSCode zu „Erweiterungen“ und suchen Sie nach „Webber“ .

Sobald es installiert ist, drücken Sie Cmd+Shift+P (oder Ctrl+Shift+P unter Linux/Windows).

Suchen und starten Sie Webber Live Preview .

Auf der rechten Seite sehen Sie das Live-Vorschaufenster, das jedes Mal aktualisiert wird, wenn Sie die Datei speichern, die die WebPreview- Klasse enthält.

Zugriff auf JavaScript

Es ist über JavaScriptKit verfügbar, das die Grundlage von SwifWeb bildet.

Lesen Sie, wie es Ihnen geht, im offiziellen Repository .

Ressourcen

Sie können css , js , png , jpg und alle anderen statischen Ressourcen innerhalb des Projekts hinzufügen.

Damit sie jedoch während des Debuggens oder in endgültigen Release- Dateien verfügbar sind, müssen Sie sie alle wie folgt in Package.swift deklarieren

 .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") ]),

Später können Sie darauf zugreifen, z. B. so Img().src(“/images/logo.png“)

Debuggen

Starten Sie Webber auf folgende Weise

webber serve nur dazu, es schnell zu starten

webber serve -t pwa -s Service , um ihn im PWA-Modus zu starten

Zusätzliche Parameter

-v oder --verbose , um zu Debugzwecken weitere Informationen in der Konsole anzuzeigen

-p 443 oder --port 443 , um den Webber-Server auf Port 443 statt auf dem Standardport 8888 zu starten

--browser chrome/safari , um den gewünschten Browser automatisch zu öffnen, standardmäßig wird keiner geöffnet

--browser-self-signed wird benötigt, um Servicemitarbeiter lokal zu debuggen, andernfalls funktionieren sie nicht

--browser-incognito zum Öffnen einer zusätzlichen Browserinstanz im Inkognito-Modus, funktioniert nur mit Chrome


Um Ihre App im Debug-Modus zu erstellen, öffnen Sie sie automatisch in Chrome und aktualisieren Sie den Browser automatisch, wenn Sie eine Datei ändern. Starten Sie sie auf diese Weise

für SPA

webber serve --browser chrome

für echte PWA-Tests

webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito


Erstmaliges Laden der App

Möglicherweise möchten Sie den anfänglichen Ladevorgang verbessern.

Öffnen Sie dazu einfach den Ordner .webber/entrypoint/dev im Projekt und bearbeiten Sie die Datei index.html .

Es enthält anfänglichen HTML-Code mit sehr nützlichen Listenern: WASMLoadingStarted WASMLoadingStartedWithoutProgress WASMLoadingProgress WASMLoadingError .

Es steht Ihnen frei, diesen Code nach Ihren Wünschen zu bearbeiten, um Ihren benutzerdefinierten Stil zu implementieren 🔥

Wenn Sie die neue Implementierung abgeschlossen haben, vergessen Sie nicht, diese im Ordner .webber/entrypoint/release zu speichern

Gebäudefreigabe

Führen Sie einfach webber release oder webber release -t pwa -s Service für PWA aus.

Dann holen Sie sich die kompilierten Dateien aus dem Ordner .webber/release und laden Sie sie auf Ihren Server hoch.

So stellen Sie es bereit

Sie können Ihre Dateien auf jedes statische Hosting hochladen.

Das Hosting sollte den richtigen Inhaltstyp für WASM- Dateien bereitstellen!

Ja, es ist sehr wichtig, den richtigen Header Content-Type: application/wasm für WASM- Dateien zu haben, andernfalls kann der Browser Ihre WebAssembly-Anwendung leider nicht laden.

GithubPages bietet beispielsweise nicht den richtigen Inhaltstyp für WASM- Dateien, sodass es leider nicht möglich ist, WebAssembly-Sites darauf zu hosten.

Nginx

Wenn Sie Ihren eigenen Server mit Nginx verwenden, öffnen Sie /etc/nginx/mime.types und prüfen Sie, ob es application/wasm wasm; enthält. aufzeichnen. Wenn ja, dann können Sie loslegen!

Abschluss

Ich hoffe, ich habe Sie heute begeistert und Sie werden SwifWeb zumindest einmal ausprobieren und es höchstens für Ihr nächstes großes Webprojekt verwenden!


Bitte zögern Sie nicht, zu einer der SwifWeb-Bibliotheken beizutragen und sie alle zu markieren!


Ich habe eine großartige SwiftStream-Community in Discord, in der Sie großen Support finden, kleine Tutorials lesen und als Erster über bevorstehende Updates informiert werden können! Wäre cool, dich bei uns zu sehen!


Es ist erst der Anfang, also bleiben Sie dran für weitere Artikel über SwifWeb!


Sag deinen Freunden!