paint-brush
Comment utiliser Swift pour le développement Webpar@imike
12,387 lectures
12,387 lectures

Comment utiliser Swift pour le développement Web

par Mikhail Isaev33m2023/03/20
Read on Terminal Reader

Trop long; Pour lire

SwifWeb est un framework qui vous donne la possibilité d'écrire des sites Web à l'aide de SwiftUI. Il encapsule l'ensemble des normes HTML et CSS, ainsi que toutes les API Web. Dans cet article, je vais vous montrer comment commencer à créer un site Web à l'aide du framework SwifWeb.
featured image - Comment utiliser Swift pour le développement Web
Mikhail Isaev HackerNoon profile picture
0-item
1-item

Le monde du développement Web est vaste et il est facile de se sentir perdu dans le flux constant de nouvelles technologies qui émergent chaque jour. La plupart de ces nouvelles technologies sont construites à l'aide de JavaScript ou de TypeScript. Cependant, dans cet article, je vais vous présenter le développement Web à l'aide de Swift natif, directement dans votre navigateur, et je suis convaincu que cela vous impressionnera.


Comment est-ce possible?

Pour que Swift fonctionne nativement sur la page Web, il doit d'abord être compilé en byte-code WebAssembly, puis JavaScript pourrait charger ce code sur la page. L'ensemble du processus de compilation est un peu délicat car nous devons utiliser la chaîne d'outils spéciale et créer des fichiers d'aide, c'est pourquoi il existe des outils CLI d'aide disponibles : Carton et Webber.

Est-il acceptable d'utiliser une chaîne d'outils tierce ?

La communauté SwiftWasm a effectué un travail considérable pour rendre possible la compilation de Swift dans WebAssembly en corrigeant la chaîne d'outils Swift d'origine. Ils mettent à jour la chaîne d'outils en extrayant automatiquement les modifications de la chaîne d'outils d'origine chaque jour et en corrigeant leur fork si les tests échouent. Leur objectif est de faire partie de la chaîne d'outils officielle et ils espèrent que cela se produira dans un proche avenir.

Carton ou Webber ?

Carton est créé par la communauté SwiftWasm et peut être utilisé pour les projets Tokamak, qui est un cadre qui vous donne la possibilité d'écrire des sites Web à l'aide de SwiftUI.


Webber est fait pour les projets SwifWeb. SwifWeb est différent en ce sens qu'il englobe l'ensemble des normes HTML et CSS, ainsi que toutes les API Web.


Bien que vous préfériez peut-être écrire des applications Web à l'aide de SwiftUI pour la cohérence du code, je pense que ce n'est pas la bonne approche car le développement Web est intrinsèquement différent et ne peut pas être abordé de la même manière que SwiftUI.


C'est pourquoi j'ai créé SwifWeb, qui vous donne la possibilité d'utiliser toute la puissance des API HTML, CSS et Web directement à partir de Swift, en utilisant sa belle syntaxe avec auto-complétion et documentation. Et j'ai créé l'outil Webber car Carton n'est pas capable de compiler, déboguer et déployer correctement les applications SwifWeb car il n'a pas été créé pour cela.


Je m'appelle Mikhail Isaev et je suis l'auteur de SwifWeb. Dans cet article, je vais vous montrer comment commencer à créer un site Web à l'aide de SwifWeb.

Outils requis


Rapide

Vous devez avoir Swift installé, le moyen le plus simple de l'avoir :

  • sur macOS est d'installer Xcode
  • sous Linux ou Windows (WSL2) consiste à utiliser le script de swiftlang.xyz


Dans les autres cas, consultez les instructions d'installation sur le site officiel

CLI Webber

J'ai créé Webber pour vous aider à créer, déboguer et déployer vos applications.


Sur macOS, il est facile à installer avec HomeBrew (installez-le depuis leur site Web )

 brew install swifweb/tap/webber

pour mettre à jour vers la dernière version plus tard, exécutez simplement

 brew upgrade webber


