paint-brush
Cómo usar Swift para el desarrollo webpor@imike
12,387 lecturas
12,387 lecturas

Cómo usar Swift para el desarrollo web

por Mikhail Isaev33m2023/03/20
Read on Terminal Reader

Demasiado Largo; Para Leer

SwifWeb es un marco que le brinda la capacidad de escribir sitios web utilizando SwiftUI. Envuelve todos los estándares HTML y CSS, así como todas las API web. En este artículo, le mostraré cómo comenzar a crear un sitio web utilizando el marco SwifWeb.
featured image - Cómo usar Swift para el desarrollo web
Mikhail Isaev HackerNoon profile picture
0-item
1-item

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 de SwiftWasm 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.

¿Cartón o Webber?

Carton 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.


Webber está hecho para proyectos SwifWeb. SwifWeb es diferente porque envuelve todos los estándares HTML y CSS, así como todas las API web.


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 principal siempre contiene un código estable, así que siéntase libre de obtener actualizaciones de ella.

Creando nuevo proyecto

Abre la terminal y ejecuta

 webber new

En el menú interactivo, elija pwa o spa e ingrese el nombre del proyecto.


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 .webber y comenzará a servir su proyecto en todas las interfaces usando el puerto 8888 de manera predeterminada.


Argumentos adicionales para webber serve

  • tipo de aplicación

    -t pwa para aplicación web progresiva

    -t spa para aplicación web única

  • Nombre del objetivo del trabajador del servicio (generalmente llamado Service en el proyecto PWA)

    -s Service

  • Nombre del destino de la aplicación ( App por defecto)

    -a App

  • Imprimir más información en la consola

    -v

  • Puerto para el servidor Webber (el predeterminado es 8888 )

    -p 8080

Use -p 443 para probar como SSL real (con la configuración SSL autofirmada permitida)

  • Nombre del navegador de destino para iniciar automáticamente en

    --browser safari o --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:

didFinishLaunching cuando la aplicación acaba de iniciarse

willTerminate cuando la aplicación va a morir

willResignActive cuando la ventana va a estar inactiva

didBecomeActive cuando la ventana está activa

didEnterBackground cuando la ventana pasa al fondo

willEnterForeground cuando la ventana pasa al primer plano


El método más útil aquí es didFinishLaunching porque es un excelente lugar para configurar la aplicación. ¡Ves que se siente realmente como una aplicación de iOS! 😀


Aquí app contiene métodos de conveniencia útiles:

registerServiceWorker(“serviceName“) llamada para registrar el trabajador de servicio de PWA

addScript(“path/to/script.js“) llamada para agregar script relativo o externo

addStylesheet(“path/to/style.css“) llamada para agregar un estilo relativo o externo

addFont(“path/to/font.woff”, type:) llame para agregar una fuente relativa o externa, opcionalmente establezca el tipo

addIcon(“path/to/icon“, type:color:) llamada para agregar icono, opcionalmente establecer tipo y color


Además, es el lugar para configurar bibliotecas adicionales como Autolayout , Bootstrap , Materialise , etc.

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í /hello/world es el 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 :id en el ejemplo anterior es una parte dinámica de la ruta. Podemos recuperar este identificador en la clase ArticlePage para mostrar el artículo asociado a él.

 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 consulta , que también es muy fácil de usar. Por ejemplo, consideremos la ruta /search , que espera tener el text de búsqueda y los parámetros de consulta 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 SearchPage como esta

 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á UserPage . De lo contrario, caerá en…


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 FragmentRouter resulta útil!


Consideremos que tenemos pestañas en la página /usuario . Cada pestaña es una subruta, y queremos reaccionar a los cambios en la subruta usando FragmentRouter .


Declarar la ruta de nivel superior en la clase App

 Page("user") { UserPage() }

Y declara FragmentRouter en la clase 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, FragmentRouter maneja las subrutas /usuario/perfil y /usuario/amigos y las representa bajo la barra de navegación , por lo que la página nunca vuelve a cargar todo el contenido sino solo fragmentos específicos.


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, FragmentRouter es un Div y puede configurarlo llamando

 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 .pointer en la etiqueta HTML necesaria como esta

 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 :hover :first :first-child , etc.

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


LightStyle y DarkStyle pueden declararse en archivos separados o, por ejemplo, en 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) } }


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 .


PageController tiene métodos de ciclo de vida como willLoad didLoad willUnload didUnload , métodos de interfaz de usuario buildUI y body , y variable contenedora de propiedades para elementos HTML.

Técnicamente, PageController es solo un Div y puede establecer cualquiera de sus propiedades en el método 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

alert(message: String) - método alert JS directo

