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.
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.
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 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.
Vous devez avoir Swift installé, le moyen le plus simple de l'avoir :
Dans les autres cas, consultez les instructions d'installation sur le site officiel
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
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
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() } }
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.
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.
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)") } } }
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.
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
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 }
Vous pouvez utiliser des fichiers CSS traditionnels, mais vous avez également la nouvelle capacité magique d'utiliser une feuille de style écrite en Swift !
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")
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 { }
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
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
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 |
---|---|
| |
| |
| |
| |
| |
| |
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-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.
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 !
lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()
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)
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.
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> """ }
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"] }
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 :)
@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.
Il s'agit d'un wrapper de propriété qui informe tous les abonnés de ses 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)") }
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
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() {} }
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)
extension Class { var my: Class { "my" } }
Ensuite, utilisez-le comme Div().class(.my)
extension Id { var myId: Id { "my" } }
Ensuite, utilisez-le comme Div().id(.my)
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
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 })
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.
Identique au drapeau de premier plan, mais accessible via App.current.window.isOnline
Il détecte si un utilisateur a toujours accès à Internet.
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.
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
.
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
.
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
.
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 .
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 .
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
.
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") }
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.
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]
La localisation classique est automatique et dépend de la langue du système utilisateur
H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
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 }
H1(LString( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
H1(Localization.currentState.map { "Curent language: \($0.rawValue)" }) H2(LString(.en("English string"), .es("Hilo Español"))) Button("change lang").onClick { Localization.current = Localization.current.rawValue.contains("en") ? .es : .en }
import FetchAPI Fetch("https://jsonplaceholder.typicode.com/todos/1") { switch $0 { case .failure: break case .success(let response): print("response.code: \(response.status)") print("response.statusText: \(response.statusText)") print("response.ok: \(response.ok)") print("response.redirected: \(response.redirected)") print("response.headers: \(response.headers.dictionary)") struct Todo: Decodable { let id, userId: Int let title: String let completed: Bool } response.json(as: Todo.self) { switch $0 { case .failure(let error): break case .success(let todo): print("decoded todo: \(todo)") } } } }
import XMLHttpRequest XMLHttpRequest() .open(method: "GET", url: "https://jsonplaceholder.typicode.com/todos/1") .onAbort { print("XHR onAbort") }.onLoad { print("XHR onLoad") }.onError { print("XHR onError") }.onTimeout { print("XHR onTimeout") }.onProgress{ progress in print("XHR onProgress") }.onLoadEnd { print("XHR onLoadEnd") }.onLoadStart { print("XHR onLoadStart") }.onReadyStateChange { readyState in print("XHR onReadyStateChange") } .send()
import WebSocket let webSocket = WebSocket("wss://echo.websocket.org").onOpen { print("ws connected") }.onClose { (closeEvent: CloseEvent) in print("ws disconnected code: \(closeEvent.code) reason: \(closeEvent.reason)") }.onError { print("ws error") }.onMessage { message in print("ws message: \(message)") switch message.data { case .arrayBuffer(let arrayBuffer): break case .blob(let blob): break case .text(let text): break case .unknown(let jsValue): break } } Dispatch.asyncAfter(2) { // send as simple string webSocket.send("Hello from SwifWeb") // send as Blob webSocket.send(Blob("Hello from SwifWeb")) }
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()
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() } }
Veuillez lire les instructions sur la page du référentiel . C'est une solution délicate mais qui fonctionne parfaitement 😎
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 .
Il est disponible via JavaScriptKit qui est la base du SwifWeb .
Lisez comment vous est dans le référentiel officiel .
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“)
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
-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
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
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.
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.
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 !
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!