Sur Ubuntu ou Windows (Ubuntu dans WSL2) clonez et compilez Webber manuellement

 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

pour mettre à jour vers la dernière version exécutée ultérieurement

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

la branche principale contient toujours du code stable, alors n'hésitez pas à en extraire les mises à jour

Création d'un nouveau projet

Ouvrez le terminal et exécutez

 webber new

Dans le menu interactif, choisissez pwa ou spa et entrez le nom du projet.


Changez le répertoire pour le projet nouvellement créé et exécutez webber serve .

Cette commande compilera votre projet dans WebAssembly, regroupera tous les fichiers nécessaires dans un dossier .webber spécial et commencera à servir votre projet sur toutes les interfaces en utilisant le port 8888 par défaut.


Arguments supplémentaires pour webber serve

  • Type d'application

    -t pwa pour l'application Web progressive

    -t spa pour une seule application Web

  • Nom de la cible du service worker (généralement nommé Service dans le projet PWA)

    -s Service

  • Nom de la cible de l'application ( App par défaut)

    -a App

  • Imprimer plus d'informations dans la console

    -v

  • Port pour le serveur Webber (la valeur par défaut est 8888 )

    -p 8080

Utilisez -p 443 pour tester comme le vrai SSL (avec le paramètre SSL auto-signé autorisé)

  • Nom du navigateur de destination dans lequel se lancer automatiquement

    --browser safari ou --browser chrome

  • Instance supplémentaire de navigateur avec paramètre SSL auto-signé autorisé pour déboguer les service-workers

    --browser-self-signed

  • Instance supplémentaire de navigateur en mode incognito

    --browser-incognito

Application

L'application commence dans 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() } }

Cycle de vie

Cela fonctionne à la manière d'iOS :

didFinishLaunching lorsque l'application vient de démarrer

willTerminate lorsque l'application va mourir

willResignActive lorsque la fenêtre va être inactive

didBecomeActive lorsque la fenêtre est active

didEnterBackground lorsque la fenêtre passe en arrière-plan

willEnterForeground lorsque la fenêtre passe au premier plan


La méthode la plus utile ici est didFinishLaunching car c'est un endroit idéal pour configurer l'application. Vous voyez, cela ressemble vraiment à une application iOS ! 😀


Ici, app contient des méthodes de commodité utiles :

registerServiceWorker(“serviceName“) appel pour enregistrer le service worker PWA

addScript(“path/to/script.js“) appel pour ajouter un script relatif ou externe

addStylesheet(“path/to/style.css“) appel pour ajouter un style relatif ou externe

addFont(“path/to/font.woff”, type:) appel pour ajouter une police relative ou externe, éventuellement définir le type

addIcon(“path/to/icon“, type:color:) appel pour ajouter une icône, éventuellement définir le type et la couleur


C'est aussi l'endroit où configurer des bibliothèques supplémentaires comme Autolayout , Bootstrap , Materialize , etc.

Itinéraires

Le routage est nécessaire pour afficher la page appropriée en fonction de l'URL actuelle.


Pour comprendre comment utiliser le routage, vous devez comprendre ce qu'est l'URL

https://website.com/hello/world - ici /hello/world est le chemin

Comme vous l'avez vu, au début, dans la classe App, nous devrions déclarer toutes les routes de niveau supérieur.

Le niveau supérieur signifie que les pages déclarées dans ces routes occuperont tout l'espace dans la fenêtre.


Ok, donc par exemple, la route racine peut être définie de trois manières

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

Je pense que la dernière est la plus belle 🙂


Les itinéraires de connexion ou d'enregistrement peuvent être définis comme ceci

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


Itinéraires liés aux paramètres

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

Le :id dans l'exemple ci-dessus est une partie dynamique de la route. On peut récupérer cet identifiant dans la classe ArticlePage pour afficher l'article qui lui est associé.

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

Vous pouvez avoir plusieurs paramètres dans le chemin. Récupérez-les tous de la même manière.

Requêtes

