El mundo del desarrollo web es enorme y es fácil sentirse perdido en el flujo constante de nuevas tecnologías que surgen todos los días. La mayoría de estas nuevas tecnologías se construyen usando JavaScript o TypeScript. Sin embargo, en este artículo, le presentaré el desarrollo web utilizando Swift nativo, directamente dentro de su navegador, y estoy seguro de que lo impresionará. ¿Como es posible? Para que Swift funcione de forma nativa en la página web, primero debe compilarse en el código de bytes de WebAssembly y luego JavaScript podría cargar ese código en la página. Todo el proceso de compilación es un poco complicado, ya que necesitamos usar la cadena de herramientas especial y crear archivos de ayuda, por eso hay herramientas CLI de ayuda disponibles: Carton y Webber. ¿Está bien usar una cadena de herramientas de terceros? La comunidad ha realizado una gran cantidad de trabajo para que sea posible compilar Swift en WebAssembly parcheando la cadena de herramientas original de Swift. Actualizan la cadena de herramientas extrayendo automáticamente los cambios de la cadena de herramientas original todos los días y reparando su bifurcación si fallan las pruebas. Su objetivo es convertirse en parte de la cadena de herramientas oficial y esperan que suceda en un futuro cercano. de SwiftWasm ¿Cartón o Webber? está hecho por la comunidad SwiftWasm y se puede usar para proyectos Tokamak, que es un marco que le brinda la capacidad de escribir sitios web usando SwiftUI. Carton está hecho para proyectos SwifWeb. SwifWeb es diferente porque envuelve todos los estándares HTML y CSS, así como todas las API web. Webber Si bien es posible que prefiera escribir aplicaciones web con SwiftUI para mantener la coherencia del código, creo que este es un enfoque incorrecto porque el desarrollo web es intrínsecamente diferente y no se puede abordar de la misma manera que SwiftUI. Es por eso que creé SwifWeb, que le brinda la capacidad de usar todo el poder de HTML, CSS y API web directamente desde Swift, usando su hermosa sintaxis con autocompletado y documentación. Y creé la herramienta Webber porque Carton no puede compilar, depurar e implementar aplicaciones SwifWeb de la manera correcta porque no se creó para ello. Mi nombre es Mikhail Isaev y soy el autor de SwifWeb. En este artículo, le mostraré cómo comenzar a crear un sitio web utilizando SwifWeb. Herramientas necesarias Rápido Necesitas tener Swift instalado, la forma más fácil de tenerlo: en macOS es instalar Xcode en Linux o Windows (WSL2) es usar un script de swiftlang.xyz En otros casos, consulte las instrucciones de instalación en el sitio web oficial. CLI de Webber Creé Webber para ayudarlo a crear, depurar e implementar sus aplicaciones. En macOS es fácil de instalar con HomeBrew (instálelo desde su ) sitio web brew install swifweb/tap/webber para actualizar a la última versión más tarde simplemente ejecute brew upgrade webber En Ubuntu o Windows (Ubuntu en WSL2) clone y compile Webber manualmente 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 para actualizar a la última versión más tarde ejecute cd /opt/webber sudo git pull sudo swift build -c release La rama siempre contiene un código estable, así que siéntase libre de obtener actualizaciones de ella. principal Creando nuevo proyecto Abre la terminal y ejecuta webber new En el menú interactivo, elija o e ingrese el nombre del proyecto. pwa spa Cambie el directorio al proyecto recién creado y ejecute . webber serve Este comando compilará su proyecto en WebAssembly, empaquetará todos los archivos necesarios dentro de una carpeta especial y comenzará a servir su proyecto en todas las interfaces usando el puerto de manera predeterminada. .webber 8888 Argumentos adicionales para webber serve tipo de aplicación para aplicación web progresiva -t pwa para aplicación web única -t spa Nombre del objetivo del trabajador del servicio (generalmente llamado en el proyecto PWA) Service -s Service Nombre del destino de la aplicación ( por defecto) App -a App Imprimir más información en la consola -v Puerto para el servidor Webber (el predeterminado es ) 8888 -p 8080 Use para probar como SSL real (con la configuración SSL autofirmada permitida) -p 443 Nombre del navegador de destino para iniciar automáticamente en o --browser safari --browser chrome Instancia adicional de navegador con configuración SSL autofirmada permitida para depurar trabajadores de servicio --browser-self-signed Instancia adicional de navegador en modo incógnito --browser-incognito Solicitud La aplicación comienza en 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() } } Ciclo vital Funciona de manera similar a iOS: cuando la aplicación acaba de iniciarse didFinishLaunching cuando la aplicación va a morir willTerminate cuando la ventana va a estar inactiva willResignActive cuando la ventana está activa didBecomeActive cuando la ventana pasa al fondo didEnterBackground cuando la ventana pasa al primer plano willEnterForeground El método más útil aquí es porque es un excelente lugar para configurar la aplicación. ¡Ves que se siente realmente como una aplicación de iOS! 😀 didFinishLaunching Aquí contiene métodos de conveniencia útiles: app llamada para registrar el trabajador de servicio de PWA registerServiceWorker(“serviceName“) llamada para agregar script relativo o externo addScript(“path/to/script.js“) llamada para agregar un estilo relativo o externo addStylesheet(“path/to/style.css“) llame para agregar una fuente relativa o externa, opcionalmente establezca el tipo addFont(“path/to/font.woff”, type:) llamada para agregar icono, opcionalmente establecer tipo y color addIcon(“path/to/icon“, type:color:) Además, es el lugar para configurar bibliotecas adicionales como , , , etc. Autolayout Bootstrap Materialise Rutas El enrutamiento es necesario para mostrar la página adecuada en función de la URL actual. Para comprender cómo usar el enrutamiento, debe comprender qué URL es https://website.com/hello/world - aquí es el /hello/world camino Como has visto, al principio, en la clase App deberíamos declarar todas las rutas de nivel superior. Nivel superior significa que las páginas declaradas en estas rutas ocuparán todo el espacio de la ventana. Ok, entonces, por ejemplo, la ruta raíz se puede configurar de tres maneras Page("/") { IndexPage() } Page("") { IndexPage() } Page { IndexPage() } Creo que el último es el más hermoso 🙂 Las rutas de inicio de sesión o registro se pueden configurar así Page("login") { LoginPage() } Page("registration") { RegistrationPage() } Rutas relacionadas con parámetros Page("article/:id") { ArticlePage() } El en el ejemplo anterior es una parte dinámica de la ruta. Podemos recuperar este identificador en la clase para mostrar el artículo asociado a él. :id ArticlePage class ArticlePage: PageController { override func didLoad(with req: PageRequest) { if let articleId = req.parameters.get("id") { // Retrieve article here } } } Puede tener más de un parámetro en la ruta. Recuperarlos todos de la misma manera. Consultas La siguiente cosa interesante en la ruta es la , que también es muy fácil de usar. Por ejemplo, consideremos la ruta , que espera tener el de búsqueda y los parámetros de consulta . consulta /search text age https://website.com/search**?text=Alex&age=19** - la última parte es la consulta Simplemente declare la ruta de búsqueda Page("search") { SearchPage() } Y recuperar datos de consulta en la clase como esta 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)") } } } Cualquier cosa También puede usar para declarar una ruta que acepte cualquier cosa en la parte de la ruta específica como esta * Page("foo", "*", "bar") { SearchPage() } La ruta anterior aceptará cualquier cosa entre foo y bar, por ejemplo, /foo/aaa/bar, /foo/bbb/bar, etc. comodín Con el signo , puede establecer una ruta general especial que manejará todo lo que no haya coincidido con otras rutas en una ruta específica. ** Úselo para hacer una ruta global 404 Page("**") { NotFoundPage() } o para una ruta específica, por ejemplo, cuando no se encuentra el usuario Page("user", "**") { UserNotFoundPage() } Aclaremos situaciones con rutas declaradas arriba /user/1: si hay una ruta para /user/:id, devolverá . De lo contrario, caerá en… UserPage UserNotFoundPage /usuario/1/hola: si hay una ruta para /usuario/:id/hola, caerá en UserNotFoundPage /algo: si no hay una ruta para /algo, caerá en NotFoundPage Enrutamiento anidado Es posible que no queramos reemplazar todo el contenido de la página para la siguiente ruta, sino solo ciertos bloques. ¡Aquí es donde el resulta útil! FragmentRouter Consideremos que tenemos pestañas en la página . Cada pestaña es una subruta, y queremos reaccionar a los cambios en la subruta usando . /usuario FragmentRouter Declarar la ruta de nivel superior en la clase App Page("user") { UserPage() } Y declara en la clase 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() } } } } En el ejemplo anterior, maneja las subrutas y y las representa bajo la , por lo que la página nunca vuelve a cargar todo el contenido sino solo fragmentos específicos. FragmentRouter /usuario/perfil /usuario/amigos barra de navegación También se pueden declarar más de un fragmento con las mismas o diferentes subrutas y ¡todos funcionarán juntos como magia! Por cierto, es un y puede configurarlo llamando FragmentRouter Div FragmentRouter(self) .configure { div in // do anything you want with the div } hojas de estilo Puede usar archivos CSS tradicionales, ¡pero también tiene la nueva y mágica habilidad de usar una hoja de estilo escrita en Swift! Lo esencial Para declarar una regla CSS usando Swift tenemos el objeto . Rule Se puede construir declarativamente llamando a sus métodos Rule(...selector...) .alignContent(.baseline) .color(.red) // or rgba/hex color .margin(v: 0, h: .auto) o una forma similar a SwiftUI usando @resultBuilder Rule(...selector...) { AlignContent(.baseline) Color(.red) Margin(v: 0, h: .auto) } Ambas formas son iguales, sin embargo, prefiero la primera debido al autocompletado justo después de escribir 😀 . Todos los métodos CSS descritos en MDN están disponibles. ¡Más que eso, maneja los prefijos del navegador automáticamente! Sin embargo, puede establecer una propiedad personalizada de esta manera en algún caso específico Rule(...selector...) .custom("customKey", "customValue") Selector Para establecer qué elementos debe afectar la regla, tenemos que establecer un selector. Veo el selector como la consulta en la base de datos, pero partes de esa consulta del selector las llamo punteros. La forma más fácil de construir un puntero es inicializarlo usando la cadena sin formato Pointer("a") Pero la forma correcta y rápida es construirlo llamando en la etiqueta HTML necesaria como esta .pointer H1.pointer // h1 A.pointer // a Pointer.any // * Class("myClass").pointer // .myClass Id("myId").pointer // #myId Se trata de punteros básicos, pero también tienen modificadores como , etc. :hover :first :first-child H1.pointer.first // h1:first H1.pointer.firstChild // h1:first-child H1.pointer.hover // h1:hover Puede declarar cualquier modificador existente, todos están disponibles. Si falta algo, ¡no dude en hacer una extensión para agregarlo! Y no olvide enviar una solicitud de extracción en github para agregarla para todos. También puede concatenar punteros 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 Cómo utilizar el selector en la Regla Rule(Pointer("a")) // or Rule(A.pointer) Cómo usar más de un selector en la Regla Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer)) Produce el siguiente código CSS a, h1#myId, div > p { } Reactividad Declaremos estilos oscuros y claros para nuestra aplicación, y luego, podremos cambiar fácilmente entre ellos. 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 }) } } y pueden declararse en archivos separados o, por ejemplo, en 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) } } Y luego, en algún lugar de la interfaz de usuario de alguna página, simplemente llame App.current.theme = .light // to switch to light theme // or App.current.theme = .dark // to switch to dark theme ¡Y activará o desactivará las hojas de estilo relacionadas! ¿No es genial? 😎 Pero puede decir que describir estilos en Swift en lugar de CSS es más difícil, entonces, ¿cuál es el punto? ¡El punto principal es la reactividad! ¡Podemos usar @State con propiedades CSS y cambiar valores sobre la marcha! Solo eche un vistazo, podemos crear una clase con alguna propiedad reactiva y cambiarla en cualquier momento en tiempo de ejecución, ¡así que cualquier elemento en la pantalla que use esa clase se actualizará! ¡Es mucho más efectivo que cambiar de clase para muchos elementos! 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) } } Más tarde desde cualquier lugar en el código solo llame App.current.reactiveColor = .yellow // or any color you want y actualizará el color en la hoja de estilo y en todos los elementos que la usan 😜 Además, es posible agregar CSS sin procesar en una hoja de estilo 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; } """ } } puede mezclar cadenas CSS sin procesar tantas veces como sea necesario Paginas El enrutador está mostrando páginas en cada ruta. Page es cualquier clase heredada de . PageController tiene métodos de ciclo de vida como , métodos de interfaz de usuario y , y variable contenedora de propiedades para elementos HTML. PageController willLoad didLoad willUnload didUnload buildUI body Técnicamente, es solo un Div y puede establecer cualquiera de sus propiedades en el método . 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 su página es muy pequeña, puede declararla incluso de esta manera breve 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 } ¿No es hermoso y lacónico? 🥲 Métodos de conveniencia de bonificación - método JS directo alert(message: String) alert - cambiar la ruta de la URL changePath(to: String) Elementos HTML ¡Finalmente, le diré cómo (!) construir y usar elementos HTML! Todos los elementos HTML con sus atributos están disponibles en Swift, la lista completa está, por ejemplo, en . MDN Solo una breve lista de ejemplo de elementos HTML: Código SwifWeb código 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”> Como puede ver, es muy fácil acceder a cualquier etiqueta HTML en Swift porque todas están representadas con el mismo nombre, excepto las entradas. Esto se debe a que los diferentes tipos de entrada tienen diferentes métodos y no quería mezclarlos. simple Div Div() podemos acceder a todos sus atributos y propiedades de estilo como este 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;"> Subclasificación Subclase el elemento HTML para predefinir su estilo, o para crear un elemento compuesto con muchos elementos secundarios predefinidos y algunos métodos convenientes disponibles fuera, o para lograr eventos de ciclo de vida como y . didAddToDOM didRemoveFromDOM Vamos a crear un elemento que es solo un pero con una clase predefinida 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) } } Es muy importante llamar a los supermétodos al crear subclases. Sin él, puede experimentar un comportamiento inesperado. Agregar a DOM El elemento se puede agregar al DOM de o de inmediato o más tarde. PageController al elemento HTML De inmediato Div { H1("Title") P("Subtitle") Div { Ul { Li("One") Li("Two") } } } O más tarde usando lazy var lazy var myDiv1 = Div() lazy var myDiv2 = Div() Div { myDiv1 myDiv2 } ¡Entonces puede declarar un por adelantado y agregarlo al DOM en cualquier momento posterior! elemento HTML Eliminación de DOM lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove() Acceder al elemento principal Cualquier elemento HTML tiene una propiedad de supervisión opcional que da acceso a su padre si se agrega al DOM Div().superview?.backgroundColor(.red) condiciones si/si no A menudo necesitamos mostrar elementos solo en ciertas condiciones, así que usemos para eso. 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") } } Pero no es reactivo. Si intenta establecer en , no sucede nada. showDiv2 false ejemplo reactivo 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 } ¿ Por qué deberíamos usar $showDiv2.map {…} ? En especie: porque no es SwiftUI. En absoluto. Lea más sobre @State a continuación. HTML sin procesar También es posible que deba agregar HTML sin procesar en la página o el elemento HTML y es fácilmente posible Div { """ <a href="https://google.com">Go to Google</a> """ } Para cada Ejemplo estático 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) } } Ejemplo dinámico @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 Igual que en los ejemplos anteriores, pero también está 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 } } Puede usar en bucles para calcular algún valor una sola vez como un valor en el siguiente ejemplo 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) } } También puede tomar la función como un argumento. BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 } también está disponible para elementos HTML :) BuilderFunction Reactividad con @State es lo más deseable hoy en día para declarativa. @State la programación Como te dije anteriormente: no es SwiftUI, por lo que no hay una máquina de estado global que rastree y vuelva a dibujar todo. Y los elementos HTML no son estructuras temporales sino clases, por lo que son objetos reales y tienes acceso directo a ellos. Es mucho mejor y flexible, tienes todo el control. ¿Qué hay debajo del capó? Es un contenedor de propiedades que notifica a todos los suscriptores sobre sus cambios. ¿Cómo suscribirse a los cambios? 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)") } ¿Cómo pueden reaccionar los elementos HTML a los cambios? ejemplo de texto sencillo @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 Ejemplo de números simples @State var height = 20.px Div().height($height) // whenever height var changes it updates height of the Div Ejemplo booleano simple @State var hidden = false Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div Ejemplo de mapeo @State var isItCold = true H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" }) Mapeo de dos estados @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 }) Mapeo de más de dos estados @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 }) Todas las propiedades HTML y CSS pueden manejar valores @State Extensiones Extender elementos HTML Puede agregar algunos métodos convenientes a elementos concretos como Div extension Div { func makeItBeautiful() {} } O grupos de elementos si conoce su principal. class Hay pocas clases para padres. : es para elementos que se pueden inicializar con una cadena, como , , etc. BaseActiveStringElement a h1 : es para todos los elementos que pueden tener contenido dentro, como , , etc. BaseContentElement div ul - es para todos los elementos BaseElement Entonces la extensión para todos los elementos se puede escribir de esta manera extension BaseElement { func doSomething() {} } declarar colores La clase es responsable de los colores. Tiene colores HTML predefinidos, pero puedes tener los tuyos propios. de color 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) } Luego utilícelo como H1(“Text“).color(.myColor1) declarar clases extension Class { var my: Class { "my" } } Luego úsalo como Div().class(.my) Declarar identificaciones extension Id { var myId: Id { "my" } } Luego úsalo como Div().id(.my) API web Ventana El objeto está completamente envuelto y accesible a través de la variable . window App.current.window La referencia completa está disponible en . MDN Hagamos el breve resumen a continuación. Bandera de primer plano Puede escucharlo en en o directamente de esta manera Lifecycle App.swift App.current.window.$isInForeground.listen { isInForeground in // foreground flag changed } o simplemente léelo en cualquier momento en cualquier lugar if App.current.window.isInForeground { // do somethign } o reaccionar con el elemento HTML Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white }) Bandera activa Es lo mismo que la bandera de primer plano, pero accesible a través de App.current.window.isActive Detecta si un usuario aún está interactuando dentro de la ventana. Estado en línea Igual que la bandera de primer plano, pero accesible a través de App.current.window.isOnline Detecta si un usuario todavía tiene acceso a Internet. Estado del modo oscuro Igual que la bandera de primer plano, pero accesible a través de App.current.window.isDark Detecta si el navegador o sistema operativo de un usuario está en modo oscuro. Tamaño interior El tamaño del área de contenido de la ventana (ventana gráfica), incluidas las barras de desplazamiento es un objeto dentro de los valores de y . App.current.window.innerSize de tamaño width height También disponible como variable . @State Tamaño exterior El tamaño de la ventana del navegador, incluidas las barras de herramientas/barras de desplazamiento. es un objeto dentro de los valores y . App.current.window.outerSize de tamaño width height También disponible como variable . @State Pantalla Objeto especial para inspeccionar las propiedades de la pantalla en la que se representa la ventana actual. Disponible a través de . App.current.window.screen La propiedad más interesante suele ser . pixelRatio Historia Contiene las URL visitadas por el usuario (dentro de una ventana del navegador). Disponible a través de o simplemente . App.current.window.history History.shared Es accesible como variable , por lo que puede escuchar sus cambios si es necesario. @State App.current.window.$history.listen { history in // read history properties } También es accesible como variable simple. 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 Más detalles están disponibles en . MDN Ubicación Contiene información sobre la URL actual. Disponible a través de o simplemente . App.current.window.location Location.shared Es accesible como variable , por lo que puede escuchar sus cambios si es necesario. @State Así es como funciona el enrutador, por ejemplo. App.current.window.$location.listen { location in // read location properties } También es accesible como una variable simple. 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 Más detalles están disponibles en . MDN Navegador Contiene información sobre el navegador. Disponible a través de o simplemente App.current.window.navigator Navigator.shared Las propiedades más interesantes suelen ser . language platform userAgent cookieEnabled Almacenamiento local Permite guardar pares clave/valor en un navegador web. Almacena datos sin fecha de caducidad. Disponible como o simplemente . 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() Seguimiento de cambios LocalStorage.onChange { key, oldValue, newValue in print("LocalStorage: key \(key) has been updated") } Seguimiento de la eliminación de todos los elementos LocalStorage.onClear { print("LocalStorage: all items has been removed") } SesiónAlmacenamiento Permite guardar pares clave/valor en un navegador web. Almacena datos para una sola sesión. Disponible como o simplemente . App.current.window.sessionStorage SessionStorage.shared La API es absolutamente la misma que en descrita anteriormente. LocalStorage Documento Representa cualquier página web cargada en el navegador y sirve como punto de entrada al contenido de la página web. Disponible a través de . 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] Localización localización estática La localización clásica es automática y depende del idioma del sistema del usuario Cómo utilizar H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは"))) Localización dinámica Si desea cambiar las cadenas localizadas en la pantalla sobre la marcha (sin recargar la página) Puede cambiar el idioma actual llamando Localization.current = .es Si guardó el idioma del usuario en algún lugar de las cookies o el almacenamiento local, debe configurarlo al iniciar la aplicación Lifecycle.didFinishLaunching { Localization.current = .es } Cómo utilizar H1(LString( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは"))) ejemplo avanzado 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 } Buscar 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)") } } } } XMLHttpSolicitud 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")) } Consola es equivalente a en JavaScript print(“Hello world“) console.log('Hello world') Los métodos también están envueltos con amor ❤️ de consola Console.dir(...) Console.error(...) Console.warning(...) Console.clear() Vista previa en vivo Para hacer que la vista previa en vivo funcione, declare la clase WebPreview en cada archivo que desee. 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() } } código x Lea las instrucciones en . Es una solución complicada pero totalmente funcional 😎 la página del repositorio Código VSC Vaya a dentro y busque . Extensiones de VSCode Webber Una vez que esté instalado, presione (o en Linux/Windows) Cmd+Shift+P Ctrl+Shift+P Busque y ejecute . Webber Live Preview En el lado derecho, verá la ventana de vista previa en vivo y se actualiza cada vez que guarda el archivo que contiene la clase . WebPreview Acceso a JavaScript Está disponible a través de , que es la base de . JavaScriptKit SwifWeb Lea cómo se encuentra en . el repositorio oficial Recursos Puede agregar , , , y cualquier otro recurso estático dentro del proyecto. css js png jpg Pero para tenerlos disponibles o en los archivos final, debe declararlos todos en de esta manera durante la depuración de versión 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") ]), Más tarde podrá acceder a ellos, por ejemplo, así Img().src(“/images/logo.png“) depuración Inicie de la siguiente manera Webber solo para lanzarlo rápidamente. webber serve para iniciarlo en modo PWA webber serve -t pwa -s Service Parámetros adicionales o para mostrar más información en la consola con fines de depuración -v --verbose o para iniciar el servidor webber en el puerto 443 en lugar del predeterminado 8888 -p 443 --port 443 para abrir automáticamente el navegador deseado, por defecto no abre ninguno --browser chrome/safari necesario para depurar los trabajadores del servicio localmente; de lo contrario, no funcionan --browser-self-signed para abrir una instancia adicional del navegador en modo incógnito, solo funciona con Chrome --browser-incognito Entonces, para crear su aplicación en modo de depuración, ábrala automáticamente en Chrome y actualice el navegador automáticamente cada vez que cambie cualquier archivo, ejecútelo de esta manera. para BALNEARIO webber serve --browser chrome para pruebas reales de PWA webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito Carga inicial de la aplicación Es posible que desee mejorar el proceso de carga inicial. Para eso, simplemente abra la carpeta dentro del proyecto y edite el archivo . .webber/entrypoint/dev index.html Contiene código HTML inicial con oyentes muy útiles: . WASMLoadingStarted WASMLoadingStartedWithoutProgress WASMLoadingProgress WASMLoadingError Eres libre de editar ese código a lo que quieras para implementar tu estilo personalizado 🔥 Cuando termine la nueva implementación, no olvide guardarla en la carpeta .webber/entrypoint/release Lanzamiento de edificio Simplemente ejecute o para PWA. webber release webber release -t pwa -s Service Luego tome los archivos compilados de la carpeta y cárguelos en su servidor. .webber/release Cómo implementar Puedes subir tus archivos a cualquier hosting estático. ¡El alojamiento debe proporcionar el tipo de contenido correcto para los archivos ! wasm Sí, es muy importante tener el encabezado correcto para archivos ; de lo contrario, desafortunadamente, el navegador no podrá cargar su aplicación WebAssembly. Content-Type: application/wasm wasm Por ejemplo, no proporciona el tipo de contenido correcto para los archivos , por lo que lamentablemente es imposible alojar sitios WebAssembly en él. GithubPages wasm Nginx Si usa su propio servidor con nginx, abra y verifique si contiene registro. Si es así, ¡entonces estás listo para irte! /etc/nginx/mime.types application/wasm wasm; Conclusión ¡Espero haberte sorprendido hoy y que al menos pruebes SwifWeb y al máximo comiences a usarlo para tu próximo gran proyecto web! ¡Siéntete libre de contribuir a cualquiera de y también de protagonizarlas todas! las bibliotecas de SwifWeb ¡Tengo una gran donde puedes encontrar un gran apoyo, leer pequeños tutoriales y ser notificado primero sobre las próximas actualizaciones! ¡Sería genial verte con nosotros! comunidad de SwiftStream en Discord Es solo el comienzo, ¡así que esté atento a más artículos sobre SwifWeb! ¡Dile a tus amigos!