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á. Como isso é possível? 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. Posso usar uma cadeia de ferramentas de terceiros? A comunidade 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. SwiftWasm Caixa ou Webber? é feito pela comunidade SwiftWasm e pode ser usado para projetos Tokamak, que é uma estrutura que permite escrever sites usando SwiftUI. Carton é feito para projetos SwifWeb. SwifWeb é diferente porque envolve todos os padrões HTML e CSS, bem como todas as APIs da web. Webber 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. ferramentas necessárias Rápido Você precisa ter o Swift instalado, a maneira mais fácil de tê-lo: no macOS é instalar o Xcode no Linux ou Windows (WSL2) é usar o script de swiftlang.xyz Em outros casos, consulte as instruções de instalação no site oficial Webber CLI Criei Webber para ajudá-lo a construir, depurar e implantar seus aplicativos. No macOS, é fácil instalar com o HomeBrew (instale-o no deles) site 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 sempre contém código estável, então fique à vontade para obter atualizações dele principal Criando novo projeto Abra o terminal e execute webber new No menu interativo, escolha ou e digite o nome do projeto. pwa spa 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 especial e começar a servir seu projeto em todas as interfaces usando a porta por padrão. .webber 8888 Argumentos adicionais para webber serve tipo de aplicativo para Progressive Web App -t pwa para aplicativo da Web único -t spa Nome do destino do service worker (geralmente chamado de no projeto PWA) Service -s Service Nome do destino do aplicativo ( por padrão) App -a App Imprimir mais informações no console -v Porta para o servidor Webber (o padrão é ) 8888 -p 8080 Use para testar como SSL real (com configuração SSL autoassinada permitida) -p 443 Nome do navegador de destino para iniciar automaticamente ou --browser safari --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 Aplicativo 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() } } Vida útil Funciona de maneira semelhante ao iOS: quando o aplicativo acabou de iniciar didFinishLaunching quando o aplicativo for morrer willTerminate quando a janela vai ficar inativa willResignActive quando a janela está ativa didBecomeActive quando a janela vai para o fundo didEnterBackground quando a janela está indo para o primeiro plano willEnterForeground O método mais útil aqui é porque é um ótimo lugar para configurar o aplicativo. Você vê que parece realmente um aplicativo iOS! 😀 didFinishLaunching Aqui contém métodos úteis de conveniência: app chamada para registrar o service worker do PWA registerServiceWorker(“serviceName“) chamada para adicionar script relativo ou externo addScript(“path/to/script.js“) chamada para adicionar estilo relativo ou externo addStylesheet(“path/to/style.css“) chamada para adicionar fonte relativa ou externa, opcionalmente defina o tipo addFont(“path/to/font.woff”, type:) chamada para adicionar ícone, opcionalmente defina o tipo e a cor addIcon(“path/to/icon“, type:color:) Além disso, é o local para configurar bibliotecas adicionais como , , , etc. Autolayout Bootstrap Materialize Rotas 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 é o /hello/world 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 no exemplo acima é uma parte dinâmica da rota. Podemos recuperar esse identificador na classe para exibir o artigo associado a ele. :id ArticlePage 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. Consultas A próxima coisa interessante no caminho é a , que também é muito fácil de usar. Por exemplo, vamos considerar a rota , que espera ter o de pesquisa e os parâmetros de consulta . query /search text 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 assim SearchPage class SearchPage: PageController { struct Query: Decodable { let text: String? let age: Int? } override func didLoad(with req: PageRequest) { do { let query = try req.query.decode(Query.self) // use optional query.text and query.age // to query search results } catch { print("Can't decode query: \(error)") } } } Qualquer coisa 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. pega-tudo 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á . Caso contrário, cairá em… UserPage 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 Roteamento aninhado Podemos não querer substituir todo o conteúdo da página pela próxima rota, mas apenas alguns blocos. É aqui que o é útil! FragmentRouter Vamos considerar que temos abas na página . Cada guia é uma subrota e queremos reagir às alterações na subrota usando o . /user FragmentRouter Declare a rota de nível superior na classe App Page("user") { UserPage() } E declare na classe FragmentRouter UserPage class UserPage: PageController { @DOM override var body: DOM.Content { // NavBar is from Materialize library :) Navbar() .item("Profile") { self.changePath(to: "/user/profile") } .item("Friends") { self.changePath(to: "/user/friends") } FragmentRouter(self) .routes { Page("profile") { UserProfilePage() } Page("friends") { UserFriendsPage() } } } } No exemplo acima, lida com as sub-rotas e e as renderiza na , de modo que a página nunca recarrega todo o conteúdo, mas apenas fragmentos específicos. FragmentRouter /user/profile /user/friends Navbar 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 é um e você pode configurá-lo chamando FragmentRouter Div FragmentRouter(self) .configure { div in // do anything you want with the div } Folhas de estilo 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! Fundamentos 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") Seletor 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 na tag HTML necessária como esta .pointer 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 etc. :hover :first :first-child 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 { } Reatividade 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 }) } } e podem ser declarados em arquivos separados ou, por exemplo, no App.swift LightStyle DarkStyle class LightStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.white) Rule(H1.pointer).color(.black) } } class DarkStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.black) Rule(H1.pointer).color(.white) } } 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 Páginas O roteador está renderizando páginas em cada rota. Page é qualquer classe herdada do . PageController tem métodos de ciclo de vida como , métodos de interface do usuário e e variável wrapper de propriedade para elementos HTML. PageController willLoad didLoad willUnload didUnload buildUI body Tecnicamente, é apenas um Div e você pode definir quaisquer propriedades no método . PageController buildUI class IndexPage: PageController { // MARK: - Lifecycle override func willLoad(with req: PageRequest) { super.willLoad(with: req) } override func didLoad(with req: PageRequest) { super.didLoad(with: req) // set page title and metaDescription self.title = "My Index Page" self.metaDescription = "..." // also parse query and hash here } override func willUnload() { super.willUnload() } override func didUnload() { super.didUnload() } // MARK: - UI override func buildUI() { super.buildUI() // access any properties of the page's div here // eg self.backgroundcolor(.lightGrey) // optionally call body method here to add child HTML elements body { P("Hello world") } // or alternatively self.appendChild(P("Hello world")) } // the best place to declare any child HTML elements @DOM override var body: DOM.Content { H1("Hello world") P("Text under title") Button("Click me") { self.alert("Click!") print("button clicked") } } } 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 - método JS direto alert(message: String) alert - troca de caminho de URL changePath(to: String) Elementos HTML 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 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 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. Simples Div 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;"> Subclasse 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 e . didAddToDOM didRemoveFromDOM Vamos criar um elemento que é apenas um , mas com a classe predefinida Divider Div .divider public class Divider: Div { // it is very important to override the name // because otherwise it will be <divider> in HTML open class override var name: String { "\(Div.self)".lowercased() } required public init() { super.init() } // this method executes immediately after any init method public override func postInit() { super.postInit() // here we are adding `divider` class self.class(.divider) } } É muito importante chamar supermétodos ao criar subclasses. Sem ele, você pode experimentar um comportamento inesperado. Anexando ao DOM O elemento pode ser anexado ao DOM de ou imediatamente ou posteriormente. PageController elemento HTML 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 com antecedência e adicioná-lo ao DOM a qualquer momento! elemento HTML Removendo do DOM lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove() Acessar elemento pai Qualquer elemento HTML tem uma propriedade superview opcional que dá acesso ao seu pai se for adicionado ao DOM Div().superview?.backgroundColor(.red) condições if/else Muitas vezes precisamos mostrar elementos apenas em certas condições, então vamos usar para isso if/else lazy var myDiv1 = Div() lazy var myDiv2 = Div() lazy var myDiv3 = Div() var myDiv4: Div? var showDiv2 = true Div { myDiv1 if showDiv2 { myDiv2 } else { myDiv3 } if let myDiv4 = myDiv4 { myDiv4 } else { P("Div 4 was nil") } } Mas não é reativo. Se você tentar definir como , nada acontecerá. showDiv2 false 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. HTML bruto 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> """ } Para cada Exemplo 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) } } Exemplo 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 aos exemplos acima, mas também está disponível 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 } } Você pode usar em loops para calcular algum valor apenas uma vez como um valor no exemplo a seguir BuilderFunction ForEach delay ForEach(1...20) { index in BuilderFunction(9999.asRandomMax()) { delay in CSSRule(Pointer(".shooting_star").nthChild("\(index)")) .custom("top", "calc(50% - (\(400.asRandomMax() - 200)px))") .custom("left", "calc(50% - (\(300.asRandomMax() + 300)px))") .animationDelay(delay.ms) CSSRule(Pointer(".shooting_star").nthChild("\(index)").before) .animationDelay(delay.ms) CSSRule(Pointer(".shooting_star").nthChild("\(index)").after) .animationDelay(delay.ms) } } Também pode funcionar como um argumento BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 } também está disponível para elementos HTML :) BuilderFunction Reatividade com @State é a coisa mais desejável hoje em dia para declarativa. @State programação 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. O que há sob o capô? É um wrapper de propriedade que notifica todos os assinantes sobre suas alterações. Como subscrever as 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)") } Como os elementos HTML podem reagir às mudanças? 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 Extensões Estender elementos HTML Você pode adicionar alguns métodos convenientes para elementos concretos como Div extension Div { func makeItBeautiful() {} } Ou grupos de elementos se você conhece sua pai. class Existem poucas classes pai. - é para elementos que podem ser inicializados com string, como , , etc. BaseActiveStringElement a h1 - é para todos os elementos que podem ter conteúdo dentro dele, como , , etc. BaseContentElement div ul - é para todos os elementos BaseElement Portanto, a extensão para todos os elementos pode ser escrita dessa maneira extension BaseElement { func doSomething() {} } Declarar cores A classe é responsável pelas cores. Tem cores HTML pré-definidas, mas você pode ter as suas próprias Color extension Color { var myColor1: Color { .hex(0xf1f1f1) } // which is 0xF1F1F1 var myColor2: Color { .hsl(60, 60, 60) } // which is hsl(60, 60, 60) var myColor3: Color { .hsla(60, 60, 60, 0.8) } // which is hsla(60, 60, 60, 0.8) var myColor4: Color { .rgb(60, 60, 60) } // which is rgb(60, 60, 60) var myColor5: Color { .rgba(60, 60, 60, 0.8) } // which is rgba(60, 60, 60, 0.8) } Em seguida, use-o como H1(“Text“).color(.myColor1) Declarar classes extension Class { var my: Class { "my" } } Em seguida, use-o como Div().class(.my) Declarar IDs extension Id { var myId: Id { "my" } } Em seguida, use-o como Div().id(.my) APIs da Web Janela o objeto é totalmente encapsulado e acessível por meio da variável . window App.current.window A referência completa está disponível no . MDN Vamos fazer uma breve visão geral abaixo Bandeira de primeiro plano Você pode ouvi-lo em no ou diretamente desta forma Lifecycle App.swift 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 }) Sinalizador ativo É 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. Status online Igual ao sinalizador Foreground, mas acessível via App.current.window.isOnline Ele detecta se um usuário ainda tem acesso à internet. status de modo escuro 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. Tamanho interno O tamanho da área de conteúdo da janela (janela de visualização), incluindo barras de rolagem é o objeto dentro dos valores de e dentro. App.current.window.innerSize Size width height Também disponível como variável . @State Tamanho Externo O tamanho da janela do navegador, incluindo barras de ferramentas/barras de rolagem. é o objeto dentro dos valores de e dentro. App.current.window.outerSize Size width height Também disponível como variável . @State Tela 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 História Contém as URLs visitadas pelo usuário (dentro de uma janela do navegador). Disponível via ou apenas . App.current.window.history History.shared É acessível como variável , para que você possa ouvir suas alterações, se necessário. @State 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 Localização Contém informações sobre o URL atual. Disponível via ou apenas . App.current.window.location Location.shared É acessível como variável , para que você possa ouvir suas alterações, se necessário. @State É 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 Navegador Contém informações sobre o navegador. Disponível via ou apenas App.current.window.navigator Navigator.shared As propriedades mais interessantes geralmente são . language platform userAgent cookieEnabled LocalStorage Permite salvar pares chave/valor em um navegador da web. Armazena dados sem data de validade. Disponível como ou apenas . App.current.window.localStorage LocalStorage.shared // You can save any value that can be represented in JavaScript LocalStorage.shared.set("key", "value") // saves String LocalStorage.shared.set("key", 123) // saves Int LocalStorage.shared.set("key", 0.8) // saves Double LocalStorage.shared.set("key", ["key":"value"]) // saves Dictionary LocalStorage.shared.set("key", ["v1", "v2"]) // saves Array // Getting values back LocalStorage.shared.string(forKey: "key") // returns String? LocalStorage.shared.integer(forKey: "key") // returns Int? LocalStorage.shared.string(forKey: "key") // returns String? LocalStorage.shared.value(forKey: "key") // returns JSValue? // Removing item LocalStorage.shared.removeItem(forKey: "key") // Removing all items LocalStorage.shared.clear() 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") } SessionStorage Permite salvar pares chave/valor em um navegador da web. Armazena dados para apenas uma sessão. Disponível como ou apenas . App.current.window.sessionStorage SessionStorage.shared API é absolutamente igual ao descrito acima. LocalStorage Documento 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] Localização localização estática A localização clássica é automática e depende do idioma do sistema do usuário Como usar H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは"))) localização dinâmica 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 } Como usar H1(LString( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは"))) Exemplo avançado 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)") } } } } XMLHttpRequest import XMLHttpRequest XMLHttpRequest() .open(method: "GET", url: "https://jsonplaceholder.typicode.com/todos/1") .onAbort { print("XHR onAbort") }.onLoad { print("XHR onLoad") }.onError { print("XHR onError") }.onTimeout { print("XHR onTimeout") }.onProgress{ progress in print("XHR onProgress") }.onLoadEnd { print("XHR onLoadEnd") }.onLoadStart { print("XHR onLoadStart") }.onReadyStateChange { readyState in print("XHR onReadyStateChange") } .send() WebSocket import WebSocket let webSocket = WebSocket("wss://echo.websocket.org").onOpen { print("ws connected") }.onClose { (closeEvent: CloseEvent) in print("ws disconnected code: \(closeEvent.code) reason: \(closeEvent.reason)") }.onError { print("ws error") }.onMessage { message in print("ws message: \(message)") switch message.data { case .arrayBuffer(let arrayBuffer): break case .blob(let blob): break case .text(let text): break case .unknown(let jsValue): break } } Dispatch.asyncAfter(2) { // send as simple string webSocket.send("Hello from SwifWeb") // send as Blob webSocket.send(Blob("Hello from SwifWeb")) } Console Simple é equivalente a em JavaScript print(“Hello world“) console.log('Hello world') Métodos também são embalados com amor ❤️ de console Console.dir(...) Console.error(...) Console.warning(...) Console.clear() Visualização ao vivo 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() } } código X Por favor, leia as instruções na . É uma solução complicada, mas totalmente funcional 😎 página do repositório VSCode Vá para dentro e pesquise . Extensions do VSCode Webber Depois de instalado, pressione (ou no Linux/Windows) Cmd+Shift+P Ctrl+Shift+P 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 Acesso ao JavaScript Está disponível através , que é a base do . do JavaScriptKit SwifWeb Leia como você está . no repositório oficial Recursos Você pode adicionar , , , e quaisquer outros recursos estáticos dentro do projeto. css js png jpg Mas para tê-los disponíveis ou nos arquivos final, você deve declará-los todos no assim durante a depuração de lançamento Package.swift .executableTarget(name: "App", dependencies: [ .product(name: "Web", package: "web") ], resources: [ .copy("css/*.css"), .copy("css"), .copy("images/*.jpg"), .copy("images/*.png"), .copy("images/*.svg"), .copy("images"), .copy("fonts/*.woff2"), .copy("fonts") ]), Mais tarde você poderá acessá-los, por exemplo, desta forma Img().src(“/images/logo.png“) Depuração Inicie da seguinte maneira o Webber apenas para iniciá-lo rapidamente webber serve para iniciá-lo no modo PWA webber serve -t pwa -s Service Parâmetros adicionais ou para mostrar mais informações no console para fins de depuração -v --verbose ou para iniciar o servidor webber na porta 443 em vez do padrão 8888 -p 443 --port 443 para abrir automaticamente o navegador desejado, por padrão não abre nenhum --browser chrome/safari necessário para depurar service workers localmente, caso contrário, eles não funcionam --browser-self-signed para abrir uma instância adicional do navegador no modo de navegação anônima, funciona apenas com o Chrome --browser-incognito 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 Carregamento inicial do aplicativo Você pode querer melhorar o processo de carregamento inicial. Para isso basta abrir a pasta dentro do projeto e editar o arquivo . .webber/entrypoint/dev 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 Liberação de construção Basta executar ou para PWA. webber release webber release -t pwa -s Service Em seguida, pegue os arquivos compilados da pasta e envie-os para o seu servidor. .webber/release como implantar 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 para arquivos , caso contrário, infelizmente, o navegador não poderá carregar seu aplicativo WebAssembly. Content-Type: application/wasm wasm Por exemplo, não fornece o tipo de conteúdo correto para arquivos , portanto, infelizmente, é impossível hospedar sites WebAssembly nele. o GithubPages wasm NginxGenericName Se você usa seu próprio servidor com nginx, abra e verifique se ele contém registro. Se sim, então você está pronto para ir! /etc/nginx/mime.types application/wasm wasm; Conclusão 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 e também para marcar ⭐️ todas elas! bibliotecas SwifWeb Eu tenho uma grande onde você pode encontrar grande suporte, ler pequenos tutoriais e ser notificado primeiro sobre as próximas atualizações! Seria legal ver você conosco! comunidade SwiftStream no Discord, É apenas o começo, então fique ligado para mais artigos sobre SwifWeb! Conte aos seus amigos!