La prochaine chose intéressante dans le chemin est la requête , qui est également très facile à utiliser. Par exemple, considérons la route /search , qui s'attend à avoir le text de recherche et les paramètres de requête age .

https://website.com/search**?text=Alex&age=19** - la dernière partie est la requête


Déclarez simplement l'itinéraire de recherche

 Page("search") { SearchPage() }

Et récupérez les données de requête dans la classe SearchPage comme ceci

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

Quoi que ce soit

Vous pouvez également utiliser * pour déclarer une route qui accepte n'importe quoi dans la partie de chemin spécifique comme celle-ci

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

La route ci-dessus acceptera tout ce qui se trouve entre foo et bar, par exemple /foo/aaa/bar, /foo/bbb/bar, etc.

Fourre-tout

Avec le signe ** vous pouvez définir un itinéraire fourre-tout spécial qui gérera tout ce qui n'a pas été mis en correspondance avec d'autres itinéraires sur un chemin spécifique.


Utilisez-le pour créer une route 404 globale

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

ou pour un chemin spécifique, par exemple lorsque l'utilisateur est introuvable

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


Clarifions les situations avec les itinéraires déclarés ci-dessus

/user/1 - s'il existe une route pour /user/:id, il renverra UserPage . Sinon, il tombera dans…


UserNotFoundPage

/user/1/hello - s'il existe une route pour /user/:id/hello alors il tombera dans UserNotFoundPage

/something - s'il n'y a pas de route pour /something alors il tombera dans NotFoundPage

Routage imbriqué

Nous ne voulons peut-être pas remplacer tout le contenu de la page pour le prochain itinéraire, mais seulement certains blocs. C'est là que le FragmentRouter devient utile !


Considérons que nous avons des onglets sur la page /user . Chaque onglet est un sous-itinéraire, et nous voulons réagir aux changements dans le sous-itinéraire en utilisant le FragmentRouter .


Déclarez la route de niveau supérieur dans la classe App

 Page("user") { UserPage() }

Et déclarez FragmentRouter dans la classe UserPage

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


Dans l'exemple ci-dessus, FragmentRouter gère les sous-routes /user/profile et /user/friends et les restitue sous la barre de navigation , de sorte que la page ne recharge jamais tout le contenu mais uniquement des fragments spécifiques.


Il est également possible de déclarer plus d'un fragment avec les mêmes sous-routes ou des sous-routes différentes et ils fonctionneront tous ensemble comme par magie !


Btw FragmentRouter est un Div et vous pouvez le configurer en appelant

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

Feuilles de style

Vous pouvez utiliser des fichiers CSS traditionnels, mais vous avez également la nouvelle capacité magique d'utiliser une feuille de style écrite en Swift !

Bases

Pour déclarer une règle CSS à l'aide de Swift, nous avons l'objet Rule .


Il peut être construit de manière déclarative en appelant ses méthodes

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

ou de manière similaire à SwiftUI en utilisant @resultBuilder

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


Les deux manières sont égales, cependant, je préfère la première à cause de la saisie semi-automatique juste après avoir tapé . 😀

Toutes les méthodes CSS décrites sur MDN sont disponibles.

Plus que cela, il gère automatiquement les préfixes du navigateur !

Cependant, vous pouvez définir une propriété personnalisée de cette manière dans certains cas spécifiques

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

Sélecteur

Pour définir les éléments que la règle doit affecter, nous devons définir un sélecteur. Je vois le sélecteur comme la requête dans la base de données, mais j'appelle des parties de cette requête de sélecteur des pointeurs.


Le moyen le plus simple de construire un pointeur est de l'initialiser à l'aide de la chaîne brute

 Pointer("a")


Mais la bonne façon rapide est de le construire en appelant .pointer à la balise HTML nécessaire comme celle-ci

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

Il s'agit de pointeurs de base, mais ils ont aussi des modificateurs comme :hover :first :first-child etc.

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

Vous pouvez déclarer n'importe quel modificateur existant, ils sont tous disponibles.

