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á.
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.
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.
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.
Necesitas tener Swift instalado, la forma más fácil de tenerlo:
En otros casos, consulte las instrucciones de instalación en el sitio web oficial.
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.
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
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() } }
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.
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.
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)") } } }
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.
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
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 }
Puede usar archivos CSS tradicionales, ¡pero también tiene la nueva y mágica habilidad de usar una hoja de estilo escrita en Swift!
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")
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 { }
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
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
¡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 |
---|---|
| |
| |
| |
| |
| |
| |
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;">
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.
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!
lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()
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)
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.
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> """ }
let names = ["Bob", "John", "Annie"] Div { ForEach(names) { name in Div(name) } // or ForEach(names) { index, name in Div("\(index). \(name)") } // or with range ForEach(1...20) { index in Div() } // and even like this 20.times { Div().class(.shootingStar) } }
@State var names = ["Bob", "John", "Annie"] Div { ForEach($names) { name in Div(name) } // or with index ForEach($names) { index, name in Div("\(index). \(name)") } } Button("Change 1").onClick { // this will append new Div with name automatically self.names.append("George") } Button("Change 2").onClick { // this will replace and update Divs with names automatically self.names = ["Bob", "Peppa", "George"] }
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 :)
@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.
Es un contenedor de propiedades que notifica a todos los suscriptores sobre sus 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)") }
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
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() {} }
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)
extension Class { var my: Class { "my" } }
Luego úsalo como Div().class(.my)
extension Id { var myId: Id { "my" } }
Luego úsalo como Div().id(.my)
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.
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 })
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.
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.
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.
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
.
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
.
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
.
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 .
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 .
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
.
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") }
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.
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]
La localización clásica es automática y depende del idioma del sistema del usuario
H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
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 }
H1(LString( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
H1(Localization.currentState.map { "Curent language: \($0.rawValue)" }) H2(LString(.en("English string"), .es("Hilo Español"))) Button("change lang").onClick { Localization.current = Localization.current.rawValue.contains("en") ? .es : .en }
import FetchAPI Fetch("https://jsonplaceholder.typicode.com/todos/1") { switch $0 { case .failure: break case .success(let response): print("response.code: \(response.status)") print("response.statusText: \(response.statusText)") print("response.ok: \(response.ok)") print("response.redirected: \(response.redirected)") print("response.headers: \(response.headers.dictionary)") struct Todo: Decodable { let id, userId: Int let title: String let completed: Bool } response.json(as: Todo.self) { switch $0 { case .failure(let error): break case .success(let todo): print("decoded todo: \(todo)") } } } }
import XMLHttpRequest XMLHttpRequest() .open(method: "GET", url: "https://jsonplaceholder.typicode.com/todos/1") .onAbort { print("XHR onAbort") }.onLoad { print("XHR onLoad") }.onError { print("XHR onError") }.onTimeout { print("XHR onTimeout") }.onProgress{ progress in print("XHR onProgress") }.onLoadEnd { print("XHR onLoadEnd") }.onLoadStart { print("XHR onLoadStart") }.onReadyStateChange { readyState in print("XHR onReadyStateChange") } .send()
import WebSocket let webSocket = WebSocket("wss://echo.websocket.org").onOpen { print("ws connected") }.onClose { (closeEvent: CloseEvent) in print("ws disconnected code: \(closeEvent.code) reason: \(closeEvent.reason)") }.onError { print("ws error") }.onMessage { message in print("ws message: \(message)") switch message.data { case .arrayBuffer(let arrayBuffer): break case .blob(let blob): break case .text(let text): break case .unknown(let jsValue): break } } Dispatch.asyncAfter(2) { // send as simple string webSocket.send("Hello from SwifWeb") // send as Blob webSocket.send(Blob("Hello from SwifWeb")) }
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()
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() } }
Lea las instrucciones en la página del repositorio . Es una solución complicada pero totalmente funcional 😎
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 .
Está disponible a través de JavaScriptKit , que es la base de SwifWeb .
Lea cómo se encuentra en el repositorio oficial .
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“)
Inicie Webber de la siguiente manera
webber serve
solo para lanzarlo rápidamente.
webber serve -t pwa -s Service
para iniciarlo en modo PWA
-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
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
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.
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.
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!
¡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!