Как использовать 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!
Расскажи друзьям!
Оригинал