S'il manque quelque chose n'hésitez pas à faire une extension pour l'ajouter !

Et n'oubliez pas d'envoyer une pull request sur github pour l'ajouter pour tout le monde.

Vous pouvez également concaténer des pointeurs

 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


Comment utiliser le sélecteur dans la règle

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

Comment utiliser plusieurs sélecteurs dans la règle

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

Il produit le code CSS suivant

 a, h1#myId, div > p { }

Réactivité

Déclarons les styles sombre et clair pour notre application, et plus tard, nous pourrons facilement basculer entre eux.

 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 et DarkStyle peuvent être déclarés dans des fichiers séparés ou par exemple dans l'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) } }


Et puis quelque part dans l'interface utilisateur d'une page, appelez simplement

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

Et cela activera ou désactivera les feuilles de style associées ! N'est-ce pas cool ? 😎


Mais vous pouvez dire que décrire des styles en Swift au lieu de CSS est plus difficile, alors à quoi ça sert ?


Le point principal est la réactivité ! Nous pouvons utiliser @State avec les propriétés CSS et modifier les valeurs à la volée !


Jetez un coup d'œil, nous pouvons créer une classe avec une propriété réactive et la modifier à tout moment pendant l'exécution, de sorte que tout élément à l'écran qui utilise cette classe sera mis à jour ! C'est bien plus efficace que de changer de classe pour de nombreux éléments !


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


Plus tard, depuis n'importe quel endroit du code, appelez simplement

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

et il mettra à jour la couleur dans la feuille de style et dans tous les éléments qui l'utilisent 😜


De plus, il est possible d'ajouter du CSS brut dans une feuille de style

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

vous pouvez mélanger des chaînes CSS brutes autant de fois que nécessaire

pages

Le routeur affiche des pages sur chaque route. Page est une classe héritée du PageController .


PageController a des méthodes de cycle de vie comme willLoad didLoad willUnload didUnload , les méthodes d'interface utilisateur buildUI et body , et une variable wrapper de propriété pour les éléments HTML.

Techniquement, PageController n'est qu'un Div et vous pouvez définir toutes ses propriétés dans la méthode 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") } } }


Si votre page est minuscule, vous pouvez la déclarer même de cette manière courte

 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 }

N'est-ce pas beau et laconique ? 🥲


Méthodes de commodité bonus

alert(message: String) - méthode alert JS directe

changePath(to: String) - changement de chemin d'URL

Éléments HTML

Enfin, je vous dirai comment (!) Construire et utiliser des éléments HTML !


Tous les éléments HTML avec leurs attributs sont disponibles dans Swift, la liste complète est par exemple sur MDN .


Juste un exemple de courte liste d'éléments HTML :

Code SwifWeb

Code HTML

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


Comme vous pouvez le voir, il est très facile d'accéder à n'importe quelle balise HTML dans Swift car elles sont toutes représentées sous le même nom, à l'exception des entrées. C'est parce que différents types d'entrée ont des méthodes différentes, et je ne voulais pas les mélanger.


Div simple

 Div()

nous pouvons accéder à tous ses attributs et propriétés de style comme celui-ci

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

Sous-classement

Sous-classe de l'élément HTML pour prédéfinir son style, ou pour créer un élément composite avec de nombreux éléments enfants prédéfinis et des méthodes pratiques disponibles à l'extérieur, ou pour réaliser des événements de cycle de vie comme didAddToDOM et didRemoveFromDOM .

Créons un élément Divider qui est juste un Div mais avec une classe .divider prédéfinie

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

Il est très important d'appeler des super méthodes lors de la sous-classe.

Sans cela, vous pourriez rencontrer un comportement inattendu.

Ajout au DOM

L'élément peut être ajouté au DOM de PageController ou à l'élément HTML immédiatement ou ultérieurement.


Tout de suite

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


Ou plus tard en utilisant lazy var

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