changePath(to: String) - cambiar la ruta de la URL

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.


Div simple

 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 didAddToDOM y didRemoveFromDOM .

Vamos a crear un elemento Divider que es solo un Div pero con una clase .divider predefinida

 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 PageController o al elemento HTML de inmediato o más tarde.


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 elemento HTML por adelantado y agregarlo al DOM en cualquier momento posterior!

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 if/else para eso.

 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 showDiv2 en false , no sucede nada.


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 BuilderFunction en bucles ForEach para calcular algún valor una sola vez como un valor delay en el siguiente ejemplo

 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 }

BuilderFunction también está disponible para elementos HTML :)

Reactividad con @State

@State es lo más deseable hoy en día para la programación declarativa.


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 class principal.


Hay pocas clases para padres.

BaseActiveStringElement : es para elementos que se pueden inicializar con una cadena, como a , h1 , etc.

BaseContentElement : es para todos los elementos que pueden tener contenido dentro, como div , ul , etc.

BaseElement - es para todos los elementos


Entonces la extensión para todos los elementos se puede escribir de esta manera

 extension BaseElement { func doSomething() {} }

declarar colores

La clase de color es responsable de los colores. Tiene colores HTML predefinidos, pero puedes tener los tuyos propios.

 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 window está completamente envuelto y accesible a través de la variable App.current.window .

La referencia completa está disponible en MDN .


Hagamos el breve resumen a continuación.

Bandera de primer plano

Puede escucharlo en Lifecycle en App.swift o directamente de esta manera

 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

App.current.window.innerSize es un objeto de tamaño dentro de los valores de width y 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.

App.current.window.outerSize es un objeto de tamaño dentro de los valores width y 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 App.current.window.history o simplemente History.shared .

Es accesible como variable @State , por lo que puede escuchar sus cambios si es necesario.

 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 App.current.window.location o simplemente Location.shared .

Es accesible como variable @State , por lo que puede escuchar sus cambios si es necesario.

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 App.current.window.navigator o simplemente 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 App.current.window.localStorage o simplemente 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 App.current.window.sessionStorage o simplemente SessionStorage.shared .

La API es absolutamente la misma que en LocalStorage descrita anteriormente.

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

print(“Hello world“) es equivalente a console.log('Hello world') en JavaScript


Los métodos de consola también están envueltos con amor ❤️

 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 la página del repositorio . Es una solución complicada pero totalmente funcional 😎


Código VSC

Vaya a Extensiones dentro de VSCode y busque Webber .

Una vez que esté instalado, presione Cmd+Shift+P (o Ctrl+Shift+P en Linux/Windows)

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 JavaScriptKit , que es la base de SwifWeb .

Lea cómo se encuentra en el repositorio oficial .

Recursos

Puede agregar css , js , png , jpg y cualquier otro recurso estático dentro del proyecto.

Pero para tenerlos disponibles durante la depuración o en los archivos de versión final, debe declararlos todos en Package.swift de esta manera

 .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 Webber de la siguiente manera

webber serve solo para lanzarlo rápidamente.

webber serve -t pwa -s Service para iniciarlo en modo PWA

Parámetros adicionales

-v o --verbose para mostrar más información en la consola con fines de depuración

-p 443 o --port 443 para iniciar el servidor webber en el puerto 443 en lugar del predeterminado 8888

--browser chrome/safari para abrir automáticamente el navegador deseado, por defecto no abre ninguno

--browser-self-signed necesario para depurar los trabajadores del servicio localmente; de lo contrario, no funcionan

--browser-incognito para abrir una instancia adicional del navegador en modo incógnito, solo funciona con Chrome


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 .webber/entrypoint/dev dentro del proyecto y edite el archivo 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 webber release o webber release -t pwa -s Service para PWA.

Luego tome los archivos compilados de la carpeta .webber/release y cárguelos en su servidor.

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 Content-Type: application/wasm para archivos wasm ; de lo contrario, desafortunadamente, el navegador no podrá cargar su aplicación WebAssembly.

Por ejemplo, GithubPages no proporciona el tipo de contenido correcto para los archivos wasm , por lo que lamentablemente es imposible alojar sitios WebAssembly en él.

Nginx

Si usa su propio servidor con nginx, abra /etc/nginx/mime.types y verifique si contiene application/wasm wasm; registro. Si es así, ¡entonces estás listo para irte!

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 las bibliotecas de SwifWeb y también de protagonizarlas todas!


¡Tengo una gran comunidad de SwiftStream en Discord 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!


Es solo el comienzo, ¡así que esté atento a más artículos sobre SwifWeb!


¡Dile a tus amigos!