Как использовать Swift для веб-разработки

Как использовать Swift для веб-разработки

20 марта 2023 г.

Мир веб-разработки огромен, и легко потеряться в постоянном потоке новых технологий, появляющихся каждый день. Большинство этих новых технологий созданы с использованием JavaScript или TypeScript. Однако в этой статье я познакомлю вас с веб-разработкой с использованием родного Swift прямо в вашем браузере, и я уверен, что это вас впечатлит.

Как это возможно?

Чтобы Swift изначально работал на веб-странице, его необходимо сначала скомпилировать в байт-код WebAssembly, а затем JavaScript сможет загрузить этот код на страницу. Весь процесс компиляции немного сложен, так как нам нужно использовать специальную цепочку инструментов и создавать вспомогательные файлы, поэтому доступны вспомогательные CLI-инструменты: Carton и Webber.

Можно ли использовать сторонний набор инструментов?

Сообщество SwiftWasm проделало огромную работу, чтобы сделать возможным компиляцию Swift в WebAssembly путем исправления исходной цепочки инструментов Swift. Они обновляют цепочку инструментов, автоматически извлекая изменения из исходной цепочки инструментов каждый день и исправляя свою вилку, если тесты не пройдены. Их цель — стать частью официальной цепочки инструментов, и они надеются, что это произойдет в ближайшем будущем.

Картон или Веббер?

Carton создается сообществом SwiftWasm и может использоваться для проектов Tokamak, представляющих собой структуру, которая дает вам возможность создавать веб-сайты с помощью SwiftUI.

Webber создан для проектов SwifWeb. SwifWeb отличается тем, что включает в себя все стандарты HTML и CSS, а также все веб-API.

Хотя вы можете предпочесть писать веб-приложения с использованием SwiftUI для согласованности кода, я считаю, что это неправильный подход, поскольку веб-разработка по своей сути отличается и не может подходить так же, как SwiftUI.

Вот почему я создал SwifWeb, который дает вам возможность использовать всю мощь HTML, CSS и веб-API непосредственно из Swift, используя его красивый синтаксис с автозаполнением и документацией. И я создал инструмент Webber, потому что Carton не может правильно компилировать, отлаживать и развертывать приложения SwifWeb, потому что он не был создан для этого.

Меня зовут Михаил Исаев, и я автор SwifWeb. В этой статье я покажу вам, как начать создавать веб-сайт с помощью SwifWeb.

Необходимые инструменты


Быстро

У вас должен быть установлен Swift. Самый простой способ:

  • в macOS необходимо установить Xcode
  • в Linux или Windows (WSL2) следует использовать скрипт из swiftlang.xyz
  • .

В остальных случаях ознакомьтесь с инструкцией по установке на официальном сайте

Интернет-интерфейс Webber

Я создал Webber, чтобы помочь вам создавать, отлаживать и развертывать приложения.

В macOS его легко установить с помощью HomeBrew (установите его с их веб-сайта)

brew install swifweb/tap/webber

для обновления до последней версии просто запустите

brew upgrade webber

В Ubuntu или Windows (Ubuntu в WSL2) клонируйте и скомпилируйте Webber вручную

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

обновить до последней версии позже

cd /opt/webber
sudo git pull
sudo swift build -c release

:::подсказка Ветка основная всегда содержит стабильный код, поэтому не стесняйтесь извлекать из нее обновления

:::

Создание нового проекта

Открыть терминал и выполнить

webber new

В интерактивном меню выберите pwa или spa и введите название проекта.

Измените каталог на только что созданный проект и выполните webber serve.

Эта команда скомпилирует ваш проект в WebAssembly, упакует все необходимые файлы в специальную папку .webber и начнет обслуживать ваш проект на всех интерфейсах, используя порт 8888 по умолчанию. .

Дополнительные аргументы для webber serve

  • Тип приложения

-t pwa для прогрессивного веб-приложения

-t spa для одного веб-приложения * Имя цели сервисного работника (обычно называется Service в проекте PWA)