Vous pouvez donc déclarer un élément HTML à l'avance et l'ajouter au DOM à tout moment par la suite !

Suppression du DOM

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

Accéder à l'élément parent

Tout élément HTML a une propriété facultative superview qui donne accès à son parent s'il est ajouté au DOM

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

conditions si/sinon

Nous avons souvent besoin de montrer des éléments uniquement dans certaines conditions, alors utilisons if/else pour cela

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

Mais ce n'est pas réactif. Si vous essayez de définir showDiv2 sur false , rien ne se passe.


Exemple réactif

 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 }


Pourquoi devrions-nous utiliser $showDiv2.map {…} ?

En tri : parce que ce n'est pas SwiftUI. Du tout.


En savoir plus sur @State ci-dessous.


HTML brut

Vous devrez peut-être également ajouter du code HTML brut dans la page ou l'élément HTML et il est facilement possible

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


Pour chaque

Exemple statique

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

Exemple dynamique

 @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

Comme dans les exemples ci-dessus, mais BuilderFunction est également disponible

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


Vous pouvez utiliser BuilderFunction dans les boucles ForEach pour calculer une valeur une seule fois comme une valeur delay dans l'exemple suivant

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


Il peut également prendre la fonction comme argument

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

BuilderFunction est également disponible pour les éléments HTML :)

Réactivité avec @State

@State est la chose la plus souhaitable de nos jours pour la programmation déclarative.


Comme je vous l'ai dit plus haut : ce n'est pas SwiftUI, il n'y a donc pas de machine d'état globale qui suit et redessine tout. Et les éléments HTML ne sont pas des structures temporaires mais des classes, ce sont donc de vrais objets et vous y avez un accès direct. C'est beaucoup mieux et flexible, vous avez tout le contrôle.

C'est quoi sous le capot ?

Il s'agit d'un wrapper de propriété qui informe tous les abonnés de ses modifications.

Comment souscrire aux modifications ?

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

Comment les éléments HTML peuvent-ils réagir aux changements ?

Exemple de texte simple

 @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

Exemple de nombre simple

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

Exemple booléen simple

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

Exemple de mappage

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

Cartographier deux états

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

Cartographier plus de deux états

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

Toutes les propriétés HTML et CSS peuvent gérer les valeurs @State

Rallonges

Étendre les éléments HTML

Vous pouvez ajouter des méthodes pratiques à des éléments concrets comme Div

 extension Div { func makeItBeautiful() {} }

Ou des groupes d'éléments si vous connaissez leur class parente.


Il y a peu de classes parentales.

BaseActiveStringElement - est pour les éléments qui peuvent être initialisés avec une chaîne, comme a , h1 , etc.

BaseContentElement - est pour tous les éléments qui peuvent avoir du contenu à l'intérieur, comme div , ul , etc.

BaseElement - est pour tous les éléments


Ainsi, l'extension pour tous les éléments peut être écrite de cette façon

 extension BaseElement { func doSomething() {} }

Déclarer les couleurs

La classe de couleur est responsable des couleurs. Il a des couleurs HTML prédéfinies, mais vous pouvez avoir les vôtres

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

Ensuite, utilisez-le comme H1(“Text“).color(.myColor1)

Déclarer des cours

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

Ensuite, utilisez-le comme Div().class(.my)

Déclarer les identifiants

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

Ensuite, utilisez-le comme Div().id(.my)

API Web

Fenêtre

L'objet window est entièrement encapsulé et accessible via la variable App.current.window .

La référence complète est disponible sur MDN .


Faisons le bref aperçu ci-dessous

Drapeau de premier plan

Vous pouvez l'écouter dans Lifecycle dans l' App.swift ou directement de cette façon

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

ou juste le lire n'importe quand n'importe où

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

ou réagissez dessus avec l'élément HTML

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

Drapeau actif

C'est la même chose que le drapeau de premier plan, mais accessible via App.current.window.isActive

Il détecte si un utilisateur interagit toujours à l'intérieur de la fenêtre.

Statut en ligne

