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é 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. SwiftWasm Carton ou Webber ? 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. Carton 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. Webber 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 contient toujours du code stable, alors n'hésitez pas à en extraire les mises à jour principale Création d'un nouveau projet Ouvrez le terminal et exécutez webber new Dans le menu interactif, choisissez ou et entrez le nom du projet. pwa spa 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 spécial et commencera à servir votre projet sur toutes les interfaces en utilisant le port par défaut. .webber 8888 Arguments supplémentaires pour webber serve Type d'application pour l'application Web progressive -t pwa pour une seule application Web -t spa Nom de la cible du service worker (généralement nommé dans le projet PWA) Service -s Service Nom de la cible de l'application ( par défaut) App -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 pour tester comme le vrai SSL (avec le paramètre SSL auto-signé autorisé) -p 443 Nom du navigateur de destination dans lequel se lancer automatiquement ou --browser safari --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 : lorsque l'application vient de démarrer didFinishLaunching lorsque l'application va mourir willTerminate lorsque la fenêtre va être inactive willResignActive lorsque la fenêtre est active didBecomeActive lorsque la fenêtre passe en arrière-plan didEnterBackground lorsque la fenêtre passe au premier plan willEnterForeground La méthode la plus utile ici est car c'est un endroit idéal pour configurer l'application. Vous voyez, cela ressemble vraiment à une application iOS ! 😀 didFinishLaunching Ici, contient des méthodes de commodité utiles : app appel pour enregistrer le service worker PWA registerServiceWorker(“serviceName“) appel pour ajouter un script relatif ou externe addScript(“path/to/script.js“) appel pour ajouter un style relatif ou externe addStylesheet(“path/to/style.css“) appel pour ajouter une police relative ou externe, éventuellement définir le type addFont(“path/to/font.woff”, type:) appel pour ajouter une icône, éventuellement définir le type et la couleur addIcon(“path/to/icon“, type:color:) C'est aussi l'endroit où configurer des bibliothèques supplémentaires comme , , , etc. Autolayout Bootstrap Materialize 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 est le /hello/world 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 dans l'exemple ci-dessus est une partie dynamique de la route. On peut récupérer cet identifiant dans la classe pour afficher l'article qui lui est associé. :id ArticlePage 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 , qui est également très facile à utiliser. Par exemple, considérons la route , qui s'attend à avoir le de recherche et les paramètres de requête . requête /search text 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 comme ceci SearchPage class SearchPage: PageController { struct Query: Decodable { let text: String? let age: Int? } override func didLoad(with req: PageRequest) { do { let query = try req.query.decode(Query.self) // use optional query.text and query.age // to query search results } catch { print("Can't decode query: \(error)") } } } 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 . Sinon, il tombera dans… UserPage 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 devient utile ! FragmentRouter Considérons que nous avons des onglets sur la page . Chaque onglet est un sous-itinéraire, et nous voulons réagir aux changements dans le sous-itinéraire en utilisant le . /user FragmentRouter Déclarez la route de niveau supérieur dans la classe App Page("user") { UserPage() } Et déclarez dans la classe FragmentRouter 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, gère les sous-routes et et les restitue sous la , de sorte que la page ne recharge jamais tout le contenu mais uniquement des fragments spécifiques. FragmentRouter /user/profile /user/friends barre de navigation 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 est un et vous pouvez le configurer en appelant FragmentRouter Div 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 à la balise HTML nécessaire comme celle-ci .pointer 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 etc. :hover :first :first-child 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 }) } } et peuvent être déclarés dans des fichiers séparés ou par exemple dans l'App.swift LightStyle DarkStyle class LightStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.white) Rule(H1.pointer).color(.black) } } class DarkStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.black) Rule(H1.pointer).color(.white) } } 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 a des méthodes de cycle de vie comme , les méthodes d'interface utilisateur et , et une variable wrapper de propriété pour les éléments HTML. PageController willLoad didLoad willUnload didUnload buildUI body Techniquement, n'est qu'un Div et vous pouvez définir toutes ses propriétés dans la méthode . PageController buildUI class IndexPage: PageController { // MARK: - Lifecycle override func willLoad(with req: PageRequest) { super.willLoad(with: req) } override func didLoad(with req: PageRequest) { super.didLoad(with: req) // set page title and metaDescription self.title = "My Index Page" self.metaDescription = "..." // also parse query and hash here } override func willUnload() { super.willUnload() } override func didUnload() { super.didUnload() } // MARK: - UI override func buildUI() { super.buildUI() // access any properties of the page's div here // eg self.backgroundcolor(.lightGrey) // optionally call body method here to add child HTML elements body { P("Hello world") } // or alternatively self.appendChild(P("Hello world")) } // the best place to declare any child HTML elements @DOM override var body: DOM.Content { H1("Hello world") P("Text under title") Button("Click me") { self.alert("Click!") print("button clicked") } } } 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 - méthode JS directe alert(message: String) alert - changement de chemin d'URL changePath(to: String) É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. simple Div 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 et . didAddToDOM didRemoveFromDOM Créons un élément qui est juste un mais avec une classe prédéfinie Divider Div .divider public class Divider: Div { // it is very important to override the name // because otherwise it will be <divider> in HTML open class override var name: String { "\(Div.self)".lowercased() } required public init() { super.init() } // this method executes immediately after any init method public override func postInit() { super.postInit() // here we are adding `divider` class self.class(.divider) } } 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 ou immédiatement ou ultérieurement. PageController à l'élément HTML 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'avance et l'ajouter au DOM à tout moment par la suite ! élément HTML 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 pour cela 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") } } Mais ce n'est pas réactif. Si vous essayez de définir sur , rien ne se passe. showDiv2 false 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 est également disponible BuilderFunction Stylesheet { ForEach(1...20) { index in CSSRule(Div.pointer.nthChild("\(index)")) // set rule properties depending on index } 20.times { index in CSSRule(Div.pointer.nthChild("\(index)")) // set rule properties depending on index } } Vous pouvez utiliser dans les boucles pour calculer une valeur une seule fois comme une valeur dans l'exemple suivant BuilderFunction ForEach 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) } } Il peut également prendre la fonction comme argument BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 } est également disponible pour les éléments HTML :) BuilderFunction Réactivité avec @State est la chose la plus souhaitable de nos jours pour déclarative. @State la programmation 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 parente. class Il y a peu de classes parentales. - est pour les éléments qui peuvent être initialisés avec une chaîne, comme , , etc. BaseActiveStringElement a h1 - est pour tous les éléments qui peuvent avoir du contenu à l'intérieur, comme , , etc. BaseContentElement div ul - est pour tous les éléments BaseElement 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 est responsable des couleurs. Il a des couleurs HTML prédéfinies, mais vous pouvez avoir les vôtres de couleur 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 est entièrement encapsulé et accessible via la variable . window 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 dans l' ou directement de cette façon Lifecycle App.swift 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 est un objet dans les valeurs et à l'intérieur. App.current.window.innerSize Size width height É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. est un objet dans les valeurs et à l'intérieur. App.current.window.outerSize Size width height É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 ou simplement . App.current.window.history History.shared Il est accessible en tant que variable , vous pouvez donc écouter ses modifications si nécessaire. @State 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 ou simplement . App.current.window.location Location.shared Il est accessible en tant que variable , vous pouvez donc écouter ses modifications si nécessaire. @State 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 ou simplement App.current.window.navigator 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 ou simplement . App.current.window.localStorage LocalStorage.shared // You can save any value that can be represented in JavaScript LocalStorage.shared.set("key", "value") // saves String LocalStorage.shared.set("key", 123) // saves Int LocalStorage.shared.set("key", 0.8) // saves Double LocalStorage.shared.set("key", ["key":"value"]) // saves Dictionary LocalStorage.shared.set("key", ["v1", "v2"]) // saves Array // Getting values back LocalStorage.shared.string(forKey: "key") // returns String? LocalStorage.shared.integer(forKey: "key") // returns Int? LocalStorage.shared.string(forKey: "key") // returns String? LocalStorage.shared.value(forKey: "key") // returns JSValue? // Removing item LocalStorage.shared.removeItem(forKey: "key") // Removing all items LocalStorage.shared.clear() 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 ou simplement . App.current.window.sessionStorage SessionStorage.shared L'API est absolument la même que dans décrit ci-dessus. LocalStorage 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 est équivalent à en JavaScript print(“Hello world“) console.log('Hello world') Les méthodes sont également emballées avec amour ❤️ de la console 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 . C'est une solution délicate mais qui fonctionne parfaitement 😎 la page du référentiel VSCode Accédez à dans et recherchez . Extensions VSCode Webber Une fois installé, appuyez sur (ou sous Linux/Windows) Cmd+Shift+P Ctrl+Shift+P 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 qui est la base du . JavaScriptKit SwifWeb Lisez comment vous est dans . le référentiel officiel Ressources Vous pouvez ajouter , , , et toute autre ressource statique à l'intérieur du projet. css js png jpg Mais pour les avoir disponibles ou dans les fichiers finale, vous devez tous les déclarer dans le comme ceci pendant le débogage de version Package.swift .executableTarget(name: "App", dependencies: [ .product(name: "Web", package: "web") ], resources: [ .copy("css/*.css"), .copy("css"), .copy("images/*.jpg"), .copy("images/*.png"), .copy("images/*.svg"), .copy("images"), .copy("fonts/*.woff2"), .copy("fonts") ]), Plus tard, vous pourrez y accéder, par exemple comme ceci Img().src(“/images/logo.png“) Débogage Lancez de la manière suivante Webber juste à le lancer rapidement webber serve pour le lancer en mode PWA webber serve -t pwa -s Service Paramètres supplémentaires ou pour afficher plus d'informations dans la console à des fins de débogage -v --verbose ou pour démarrer le serveur webber sur le port 443 au lieu du 8888 par défaut -p 443 --port 443 pour ouvrir automatiquement le navigateur souhaité, par défaut il n'en ouvre aucun --browser chrome/safari nécessaire pour déboguer les service workers localement, sinon ils ne fonctionnent pas --browser-self-signed pour ouvrir une instance supplémentaire du navigateur en mode incognito, fonctionne uniquement avec chrome --browser-incognito 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 à l'intérieur du projet et modifiez le fichier . .webber/entrypoint/dev 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 ou for PWA. webber release webber release -t pwa -s Service Ensuite, récupérez les fichiers compilés du dossier et téléchargez-les sur votre serveur. .webber/release 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 pour les fichiers , sinon malheureusement le navigateur ne pourra pas charger votre application WebAssembly. Content-Type: application/wasm wasm Par exemple ne fournit pas le type de contenu correct pour les fichiers , il est donc malheureusement impossible d'y héberger des sites WebAssembly. , GithubPages wasm Nginx Si vous utilisez votre propre serveur avec nginx, ouvrez et vérifiez s'il contient enregistrer. Si oui, vous êtes prêt à partir ! /etc/nginx/mime.types application/wasm wasm; 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 et également à les mettre toutes en vedette ! bibliothèques SwifWeb J'ai une excellente 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 ! communauté SwiftStream dans Discord Ce n'est que le tout début, alors restez à l'écoute pour plus d'articles sur SwifWeb ! Dis-le à tes amis!