-s Служба * Имя целевого приложения (по умолчанию App)

-приложение * Распечатать дополнительную информацию в консоли

-v * Порт для сервера Webber (по умолчанию 8888)

-p 8080

:::подсказка Используйте -p 443 для проверки подлинности SSL (с разрешенной настройкой самозаверяющего SSL)

:::

  • Название целевого браузера для автоматического запуска

--browser Safari или --browser chrome * Дополнительный экземпляр браузера с разрешенной настройкой самозаверяющего SSL для отладки сервис-воркеров

--browser-self-signed * Дополнительный экземпляр браузера в режиме инкогнито

--браузер-инкогнито

Приложение

Приложение начинается с 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()
    }
}

Жизненный цикл

Он работает аналогично iOS:

didFinishLaunching, когда приложение только что запустилось

willTerminate, когда приложение умрет

willResignActive, когда окно будет неактивным

didBecomeActive, когда окно активно

didEnterBackground, когда окно переходит в фоновый режим

willEnterForeground, когда окно выходит на передний план

Наиболее полезным методом здесь является didFinishLaunching, потому что это отличное место для настройки приложения. Вы видите, что это действительно похоже на приложение для iOS! :ухмыляясь:

Здесь app содержит полезные удобные методы:

registerServiceWorker("serviceName") вызов для регистрации сервисного работника PWA

addScript("path/to/script.js") вызов для добавления относительного или внешнего скрипта

addStylesheet("path/to/style.css") вызов для добавления относительного или внешнего стиля

addFont("path/to/font.woff", type:) вызов для добавления относительного или внешнего шрифта, при необходимости установить тип

addIcon("path/to/icon", type:color:) вызов для добавления значка, при необходимости задайте тип и цвет

Кроме того, это место для настройки дополнительных библиотек, таких как Autolayout, Bootstrap, Materialize и т. д.

Маршруты

Маршрутизация необходима для отображения соответствующей страницы на основе текущего URL-адреса.

Чтобы понять, как использовать маршрутизацию, вы должны понимать, что такое URL

:::подсказка https://website.com/hello/world — здесь /hello/world указан путь

:::

Как вы видели, вначале в классе App мы должны объявить все маршруты верхнего уровня.

Верхний уровень означает, что страницы, объявленные в этих маршрутах, будут занимать все пространство в окне.

Хорошо, например, корневой маршрут можно задать тремя способами

Page("/") { IndexPage() }
Page("") { IndexPage() }
Page { IndexPage() }

Я думаю, что последний самый красивый 🙂

Маршруты входа или регистрации могут быть установлены следующим образом

Page("login") { LoginPage() }
Page("registration") { RegistrationPage() }

Маршруты, связанные с параметрами

Page("article/:id") { ArticlePage() }

Идентификатор :id в приведенном выше примере является динамической частью маршрута. Мы можем получить этот идентификатор в классе ArticlePage, чтобы отобразить связанную с ним статью.

class ArticlePage: PageController {
    override func didLoad(with req: PageRequest) {
        if let articleId = req.parameters.get("id") {
            // Retrieve article here
        }
    }
}

:::подсказка В пути может быть более одного параметра. Получите их все одним и тем же способом.

:::

Запросы

Следующая интересная вещь на пути — это запрос, который также очень прост в использовании. Например, рассмотрим маршрут /search, который должен иметь параметры поиска text и age.

:::подсказка https://website.com/search**?text=Alex&age=19** — последняя часть — это запрос

.

:::

Просто объявите маршрут поиска

Page("search") { SearchPage() }

И получить данные запроса в классе 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)")
        }
    }
}

Все

Вы также можете использовать * для объявления маршрута, который принимает что-либо в определенной части пути, как здесь

Page("foo", "*", "bar") { SearchPage() }

Приведенный выше маршрут будет принимать все значения между foo и bar, например /foo/aaa/bar, /foo/bbb/bar и т. д.

Универсальный