Identique au drapeau de premier plan, mais accessible via App.current.window.isOnline

Il détecte si un utilisateur a toujours accès à Internet.

Statut du mode sombre

Identique au drapeau de premier plan, mais accessible via App.current.window.isDark

Il détecte si le navigateur ou le système d'exploitation d'un utilisateur est en mode sombre.

Taille intérieure

La taille de la zone de contenu de la fenêtre (fenêtre) y compris les barres de défilement

App.current.window.innerSize est un objet Size dans les valeurs width et height à l'intérieur.

Également disponible en tant que variable @State .

Taille extérieure

La taille de la fenêtre du navigateur, y compris les barres d'outils/barres de défilement.

App.current.window.outerSize est un objet Size dans les valeurs width et height à l'intérieur.

Également disponible en tant que variable @State .

Filtrer

Objet spécial pour inspecter les propriétés de l'écran sur lequel la fenêtre actuelle est rendue. Disponible via App.current.window.screen .

La propriété la plus intéressante est généralement pixelRatio .

Histoire

Contient les URL visitées par l'utilisateur (dans une fenêtre de navigateur).

Disponible via App.current.window.history ou simplement History.shared .

Il est accessible en tant que variable @State , vous pouvez donc écouter ses modifications si nécessaire.

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

Elle est également accessible en simple variable

 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

Plus de détails sont disponibles sur MDN .

Emplacement

Contient des informations sur l'URL actuelle.

Disponible via App.current.window.location ou simplement Location.shared .

Il est accessible en tant que variable @State , vous pouvez donc écouter ses modifications si nécessaire.

C'est ainsi que fonctionne le routeur par exemple.

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


Il est également accessible en tant que simple variable

 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

Plus de détails sont disponibles sur MDN .

Navigateur

Contient des informations sur le navigateur.

Disponible via App.current.window.navigator ou simplement Navigator.shared

Les propriétés les plus intéressantes sont généralement language platform userAgent cookieEnabled .

Stockage local

Permet d'enregistrer des paires clé/valeur dans un navigateur Web. Stocke les données sans date d'expiration.

Disponible en tant que App.current.window.localStorage ou simplement 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()

Suivi des modifications

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

Suivi de la suppression de tous les éléments

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

Stockage de session

Permet d'enregistrer des paires clé/valeur dans un navigateur Web. Stocke les données pour une seule session.

Disponible en tant que App.current.window.sessionStorage ou simplement SessionStorage.shared .

L'API est absolument la même que dans LocalStorage décrit ci-dessus.

Document

Représente toute page Web chargée dans le navigateur et sert de point d'entrée dans le contenu de la page Web.

Disponible via 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]

Localisation

Localisation statique

La localisation classique est automatique et dépend de la langue du système utilisateur

Comment utiliser

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

Localisation dynamique

Si vous souhaitez modifier des chaînes localisées à l'écran à la volée (sans rechargement de page)

Vous pouvez changer la langue actuelle en appelant

 Localization.current = .es

Si vous avez enregistré la langue de l'utilisateur quelque part dans les cookies ou le stockage local, vous devez la définir au lancement de l'application

 Lifecycle.didFinishLaunching { Localization.current = .es }

Comment utiliser

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

Exemple avancé

 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 }

Aller chercher

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

Console

Simple print(“Hello world“) est équivalent à console.log('Hello world') en JavaScript


Les méthodes de la console sont également emballées avec amour ❤️

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

Aperçu en direct

Pour que l'aperçu en direct fonctionne, déclarez la classe WebPreview dans chaque fichier souhaité.

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


Code X

Veuillez lire les instructions sur la page du référentiel . C'est une solution délicate mais qui fonctionne parfaitement 😎


VSCode

Accédez à Extensions dans VSCode et recherchez Webber .

Une fois installé, appuyez sur Cmd+Shift+P (ou Ctrl+Shift+P sous Linux/Windows)

Recherchez et lancez Webber Live Preview .

