O mundo do desenvolvimento web é vasto e é fácil se sentir perdido no fluxo constante de novas tecnologias que surgem todos os dias. A maioria dessas novas tecnologias é construída usando JavaScript ou TypeScript. No entanto, neste artigo, apresentarei a você o desenvolvimento da Web usando o Swift nativo, diretamente no seu navegador, e tenho certeza de que isso o impressionará.
Para que o Swift funcione nativamente na página da Web, ele precisa ser compilado primeiro para o código de bytes do WebAssembly e, em seguida, o JavaScript pode carregar esse código na página. Todo o processo de compilação é um pouco complicado, pois precisamos usar a cadeia de ferramentas especial e criar arquivos auxiliares, é por isso que existem ferramentas CLI auxiliares disponíveis: Carton e Webber.
A comunidade SwiftWasm fez um tremendo trabalho para tornar possível compilar o Swift no WebAssembly corrigindo a cadeia de ferramentas Swift original. Eles atualizam a cadeia de ferramentas puxando automaticamente as alterações da cadeia de ferramentas original todos os dias e consertando sua bifurcação se os testes falharem. O objetivo deles é fazer parte da cadeia de ferramentas oficial e eles esperam que isso aconteça em um futuro próximo.
Carton é feito pela comunidade SwiftWasm e pode ser usado para projetos Tokamak, que é uma estrutura que permite escrever sites usando SwiftUI.
Webber é feito para projetos SwifWeb. SwifWeb é diferente porque envolve todos os padrões HTML e CSS, bem como todas as APIs da web.
Embora você prefira escrever aplicativos da Web usando SwiftUI para consistência de código, acredito que essa seja a abordagem errada porque o desenvolvimento da Web é inerentemente diferente e não pode ser abordado da mesma forma que o SwiftUI.
É por isso que criei o SwifWeb, que oferece a capacidade de usar todo o poder do HTML, CSS e APIs da Web diretamente do Swift, usando sua bela sintaxe com preenchimento automático e documentação. E criei a ferramenta Webber porque o Carton não é capaz de compilar, depurar e implantar aplicativos SwifWeb da maneira certa porque não foi criado para isso.
Meu nome é Mikhail Isaev e sou o autor do SwifWeb. Neste artigo, mostrarei como começar a criar um site usando o SwifWeb.
Você precisa ter o Swift instalado, a maneira mais fácil de tê-lo:
Em outros casos, consulte as instruções de instalação no site oficial
Criei Webber para ajudá-lo a construir, depurar e implantar seus aplicativos.
No macOS, é fácil instalar com o HomeBrew (instale-o no site deles)
brew install swifweb/tap/webber
para atualizar para a versão mais recente mais tarde, basta executar
brew upgrade webber
No Ubuntu ou Windows (Ubuntu no WSL2) clone e compile o 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 atualizar para a última versão mais tarde execute
cd /opt/webber sudo git pull sudo swift build -c release
ramo principal sempre contém código estável, então fique à vontade para obter atualizações dele
Abra o terminal e execute
webber new
No menu interativo, escolha pwa
ou spa
e digite o nome do projeto.
Altere o diretório para o projeto recém-criado e execute webber serve
.
Este comando irá compilar seu projeto no WebAssembly, empacotar todos os arquivos necessários dentro de uma pasta .webber
especial e começar a servir seu projeto em todas as interfaces usando a porta 8888
por padrão.
Argumentos adicionais para webber serve
tipo de aplicativo
-t pwa
para Progressive Web App
-t spa
para aplicativo da Web único
Nome do destino do service worker (geralmente chamado de Service
no projeto PWA)
-s Service
Nome do destino do aplicativo ( App
por padrão)
-a App
Imprimir mais informações no console
-v
Porta para o servidor Webber (o padrão é 8888
)
-p 8080
Use -p 443
para testar como SSL real (com configuração SSL autoassinada permitida)
Nome do navegador de destino para iniciar automaticamente
--browser safari
ou --browser chrome
Instância adicional de navegador com configuração SSL autoassinada permitida para depurar service workers
--browser-self-signed
Instância adicional do navegador no modo de navegação anônima
--browser-incognito
O aplicativo começa em 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 maneira semelhante ao iOS:
didFinishLaunching
quando o aplicativo acabou de iniciar
willTerminate
quando o aplicativo for morrer
willResignActive
quando a janela vai ficar inativa
didBecomeActive
quando a janela está ativa
didEnterBackground
quando a janela vai para o fundo
willEnterForeground
quando a janela está indo para o primeiro plano
O método mais útil aqui é didFinishLaunching
porque é um ótimo lugar para configurar o aplicativo. Você vê que parece realmente um aplicativo iOS! 😀
Aqui app
contém métodos úteis de conveniência:
registerServiceWorker(“serviceName“)
chamada para registrar o service worker do PWA
chamada addScript(“path/to/script.js“)
para adicionar script relativo ou externo
addStylesheet(“path/to/style.css“)
chamada para adicionar estilo relativo ou externo
addFont(“path/to/font.woff”, type:)
chamada para adicionar fonte relativa ou externa, opcionalmente defina o tipo
addIcon(“path/to/icon“, type:color:)
chamada para adicionar ícone, opcionalmente defina o tipo e a cor
Além disso, é o local para configurar bibliotecas adicionais como Autolayout , Bootstrap , Materialize , etc.
O roteamento é necessário para mostrar a página apropriada com base na URL atual.
Para entender como usar o roteamento, você precisa entender o que é URL
https://website.com/hello/world - aqui /hello/world é o caminho
Como você viu, no começo, na classe App devemos declarar todas as rotas de nível superior.
Nível superior significa que as páginas declaradas nessas rotas ocuparão todo o espaço da janela.
Ok, por exemplo, a rota raiz pode ser definida de três maneiras
Page("/") { IndexPage() } Page("") { IndexPage() } Page { IndexPage() }
Acho que o último é o mais bonito 🙂
As rotas de login ou registro podem ser definidas assim
Page("login") { LoginPage() } Page("registration") { RegistrationPage() }
Rotas relacionadas a parâmetros
Page("article/:id") { ArticlePage() }
O :id no exemplo acima é uma parte dinâmica da rota. Podemos recuperar esse identificador na classe ArticlePage para exibir o artigo associado a ele.
class ArticlePage: PageController { override func didLoad(with req: PageRequest) { if let articleId = req.parameters.get("id") { // Retrieve article here } } }
Você pode ter mais de um parâmetro no caminho. Recupere todos eles da mesma maneira.
A próxima coisa interessante no caminho é a query , que também é muito fácil de usar. Por exemplo, vamos considerar a rota /search , que espera ter o text
de pesquisa e os parâmetros de consulta age
.
https://website.com/search**?text=Alex&age=19** - a última parte é a consulta
Simplesmente declare a rota de busca
Page("search") { SearchPage() }
E recupere os dados da consulta na classe SearchPage assim
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)") } } }
Você também pode usar *
para declarar a rota que aceita qualquer coisa na parte específica do caminho como esta
Page("foo", "*", "bar") { SearchPage() }
A rota acima aceitará qualquer coisa entre foo e bar, por exemplo /foo/aaa/bar, /foo/bbb/bar, etc.
Com o sinal **
, você pode definir uma rota especial que irá lidar com qualquer coisa que não corresponda a outras rotas em um caminho específico.
Use-o para fazer a rota 404 global
Page("**") { NotFoundPage() }
ou para um caminho específico, por exemplo, quando o usuário não foi encontrado
Page("user", "**") { UserNotFoundPage() }
Vamos esclarecer situações com rotas declaradas acima
/user/1 - se houver uma rota para /user/:id, ele retornará UserPage . Caso contrário, cairá em…
UserNotFoundPage
/user/1/hello - se houver rota para /user/:id/hello, ele cairá em UserNotFoundPage
/algo - se não houver rota para /algo, ele cairá em NotFoundPage
Podemos não querer substituir todo o conteúdo da página pela próxima rota, mas apenas alguns blocos. É aqui que o FragmentRouter é útil!
Vamos considerar que temos abas na página /user . Cada guia é uma subrota e queremos reagir às alterações na subrota usando o FragmentRouter .
Declare a rota de nível superior na classe App
Page("user") { UserPage() }
E declare FragmentRouter na classe UserPage
class UserPage: PageController { @DOM override var body: DOM.Content { // NavBar is from Materialize library :) Navbar() .item("Profile") { self.changePath(to: "/user/profile") } .item("Friends") { self.changePath(to: "/user/friends") } FragmentRouter(self) .routes { Page("profile") { UserProfilePage() } Page("friends") { UserFriendsPage() } } } }
No exemplo acima, FragmentRouter lida com as sub-rotas /user/profile e /user/friends e as renderiza na Navbar , de modo que a página nunca recarrega todo o conteúdo, mas apenas fragmentos específicos.
Também podem ser declarados mais de um fragmento com as mesmas ou diferentes sub-rotas e todos eles funcionarão juntos como mágica!
Btw FragmentRouter é um Div e você pode configurá-lo chamando
FragmentRouter(self) .configure { div in // do anything you want with the div }
Você pode usar arquivos CSS tradicionais, mas também tem a nova e mágica habilidade de usar uma folha de estilo escrita em Swift!
Para declarar uma regra CSS usando Swift, temos o objeto Rule .
Ele pode ser construído declarativamente chamando seus métodos
Rule(...selector...) .alignContent(.baseline) .color(.red) // or rgba/hex color .margin(v: 0, h: .auto)
ou estilo SwiftUI usando @resultBuilder
Rule(...selector...) { AlignContent(.baseline) Color(.red) Margin(v: 0, h: .auto) }
As duas formas são iguais, porém, prefiro a primeira por causa do preenchimento automático logo após digitar .
😀
Todos os métodos CSS descritos no MDN estão disponíveis.
Mais do que isso, ele lida com os prefixos do navegador automaticamente!
No entanto, você pode definir a propriedade personalizada dessa maneira em algum caso específico
Rule(...selector...) .custom("customKey", "customValue")
Para definir quais elementos a regra deve afetar, temos que definir um seletor. Eu vejo o seletor como a consulta no banco de dados, mas partes dessa consulta do seletor eu chamo de ponteiros.
A maneira mais fácil de construir um ponteiro é inicializá-lo usando a string bruta
Pointer("a")
Mas a maneira mais rápida é construí-lo chamando .pointer
na tag HTML necessária como esta
H1.pointer // h1 A.pointer // a Pointer.any // * Class("myClass").pointer // .myClass Id("myId").pointer // #myId
Trata-se de ponteiros básicos, mas eles também têm modificadores como :hover
:first
:first-child
etc.
H1.pointer.first // h1:first H1.pointer.firstChild // h1:first-child H1.pointer.hover // h1:hover
Você pode declarar qualquer modificador existente, eles estão todos disponíveis.
Se algo estiver faltando, não hesite em fazer uma extensão para adicioná-lo!
E não se esqueça de enviar pull request no github para adicioná-lo para todos.
Você também pode concatenar ponteiros
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
Como usar o seletor na Regra
Rule(Pointer("a")) // or Rule(A.pointer)
Como usar mais de um seletor na Regra
Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer))
Ele produz o seguinte código CSS
a, h1#myId, div > p { }
Vamos declarar os estilos escuro e claro para nosso aplicativo e, posteriormente, poderemos alternar facilmente entre eles.
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 e DarkStyle podem ser declarados em arquivos separados ou, por exemplo, no 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) } }
E então, em algum lugar na interface do usuário de alguma página, basta ligar
App.current.theme = .light // to switch to light theme // or App.current.theme = .dark // to switch to dark theme
E ativará ou desativará as folhas de estilo relacionadas! Isso não é legal? 😎
Mas você pode dizer que descrever estilos em Swift em vez de CSS é mais difícil, então qual é o objetivo?
O ponto principal é a reatividade! Podemos usar @State com propriedades CSS e alterar valores na hora!
Veja só, podemos criar uma classe com alguma propriedade reativa e alterá-la a qualquer momento em tempo de execução, assim qualquer elemento da tela que utilizar aquela classe será atualizado! É muito mais eficaz do que mudar de classe para muitos 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) } }
Mais tarde, de qualquer lugar no código, basta ligar
App.current.reactiveColor = .yellow // or any color you want
e atualizará a cor na folha de estilo e em todos os elementos que a usam 😜
Além disso, é possível adicionar CSS bruto em uma folha 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; } """ } }
você pode misturar strings CSS brutas quantas vezes forem necessárias
O roteador está renderizando páginas em cada rota. Page é qualquer classe herdada do PageController .
PageController tem métodos de ciclo de vida como willLoad
didLoad
willUnload
didUnload
, métodos de interface do usuário buildUI
e body
e variável wrapper de propriedade para elementos HTML.
Tecnicamente, PageController é apenas um Div e você pode definir quaisquer propriedades no 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") } } }
Se sua página for minúscula, você pode declará-la mesmo desta forma
PageController { page in H1("Hello world") P("Text under title") Button("Click me") { page.alert("Click!") print("button clicked") } } .backgroundcolor(.lightGrey) .onWillLoad { page in } .onDidLoad { page in } .onWillUnload { page in } .onDidUnload { page in }
Não é lindo e lacônico? 🥲
Métodos de conveniência de bônus
alert(message: String)
- método alert
JS direto
changePath(to: String)
- troca de caminho de URL
Por fim, direi como (!) Construir e usar elementos HTML!
Todos os elementos HTML com seus atributos estão disponíveis no Swift, a lista completa está, por exemplo, no MDN .
Apenas um pequeno exemplo de lista de elementos HTML:
código swifweb | Código HTML |
---|---|
| |
| |
| |
| |
| |
| |
Como você pode ver, é muito fácil acessar qualquer tag HTML no Swift porque todas elas são representadas com o mesmo nome, exceto as entradas. Isso ocorre porque diferentes tipos de entrada têm métodos diferentes e eu não queria misturá-los.
Div
Simples
Div()
podemos acessar todos os seus atributos e propriedades de estilo assim
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;">
Elemento HTML de subclasse para predefinir o estilo para ele ou para criar um elemento composto com muitos elementos filho predefinidos e alguns métodos convenientes disponíveis fora ou para obter eventos de ciclo de vida como didAddToDOM
e didRemoveFromDOM
.
Vamos criar um elemento Divider
que é apenas um Div
, mas com a classe .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) } }
É muito importante chamar supermétodos ao criar subclasses.
Sem ele, você pode experimentar um comportamento inesperado.
O elemento pode ser anexado ao DOM de PageController ou elemento HTML imediatamente ou posteriormente.
Agora mesmo
Div { H1("Title") P("Subtitle") Div { Ul { Li("One") Li("Two") } } }
Ou mais tarde usando lazy var
lazy var myDiv1 = Div() lazy var myDiv2 = Div() Div { myDiv1 myDiv2 }
Assim, você pode declarar um elemento HTML com antecedência e adicioná-lo ao DOM a qualquer momento!
lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()
Qualquer elemento HTML tem uma propriedade superview opcional que dá acesso ao seu pai se for adicionado ao DOM
Div().superview?.backgroundColor(.red)
Muitas vezes precisamos mostrar elementos apenas em certas condições, então vamos usar if/else
para isso
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") } }
Mas não é reativo. Se você tentar definir showDiv2
como false
, nada acontecerá.
exemplo reativo
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 que devemos usar $showDiv2.map {…}
?
Em ordem: porque não é SwiftUI. De forma alguma.
Leia mais sobre @State
abaixo.
Você também pode precisar adicionar HTML bruto na página ou elemento HTML e é facilmente possível
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 aos exemplos acima, mas também BuilderFunction
está disponível
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 } }
Você pode usar BuilderFunction
em loops ForEach
para calcular algum valor apenas uma vez como um valor delay
no exemplo a seguir
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) } }
Também pode funcionar como um argumento
BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 }
BuilderFunction também está disponível para elementos HTML :)
@State
é a coisa mais desejável hoje em dia para programação declarativa.
Como eu disse acima: não é SwiftUI, então não há máquina de estado global que rastreie e redesenhe tudo. E os elementos HTML não são estruturas temporárias, mas classes, portanto, são objetos reais e você tem acesso direto a eles. É muito melhor e flexível, você tem todo o controle.
É um wrapper de propriedade que notifica todos os assinantes sobre suas alterações.
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)") }
Exemplo de texto simples
@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
Exemplo de número simples
@State var height = 20.px Div().height($height) // whenever height var changes it updates height of the Div
Exemplo booleano simples
@State var hidden = false Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div
Exemplo de mapeamento
@State var isItCold = true H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })
Mapeando dois 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 })
Mapeando mais de dois 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 as propriedades HTML e CSS podem lidar com valores @State
Você pode adicionar alguns métodos convenientes para elementos concretos como Div
extension Div { func makeItBeautiful() {} }
Ou grupos de elementos se você conhece sua class
pai.
Existem poucas classes pai.
BaseActiveStringElement
- é para elementos que podem ser inicializados com string, como a
, h1
, etc.
BaseContentElement
- é para todos os elementos que podem ter conteúdo dentro dele, como div
, ul
, etc.
BaseElement
- é para todos os elementos
Portanto, a extensão para todos os elementos pode ser escrita dessa maneira
extension BaseElement { func doSomething() {} }
A classe Color é responsável pelas cores. Tem cores HTML pré-definidas, mas você pode ter as suas próprias
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) }
Em seguida, use-o como H1(“Text“).color(.myColor1)
extension Class { var my: Class { "my" } }
Em seguida, use-o como Div().class(.my)
extension Id { var myId: Id { "my" } }
Em seguida, use-o como Div().id(.my)
o objeto window
é totalmente encapsulado e acessível por meio da variável App.current.window
.
A referência completa está disponível no MDN .
Vamos fazer uma breve visão geral abaixo
Você pode ouvi-lo em Lifecycle
no App.swift
ou diretamente desta forma
App.current.window.$isInForeground.listen { isInForeground in // foreground flag changed }
ou apenas leia a qualquer hora em qualquer lugar
if App.current.window.isInForeground { // do somethign }
ou reagir com elemento HTML
Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white })
É o mesmo que o sinalizador Foreground, mas acessível via App.current.window.isActive
Ele detecta se um usuário ainda está interagindo dentro da janela.
Igual ao sinalizador Foreground, mas acessível via App.current.window.isOnline
Ele detecta se um usuário ainda tem acesso à internet.
Igual ao sinalizador Foreground, mas acessível via App.current.window.isDark
Ele detecta se o navegador ou sistema operacional de um usuário está no modo escuro.
O tamanho da área de conteúdo da janela (janela de visualização), incluindo barras de rolagem
App.current.window.innerSize
é o objeto Size dentro dos valores de width
e height
dentro.
Também disponível como variável @State
.
O tamanho da janela do navegador, incluindo barras de ferramentas/barras de rolagem.
App.current.window.outerSize
é o objeto Size dentro dos valores de width
e height
dentro.
Também disponível como variável @State
.
Objeto especial para inspecionar as propriedades da tela na qual a janela atual está sendo renderizada. Disponível via App.current.window.screen
.
A propriedade mais interessante geralmente é pixelRatio
.
Contém as URLs visitadas pelo usuário (dentro de uma janela do navegador).
Disponível via App.current.window.history
ou apenas History.shared
.
É acessível como variável @State
, para que você possa ouvir suas alterações, se necessário.
App.current.window.$history.listen { history in // read history properties }
Também é acessível como variável simples
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
Mais detalhes estão disponíveis no MDN .
Contém informações sobre o URL atual.
Disponível via App.current.window.location
ou apenas Location.shared
.
É acessível como variável @State
, para que você possa ouvir suas alterações, se necessário.
É assim que o roteador funciona, por exemplo.
App.current.window.$location.listen { location in // read location properties }
Também é acessível como uma variável simples
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
Mais detalhes estão disponíveis no MDN .
Contém informações sobre o navegador.
Disponível via App.current.window.navigator
ou apenas Navigator.shared
As propriedades mais interessantes geralmente são language
platform
userAgent
cookieEnabled
.
Permite salvar pares chave/valor em um navegador da web. Armazena dados sem data de validade.
Disponível como App.current.window.localStorage
ou apenas 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()
Alterações de rastreamento
LocalStorage.onChange { key, oldValue, newValue in print("LocalStorage: key \(key) has been updated") }
Acompanhando a remoção de todos os itens
LocalStorage.onClear { print("LocalStorage: all items has been removed") }
Permite salvar pares chave/valor em um navegador da web. Armazena dados para apenas uma sessão.
Disponível como App.current.window.sessionStorage
ou apenas SessionStorage.shared
.
API é absolutamente igual ao LocalStorage descrito acima.
Representa qualquer página da web carregada no navegador e serve como ponto de entrada para o conteúdo da página da web.
Disponível via App.current.window.document
.
App.current.window.document.title // also $title App.current.window.document.metaDescription // also $metaDescription App.current.window.document.head // <head> element App.current.window.document.body // <body> element App.current.window.documentquerySelector("#my") // returns BaseElement? App.current.window.document.querySelectorAll(".my") // returns [BaseElement]
A localização clássica é automática e depende do idioma do sistema do usuário
H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
Se você quiser alterar as strings localizadas na tela em tempo real (sem recarregar a página)
Você pode alterar o idioma atual chamando
Localization.current = .es
Se você salvou o idioma do usuário em algum lugar nos cookies ou localstorage, deverá defini-lo na inicialização do aplicativo
Lifecycle.didFinishLaunching { Localization.current = .es }
H1(LString( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
H1(Localization.currentState.map { "Curent language: \($0.rawValue)" }) H2(LString(.en("English string"), .es("Hilo Español"))) Button("change lang").onClick { Localization.current = Localization.current.rawValue.contains("en") ? .es : .en }
import FetchAPI Fetch("https://jsonplaceholder.typicode.com/todos/1") { switch $0 { case .failure: break case .success(let response): print("response.code: \(response.status)") print("response.statusText: \(response.statusText)") print("response.ok: \(response.ok)") print("response.redirected: \(response.redirected)") print("response.headers: \(response.headers.dictionary)") struct Todo: Decodable { let id, userId: Int let title: String let completed: Bool } response.json(as: Todo.self) { switch $0 { case .failure(let error): break case .success(let todo): print("decoded todo: \(todo)") } } } }
import XMLHttpRequest XMLHttpRequest() .open(method: "GET", url: "https://jsonplaceholder.typicode.com/todos/1") .onAbort { print("XHR onAbort") }.onLoad { print("XHR onLoad") }.onError { print("XHR onError") }.onTimeout { print("XHR onTimeout") }.onProgress{ progress in print("XHR onProgress") }.onLoadEnd { print("XHR onLoadEnd") }.onLoadStart { print("XHR onLoadStart") }.onReadyStateChange { readyState in print("XHR onReadyStateChange") } .send()
import WebSocket let webSocket = WebSocket("wss://echo.websocket.org").onOpen { print("ws connected") }.onClose { (closeEvent: CloseEvent) in print("ws disconnected code: \(closeEvent.code) reason: \(closeEvent.reason)") }.onError { print("ws error") }.onMessage { message in print("ws message: \(message)") switch message.data { case .arrayBuffer(let arrayBuffer): break case .blob(let blob): break case .text(let text): break case .unknown(let jsValue): break } } Dispatch.asyncAfter(2) { // send as simple string webSocket.send("Hello from SwifWeb") // send as Blob webSocket.send(Blob("Hello from SwifWeb")) }
Simple print(“Hello world“)
é equivalente a console.log('Hello world')
em JavaScript
Métodos de console também são embalados com amor ❤️
Console.dir(...) Console.error(...) Console.warning(...) Console.clear()
Para fazer a visualização ao vivo funcionar, declare a classe WebPreview em cada arquivo desejado.
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() } }
Por favor, leia as instruções na página do repositório . É uma solução complicada, mas totalmente funcional 😎
Vá para Extensions dentro do VSCode e pesquise Webber .
Depois de instalado, pressione Cmd+Shift+P
(ou Ctrl+Shift+P
no Linux/Windows)
Encontre e inicie Webber Live Preview
.
No lado direito, você verá a janela de visualização ao vivo e ela será atualizada sempre que você salvar o arquivo que contém a classe WebPreview .
Está disponível através do JavaScriptKit , que é a base do SwifWeb .
Leia como você está no repositório oficial .
Você pode adicionar css
, js
, png
, jpg
e quaisquer outros recursos estáticos dentro do projeto.
Mas para tê-los disponíveis durante a depuração ou nos arquivos de lançamento final, você deve declará-los todos no Package.swift assim
.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") ]),
Mais tarde você poderá acessá-los, por exemplo, desta forma Img().src(“/images/logo.png“)
Inicie o Webber da seguinte maneira
webber serve
apenas para iniciá-lo rapidamente
webber serve -t pwa -s Service
para iniciá-lo no modo PWA
-v
ou --verbose
para mostrar mais informações no console para fins de depuração
-p 443
ou --port 443
para iniciar o servidor webber na porta 443 em vez do padrão 8888
--browser chrome/safari
para abrir automaticamente o navegador desejado, por padrão não abre nenhum
--browser-self-signed
necessário para depurar service workers localmente, caso contrário, eles não funcionam
--browser-incognito
para abrir uma instância adicional do navegador no modo de navegação anônima, funciona apenas com o Chrome
Portanto, para criar seu aplicativo no modo de depuração, abra-o automaticamente no Chrome e atualize o navegador automaticamente sempre que alterar qualquer arquivo, inicie-o dessa maneira
para SPA
webber serve --browser chrome
para testes reais de PWA
webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito
Você pode querer melhorar o processo de carregamento inicial.
Para isso basta abrir a pasta .webber/entrypoint/dev
dentro do projeto e editar o arquivo index.html
.
Ele contém o código HTML inicial com ouvintes muito úteis: WASMLoadingStarted
WASMLoadingStartedWithoutProgress
WASMLoadingProgress
WASMLoadingError
.
Você é livre para editar esse código para o que quiser para implementar seu estilo personalizado 🔥
Quando terminar a nova implementação, não se esqueça de salvá-la na pasta .webber/entrypoint/release
Basta executar webber release
ou webber release -t pwa -s Service
para PWA.
Em seguida, pegue os arquivos compilados da pasta .webber/release
e envie-os para o seu servidor.
Você pode enviar seus arquivos para qualquer hospedagem estática.
A hospedagem deve fornecer o tipo de conteúdo correto para arquivos wasm !
Sim, é muito importante ter o cabeçalho correto Content-Type: application/wasm
para arquivos wasm , caso contrário, infelizmente, o navegador não poderá carregar seu aplicativo WebAssembly.
Por exemplo, o GithubPages não fornece o tipo de conteúdo correto para arquivos wasm , portanto, infelizmente, é impossível hospedar sites WebAssembly nele.
Se você usa seu próprio servidor com nginx, abra /etc/nginx/mime.types
e verifique se ele contém application/wasm wasm;
registro. Se sim, então você está pronto para ir!
Espero ter te surpreendido hoje e que você pelo menos experimente o SwifWeb e, no máximo, comece a usá-lo em seu próximo grande projeto da web!
Sinta-se à vontade para contribuir com qualquer uma das bibliotecas SwifWeb e também para marcar ⭐️ todas elas!
Eu tenho uma grande comunidade SwiftStream no Discord, onde você pode encontrar grande suporte, ler pequenos tutoriais e ser notificado primeiro sobre as próximas atualizações! Seria legal ver você conosco!
É apenas o começo, então fique ligado para mais artigos sobre SwifWeb!
Conte aos seus amigos!