С помощью знака ** вы можете установить специальный универсальный маршрут, который будет обрабатывать все, что не было сопоставлено с другими маршрутами на определенном пути.

Используйте его, чтобы создать глобальный маршрут 404

Page("**") { NotFoundPage() }

или для определенного пути, например. когда пользователь не найден

Page("user", "**") { UserNotFoundPage() }

Проясним ситуации с объявленными выше маршрутами

/user/1 — если есть маршрут для /user/:id, он вернет UserPage. Иначе он попадет в…

UserNotFoundPage

/user/1/hello — если есть маршрут для /user/:id/hello, то он попадет в UserNotFoundPage

/something — если маршрута для /something нет, то он попадет в NotFoundPage

Вложенная маршрутизация

Мы можем не захотеть заменить весь контент на странице для следующего маршрута, а только определенные блоки. Вот здесь и пригодится FragmentRouter!

Предположим, что у нас есть вкладки на странице /user. Каждая вкладка является подмаршрутом, и мы хотим реагировать на изменения в подмаршруте с помощью FragmentRouter.

Объявите маршрут верхнего уровня в классе App

.
Page("user") { UserPage() }

И объявите 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() }
            }
    }
}

В приведенном выше примере FragmentRouter обрабатывает подмаршруты /user/profile и /user/friends и отображает их на панели навигации, поэтому страница никогда не перезагружает весь контент, а только определенные фрагменты.

Также может быть объявлено более одного фрагмента с одинаковыми или разными подмаршрутами, и все они будут работать вместе, как по волшебству!

Кстати, FragmentRouter — это Div, и вы можете настроить его, вызвав

FragmentRouter(self)
    .configure { div in
        // do anything you want with the div
    }

Таблицы стилей

Вы можете использовать традиционные файлы CSS, но у вас также есть новая волшебная возможность использовать таблицу стилей, написанную на Swift!

Основы

Чтобы объявить правило CSS с помощью Swift, у нас есть объект Rule.

Его можно построить декларативно, вызвав его методы

Rule(...selector...)
    .alignContent(.baseline)
    .color(.red) // or rgba/hex color
    .margin(v: 0, h: .auto)

или способ, подобный SwiftUI, с использованием @resultBuilder

Rule(...selector...) {
    AlignContent(.baseline)
    Color(.red)
    Margin(v: 0, h: .auto)
}

Оба способа равны, однако я предпочитаю первый из-за автодополнения сразу после ввода . 😀

:::подсказка Доступны все методы CSS, описанные в MDN.

Более того, он автоматически обрабатывает префиксы браузера!

:::

Однако в некоторых случаях вы можете установить таким образом пользовательское свойство

Rule(...selector...)
  .custom("customKey", "customValue")

Селектор

Чтобы установить, на какие элементы должно влиять правило, мы должны установить селектор. Я вижу селектор как запрос в базе данных, но части этого селекторного запроса я называю указателями.

Самый простой способ создать указатель — это инициализировать его с помощью необработанной строки

Pointer("a")

Но правильный быстрый способ — создать его, вызвав .pointer в нужном HTML-теге, подобном этому

H1.pointer // h1
A.pointer // a
Pointer.any // *
Class("myClass").pointer // .myClass
Id("myId").pointer // #myId

Речь идет о базовых указателях, но они также имеют модификаторы, такие как :hover :first :first-child и т. д.

H1.pointer.first // h1:first
H1.pointer.firstChild // h1:first-child
H1.pointer.hover // h1:hover

Вы можете объявить любой существующий модификатор, все они доступны.

:::подсказка Если чего-то не хватает, не стесняйтесь сделать расширение, чтобы добавить это!

И не забудьте отправить запрос на включение на github, чтобы добавить его для всех.

:::

Вы также можете объединять указатели

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

Как использовать селектор в правиле

Rule(Pointer("a"))
// or
Rule(A.pointer)

Как использовать более одного селектора в правиле

Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer))

Он создает следующий код CSS

a, h1#myId, div > p {

}