Sur le côté droit, vous verrez la fenêtre d'aperçu en direct et elle s'actualise chaque fois que vous enregistrez le fichier contenant la classe WebPreview .

Accéder à JavaScript

Il est disponible via JavaScriptKit qui est la base du SwifWeb .

Lisez comment vous est dans le référentiel officiel .

Ressources

Vous pouvez ajouter css , js , png , jpg et toute autre ressource statique à l'intérieur du projet.

Mais pour les avoir disponibles pendant le débogage ou dans les fichiers de version finale, vous devez tous les déclarer dans le Package.swift comme ceci

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

Plus tard, vous pourrez y accéder, par exemple comme ceci Img().src(“/images/logo.png“)

Débogage

Lancez Webber de la manière suivante

webber serve juste à le lancer rapidement

webber serve -t pwa -s Service pour le lancer en mode PWA

Paramètres supplémentaires

-v ou --verbose pour afficher plus d'informations dans la console à des fins de débogage

-p 443 ou --port 443 pour démarrer le serveur webber sur le port 443 au lieu du 8888 par défaut

--browser chrome/safari pour ouvrir automatiquement le navigateur souhaité, par défaut il n'en ouvre aucun

--browser-self-signed nécessaire pour déboguer les service workers localement, sinon ils ne fonctionnent pas

--browser-incognito pour ouvrir une instance supplémentaire du navigateur en mode incognito, fonctionne uniquement avec chrome


Donc, pour créer votre application en mode débogage, ouvrez-la automatiquement dans Chrome et actualisez automatiquement le navigateur chaque fois que vous modifiez un fichier, lancez-le de cette façon.

pour SPA

webber serve --browser chrome

pour de vrais tests PWA

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


Chargement initial de l'application

Vous voudrez peut-être améliorer le processus de chargement initial.

Pour cela, ouvrez simplement le dossier .webber/entrypoint/dev à l'intérieur du projet et modifiez le fichier index.html .

Il contient le code HTML initial avec des écouteurs très utiles : WASMLoadingStarted WASMLoadingStartedWithoutProgress WASMLoadingProgress WASMLoadingError .

Vous êtes libre de modifier ce code en ce que vous voulez pour implémenter votre style personnalisé 🔥

Lorsque vous avez terminé la nouvelle implémentation, n'oubliez pas de l'enregistrer dans le dossier .webber/entrypoint/release

Libération du bâtiment

Exécutez simplement webber release ou webber release -t pwa -s Service for PWA.

Ensuite, récupérez les fichiers compilés du dossier .webber/release et téléchargez-les sur votre serveur.

Comment déployer

Vous pouvez télécharger vos fichiers sur n'importe quel hébergement statique.

L'hébergement doit fournir le type de contenu correct pour les fichiers wasm !

Oui, il est très important d'avoir le bon en-tête Content-Type: application/wasm pour les fichiers wasm , sinon malheureusement le navigateur ne pourra pas charger votre application WebAssembly.

Par exemple , GithubPages ne fournit pas le type de contenu correct pour les fichiers wasm , il est donc malheureusement impossible d'y héberger des sites WebAssembly.

Nginx

Si vous utilisez votre propre serveur avec nginx, ouvrez /etc/nginx/mime.types et vérifiez s'il contient application/wasm wasm; enregistrer. Si oui, vous êtes prêt à partir !

Conclusion

J'espère que je vous ai surpris aujourd'hui et que vous allez au moins essayer SwifWeb et commencer à l'utiliser au maximum pour votre prochain grand projet Web !


N'hésitez pas à contribuer à l'une des bibliothèques SwifWeb et également à les mettre toutes en vedette !


J'ai une excellente communauté SwiftStream dans Discord où vous pouvez trouver un support énorme, lire de petits tutoriels et être informé en premier des mises à jour à venir ! Ce serait cool de te voir avec nous !


Ce n'est que le tout début, alors restez à l'écoute pour plus d'articles sur SwifWeb !


Dis-le à tes amis!