Реактивность

Давайте объявим темный и светлый стили для нашего приложения, и позже мы сможем легко переключаться между ними.

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 и DarkStyle могут быть объявлены в отдельных файлах или, например. в 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)
    }
}

А потом где-то в UI какой-то страницы просто вызвать

App.current.theme = .light // to switch to light theme
// or
App.current.theme = .dark // to switch to dark theme

И это активирует или деактивирует соответствующие таблицы стилей! Разве это не круто? 😎

<цитата>

Но вы можете сказать, что описывать стили в Swift, а не в CSS, сложнее, так в чем смысл?

Главное - реактивность! Мы можем использовать @State со свойствами CSS и изменять значения на лету!

Просто взгляните, мы можем создать класс с некоторым реактивным свойством и изменить его в любое время во время выполнения, поэтому любой элемент на экране, который использует этот класс, будет обновлен! Это намного эффективнее, чем переключение классов для многих элементов!

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

Позже из любого места в коде просто позвоните

App.current.reactiveColor = .yellow // or any color you want

и он обновит цвет в таблице стилей и во всех элементах, которые его используют 😜

Кроме того, в таблицу стилей можно добавить необработанный CSS

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;
        }
        """
    }
}

вы можете смешивать необработанные строки CSS столько раз, сколько необходимо

Страницы

Маршрутизатор отображает страницы на каждом маршруте. Page — это любой класс, унаследованный от PageController.

PageController имеет методы жизненного цикла, такие как willLoad didLoad willUnload didUnload, методы пользовательского интерфейса buildUI и body, а также переменная-оболочка свойств для элементов HTML.

Технически PageController — это просто Div, и вы можете установить любые его свойства в методе 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
        // e.g.
        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")
        }
    }
}

Если ваша страница крошечная, вы можете объявить ее даже таким коротким способом

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 }

Разве это не красиво и лаконично? 🥲

Удобные бонусы

alert(message: String) — прямой метод JS alert

changePath(to: String) — переключение URL-пути

HTML-элементы

Наконец, я расскажу вам, как(!) создавать и использовать HTML-элементы!

Все элементы HTML с их атрибутами доступны в Swift, полный список, например. на MDN.

Просто пример короткого списка HTML-элементов:

| Код SwifWeb | HTML-код | |----|----| | Div() | <div></div> | | H1("текст") | <h1>текст</h1> | | A("Нажми меня").href("").target(.blank) | <a href="" target="_blank">Нажмите на меня</a> | | Button("Щелчок").onClick { print("Щелчок") } | <button onclick="…">Click</button> | | InputText($text).placeholder("Заголовок") | <input type="text" placeholder="title"> | | InputCheckbox($checked) | <input type="checkbox"> |

Как видите, получить доступ к любому тегу HTML в Swift очень просто, потому что все они представлены под одним и тем же именем, за исключением входных данных. Это связано с тем, что разные типы ввода имеют разные методы, и я не хотел их смешивать.

Простой Div

Div()

мы можем получить доступ ко всем его атрибутам и свойствам стиля следующим образом

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;">

Подклассы

:::информация HTML-элемент подкласса, чтобы предопределить для него стиль, или создать составной элемент с множеством предопределенных дочерних элементов и некоторыми удобными методами, доступными снаружи, или для достижения событий жизненного цикла, таких как didAddToDOM и didRemoveFromDOM.

:::

Давайте создадим элемент 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)
    }
}

:::предупреждение Очень важно вызывать суперметоды при создании подклассов.

Без этого вы можете столкнуться с неожиданным поведением.

:::

Добавление к DOM

Элемент можно добавить в DOM PageController или HTML-элемент сразу или позже.

Сразу

Div {
    H1("Title")
    P("Subtitle")
    Div {
        Ul {
            Li("One")
            Li("Two")
        }
    }
}

Или позже, используя lazy var

lazy var myDiv1 = Div()
lazy var myDiv2 = Div()

Div {
    myDiv1
    myDiv2
}

:::подсказка Таким образом, вы можете объявить HTML-элемент заранее и добавить его в DOM в любое время позже!

:::

Удаление из DOM

lazy var myDiv = Div()

Div {
    myDiv
}

// somewhere later
myDiv.remove()

Доступ к родительскому элементу

Любой элемент HTML имеет необязательное свойство superview, которое дает доступ к его родительскому элементу, если он добавлен в DOM

Div().superview?.backgroundColor(.red)

условия if/else

Нам часто нужно показывать элементы только при определенных условиях, поэтому давайте использовать для этого 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")
    }
}

Но это не реактивно. Если вы попытаетесь установить для showDiv2 значение false, ничего не произойдет.

Реактивный пример

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
}

:::информация Почему мы должны использовать $showDiv2.map {…} ?

:::

В сортировке: потому что это не SwiftUI. Совсем.

==Подробнее о== @State ==ниже.==

Необработанный HTML

Вам также может понадобиться добавить необработанный HTML-код на страницу или элемент HTML, и это легко сделать

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"]
}

CSS

То же, что и в примерах выше, но также доступна 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
    }
}

Вы можете использовать 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)
    }
}

Он также может принимать функцию в качестве аргумента

BuilderFunction(calculate) { calculatedValue in
    // CSS rule or DOM element
}

func calculate() -> Int {
    return 1 + 1
}

:::подсказка BuilderFunction также доступна для HTML-элементов :)

:::

Реакция с @State

@State — наиболее желанная вещь в настоящее время для декларативного программирования.

Как я уже сказал вам выше: это не SwiftUI, поэтому нет глобального конечного автомата, который все отслеживает и перерисовывает. И элементы HTML — это не временные структуры, а классы, поэтому они являются реальными объектами, и у вас есть прямой доступ к ним. Это намного лучше и гибче, у вас есть полный контроль.

Что скрывается под капотом?

Это оболочка свойства, которая уведомляет всех подписчиков о его изменениях.

Как подписаться на изменения?

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

Как элементы HTML могут реагировать на изменения?

Пример простого текста

@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

Пример простого числа

@State var height = 20.px

Div().height($height) // whenever height var changes it updates height of the Div

Простой логический пример

@State var hidden = false

Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div

Пример сопоставления

@State var isItCold = true

H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })

Сопоставление двух состояний

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

Сопоставление более двух состояний

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

:::подсказка Все свойства HTML и CSS могут обрабатывать значения @State

.

:::

Расширения

Расширить элементы HTML

Вы можете добавить несколько удобных методов к конкретным элементам, таким как Div

extension Div {
    func makeItBeautiful() {}
}

Или группы элементов, если известен их родительский класс.

Родительских классов немного.

BaseActiveStringElement — для элементов, которые можно инициализировать строкой, например a, h1 и т. д.

BaseContentElement — для всех элементов, внутри которых может быть содержимое, например div, ul и т. д.

BaseElement — для всех элементов

Таким образом можно написать расширение для всех элементов

extension BaseElement {
    func doSomething() {}
}

Объявить цвета

Класс

Color отвечает за цвета. Он имеет предопределенные HTML-цвета, но вы можете использовать свои собственные

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

Затем используйте его как H1("Text").color(.myColor1)

Объявить классы

extension Class {
    var my: Class { "my" }
}

Затем используйте его как Div().class(.my)

Объявить идентификаторы

extension Id {
    var myId: Id { "my" }
}

Затем используйте его как Div().id(.my)

Веб-API

Окно

Объект

window полностью упакован и доступен через переменную App.current.window.

Полный справочник доступен на MDN.

Давайте сделаем краткий обзор внизу

Флажок переднего плана

Вы можете прослушать его в Lifecycle в App.swift или прямо здесь

App.current.window.$isInForeground.listen { isInForeground in
    // foreground flag changed
}

или просто читайте в любое время в любом месте

if App.current.window.isInForeground {
    // do somethign
}

или реагировать на это с помощью элемента HTML

Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white })

Отметить как активный

Это то же самое, что флаг переднего плана, но доступный через App.current.window.isActive

Он определяет, взаимодействует ли пользователь внутри окна.

Сетевой статус

То же, что и флаг Foreground, но доступен через App.current.window.isOnline

Он определяет, есть ли у пользователя доступ к Интернету.

Статус темного режима

То же, что флаг переднего плана, но доступный через App.current.window.isDark

Он определяет, находится ли браузер или операционная система пользователя в темном режиме.

Внутренний размер

Размер области содержимого окна (окна просмотра), включая полосы прокрутки

App.current.window.innerSize — это объект Size со значениями width и height внутри.

Также доступно как переменная @State.

Внешний размер

Размер окна браузера, включая панели инструментов/полосы прокрутки.

App.current.window.outerSize — это объект Size со значениями width и height внутри.

Также доступно как переменная @State.

Экран

Специальный объект для проверки свойств экрана, на котором отображается текущее окно. Доступно через App.current.window.screen.

Наиболее интересным свойством обычно является pixelRatio.

История

Содержит URL-адреса, посещенные пользователем (в окне браузера).

Доступно через App.current.window.history или просто через History.shared.

Он доступен как переменная @State, поэтому при необходимости вы можете прослушивать его изменения.

App.current.window.$history.listen { history in
    // read history properties
}

Он также доступен как простая переменная

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

Дополнительные сведения доступны на MDN.

Местоположение

Содержит информацию о текущем URL.

Доступно через App.current.window.location или просто Location.shared.

Он доступен как переменная @State, поэтому при необходимости вы можете прослушивать его изменения.

Например, так работает маршрутизатор.

App.current.window.$location.listen { location in
    // read location properties
}

Он также доступен как простая переменная

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

Подробнее см. на MDN.

Навигатор

Содержит информацию о браузере.

Доступно через App.current.window.navigator или просто Navigator.shared

Наиболее интересные свойства обычно userAgent платформа язык cookieEnabled.

Локальное хранилище

Позволяет сохранять пары ключ/значение в веб-браузере. Хранит данные без ограничения срока действия.

Доступно как 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()

Отслеживание изменений

LocalStorage.onChange { key, oldValue, newValue in
    print("LocalStorage: key (key) has been updated")
}

Отслеживание удаления всех элементов

LocalStorage.onClear { print("LocalStorage: all items has been removed") }

Хранилище сеансов

Позволяет сохранять пары ключ/значение в веб-браузере. Сохраняет данные только для одного сеанса.

Доступно как App.current.window.sessionStorage или просто как SessionStorage.shared.

API абсолютно такой же, как в описанном выше LocalStorage.

Документ

Представляет любую веб-страницу, загруженную в браузер, и служит точкой входа в содержимое веб-страницы.

Доступно через 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]

Локализация

Статическая локализация

Классическая локализация выполняется автоматически и зависит от языка системы пользователя

Как использовать

H1(String(
    .en("Hello"),
    .fr("Bonjour"),
    .ru("Привет"),
    .es("Hola"),
    .zh_Hans("你好"),
    .ja("こんにちは")))

Динамическая локализация

Если вы хотите менять локализованные строки на экране "на лету" (без перезагрузки страницы)

Вы можете изменить текущий язык, позвонив

Localization.current = .es

Если вы сохранили язык пользователя где-то в файлах cookie или локальном хранилище, вам необходимо установить его при запуске приложения

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

XMLHttpЗапрос

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") эквивалентен console.log('Hello world') в JavaScript

Консольные методы тоже созданы с любовью ❤️

Console.dir(...)
Console.error(...)
Console.warning(...)
Console.clear()

Предварительный просмотр

Чтобы предварительный просмотр в реальном времени работал, объявите класс WebPreview в каждом файле, который вы хотите.

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

Xcode

Прочитайте инструкции на странице репозитория. Это сложное, но вполне рабочее решение 😎

VSCode

Перейдите в раздел Расширения внутри VSCode и выполните поиск Webber.

После установки нажмите Cmd+Shift+P (или Ctrl+Shift+P в Linux/Windows)

Найдите и запустите Webber Live Preview.

С правой стороны вы увидите окно предварительного просмотра в реальном времени, которое обновляется всякий раз, когда вы сохраняете файл, содержащий класс WebPreview.

Доступ к JavaScript

Он доступен через JavaScriptKit, который является основой SwifWeb.

Прочитайте, как вам это сделать, в официальном репозитории.

Ресурсы

Вы можете добавить css, js, png, jpg и любые другие статические ресурсы внутри проект.

Но чтобы они были доступны во время отладки или в окончательных файлах выпуска, вы должны объявить их все в Package.swift следующим образом< /p>

.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")
]),

Позже вы сможете получить к ним доступ, например. вот так Img().src("/images/logo.png")

Отладка

Запустите Webber следующим образом

webber serve просто для быстрого запуска

webber serve -t pwa -s Service для запуска в режиме PWA

Дополнительные параметры

-v или --verbose, чтобы отобразить дополнительную информацию в консоли для целей отладки

-p 443 или --port 443 для запуска webber-сервера на порту 443 вместо стандартного 8888

--browser chrome/safari для автоматического открытия нужного браузера, по умолчанию он не открывается

--browser-self-signed необходим для локальной отладки сервис-воркеров, иначе они не работают

--browser-incognito, чтобы открыть дополнительный экземпляр браузера в режиме инкогнито, работает только с Chrome

Таким образом, чтобы создать приложение в режиме отладки, автоматически открывайте его в Chrome и автоматически обновляйте браузер всякий раз, когда вы меняете какой-либо файл, запускайте его таким образом

для СПА

webber serve --browser chrome

для реального тестирования PWA

webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito

Первоначальная загрузка приложения

Возможно, вы захотите улучшить процесс начальной загрузки.

Для этого просто откройте папку .webber/entrypoint/dev внутри проекта и отредактируйте файл index.html.

Он содержит исходный код HTML с очень полезными слушателями: WASMLoadingStarted WASMLoadingStartedWithoutProgress WASMLoadingProgress WASMLoadingError.

Вы можете изменить этот код так, как хотите, чтобы реализовать свой собственный стиль 🔥

:::информация Когда вы закончите новую реализацию, не забудьте сохранить ее в папку .webber/entrypoint/release

.

:::

Выпуск сборки

Просто выполните webber release или webber release -t pwa -s Service для PWA.

Затем возьмите скомпилированные файлы из папки .webber/release и загрузите их на свой сервер.

Как развернуть

Вы можете загружать свои файлы на любой статический хостинг.

:::предупреждение Хостинг должен предоставлять правильный Content-Type для файлов wasm!

:::

Да, очень важно иметь правильный заголовок Content-Type: application/wasm для файлов wasm, иначе, к сожалению, браузер не сможет загрузить ваш WebAssembly приложение.

:::информация Например, GithubPages не предоставляет правильный Content-Type для файлов wasm, поэтому, к сожалению, на нем невозможно разместить сайты WebAssembly.

:::

Nginx

Если вы используете собственный сервер с nginx, откройте /etc/nginx/mime.types и проверьте, содержит ли он запись application/wasm wasm;. Если да, то все готово!

Заключение

Надеюсь, я поразил вас сегодня, и вы, по крайней мере, попробуете SwifWeb и, как максимум, начнете использовать его для своего следующего крупного веб-проекта!

Пожалуйста, не стесняйтесь вносить свой вклад в любую из библиотек SwifWeb, а также помечать ⭐️их всех!

У меня есть отличное сообщество SwiftStream в Discord, где вы можете найти огромную поддержку, прочитать небольшие руководства и первыми получать уведомления о любых предстоящих обновлениях! Было бы здорово увидеть вас с нами!

Это только начало, так что следите за новыми статьями о SwifWeb!

Расскажи друзьям!


Оригинал