Web 開発の世界は広大で、毎日出現する新しいテクノロジの絶え間ない流れの中で迷子になりがちです。これらの新しいテクノロジーのほとんどは、JavaScript または TypeScript を使用して構築されています。しかし、この記事では、ブラウザー内で直接ネイティブ Swift を使用して Web 開発を行う方法を紹介します。
Swift が Web ページでネイティブに動作するには、最初に WebAssembly バイトコードにコンパイルする必要があり、その後 JavaScript がそのコードをページにロードできます。特別なツールチェーンを使用してヘルパー ファイルを作成する必要があるため、コンパイルのプロセス全体は少しトリッキーです。そのため、Carton と Webber というヘルパー CLI ツールが用意されています。
SwiftWasmコミュニティは、元の Swift ツールチェーンにパッチを適用することにより、Swift を WebAssembly にコンパイルできるようにするために多大な作業を行いました。毎日、元のツールチェーンから変更を自動的にプルし、テストが失敗した場合はフォークを修正することで、ツールチェーンを更新します。彼らの目標は、公式ツールチェーンの一部になることであり、近い将来それが実現することを望んでいます。
Cartonは SwiftWasm コミュニティによって作成され、SwiftUI を使用して Web サイトを作成できるフレームワークである Tokamak プロジェクトに使用できます。
Webber はSwifWeb プロジェクト用に作られています。 SwifWeb は、すべての Web API だけでなく、HTML および CSS 標準全体をラップするという点で異なります。
コードの一貫性のために SwiftUI を使用して Web アプリを作成することを好むかもしれませんが、Web 開発は本質的に異なり、SwiftUI と同じ方法でアプローチすることはできないため、これは間違ったアプローチだと思います。
そのため、私は SwifWeb を作成しました。これにより、HTML、CSS、および Web API のすべての機能を Swift から直接使用でき、オートコンプリートとドキュメントを備えた美しい構文を使用できます。また、Webber ツールを作成したのは、Carton が SwifWeb アプリ用に作成されていないため、SwifWeb アプリを正しい方法でコンパイル、デバッグ、デプロイできないためです。
私の名前は Mikhail Isaev で、SwifWeb の作成者です。この記事では、SwifWeb を使用して Web サイトの構築を開始する方法を紹介します。
Swift をインストールする必要があります。最も簡単な方法は次のとおりです。
それ以外の場合は、公式 Web サイトのインストール手順をご覧ください。
アプリのビルド、デバッグ、デプロイを支援する Webber を作成しました。
macOS では、HomeBrew を使用して簡単にインストールできます ( Web サイトからインストールしてください)。
brew install swifweb/tap/webber
後で最新バージョンに更新するには、実行するだけです
brew upgrade webber
Ubuntu または Windows (WSL2 の Ubuntu) では、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
の追加引数
アプリの種類
プログレッシブ Web アプリの-t pwa
-t spa
for Single Web App
Service Worker ターゲットの名前 (通常、PWA プロジェクトではService
という名前)
-s Service
アプリ ターゲットの名前 (既定ではApp
)
-a App
コンソールに詳細情報を表示
-v
Webber サーバーのポート (デフォルトは8888
)
-p 8080
-p 443
を使用して、実際の SSL のようにテストします (自己署名 SSL 設定を許可)
自動起動先ブラウザ名
--browser safari
または--browser chrome
Service-Worker をデバッグするための自己署名 SSL 設定が許可されたブラウザーの追加インスタンス
--browser-self-signed
シークレット モードのブラウザの追加インスタンス
--browser-incognito
アプリは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 クラスですべての最上位ルートを宣言する必要があります。
トップレベルとは、これらのルートで宣言されたページがウィンドウ内のすべてのスペースを占有することを意味します。
たとえば、ルート ルートは 3 つの方法で設定できます。
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 } } }
パスには複数のパラメーターを含めることができます。すべて同じ方法で取得します。
パスで次に興味深いのはqueryです。これも非常に使いやすいです。たとえば、検索text
とage
クエリ パラメーターが必要な/searchルートを考えてみましょう。
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() }
そしてUserPageクラスでFragmentRouter を宣言します
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サブルートを処理し、それをNavbarの下にレンダリングするため、ページはコンテンツ全体をリロードすることはなく、特定のフラグメントのみをリロードします。
同じまたは異なるサブルートを持つ複数のフラグメントを宣言することもでき、それらはすべて魔法のように連携します!
ところでFragmentRouterはDivであり、呼び出すことで構成できます
FragmentRouter(self) .configure { div in // do anything you want with the div }
従来の CSS ファイルを使用できますが、Swift で記述されたスタイルシートを使用する新しい魔法の機能も備えています。
Swift を使用して CSS ルールを宣言するために、 Ruleオブジェクトがあります。
メソッドを呼び出すことで宣言的に構築できます
Rule(...selector...) .alignContent(.baseline) .color(.red) // or rgba/hex color .margin(v: 0, h: .auto)
または @resultBuilder を使用した SwiftUI のような方法
Rule(...selector...) { AlignContent(.baseline) Color(.red) Margin(v: 0, h: .auto) }
どちらの方法も同じですが、入力した直後にオートコンプリートされるため、最初の方法を好みます.
😀
MDN で説明されているすべての CSS メソッドが利用可能です。
それ以上に、ブラウザのプレフィックスを自動的に処理します!
ただし、特定のケースでは、この方法でカスタム プロパティを設定できます。
Rule(...selector...) .custom("customKey", "customValue")
Rule が影響を与える要素を設定するには、セレクターを設定する必要があります。セレクターはデータベース内のクエリと見なされますが、そのセレクター クエリの一部をポインターと呼んでいます。
ポインターを構築する最も簡単な方法は、生の文字列を使用して初期化することです
Pointer("a")
しかし、正しい迅速な方法は、このように必要な HTML タグで.pointer
を呼び出してビルドすることです
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
そして、関連するスタイルシートを有効または無効にします!かっこよくないですか? 😎
しかし、CSS の代わりに Swift でスタイルを記述するのは難しいと言うかもしれません。
ポイントは反応性! @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
、UI メソッド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 // 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") } } }
あなたのページが小さい場合は、この短い方法でも宣言できます
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 要素は Swift で利用できます。完全なリストはMDNなどにあります。
HTML 要素の短いリストの例:
SwifWeb コード | HTMLコード |
---|---|
| |
| |
| |
| |
| |
| |
ご覧のとおり、Swift で HTML タグにアクセスするのは非常に簡単です。入力を除いて、それらはすべて同じ名前で表されているからです。これは、異なる入力タイプには異なるメソッドがあり、それらを混在させたくなかったためです。
単純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
などのライフサイクル イベントを実現します。
単なるDiv
であるが定義済みの.divider
クラスを持つ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) } }
サブクラス化するときは、スーパー メソッドを呼び出すことが非常に重要です。
これがないと、予期しない動作が発生する可能性があります。
Element は、すぐにまたは後でPageControllerまたはHTML 要素の DOM に追加できます。
すぐに
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 に追加できます。
lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()
すべての HTML 要素には、DOM に追加された場合にその親へのアクセスを提供するオプションのスーパービュー プロパティがあります。
Div().superview?.backgroundColor(.red)
特定の条件でのみ要素を表示する必要があることが多いので、そのために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 要素に追加する必要がある場合もありますが、それは簡単に可能です。
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"] }
上記の例と同じですが、 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 } }
ForEach
ループでBuilderFunction
使用して、次の例のdelay
値のように、値を 1 回だけ計算できます。
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
は、今日の宣言型プログラミングにとって最も望ましいものです。
上記で説明したように、これは 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)") }
簡単なテキストの例
@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 😌" })
2 つの状態のマッピング
@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 })
3 つ以上の州のマッピング
@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
値を処理できます
Div のような具体的な要素にいくつかの便利なメソッドを追加できます
extension Div { func makeItBeautiful() {} }
または、親class
がわかっている場合は要素のグループ。
親子クラスは少ないです。
BaseActiveStringElement
- a
、 h1
などの文字列で初期化できる要素用です。
BaseContentElement
- div
、 ul
など、内部にコンテンツを持つことができるすべての要素用です。
BaseElement
- すべての要素用です
したがって、すべての要素の拡張はこのように記述できます
extension BaseElement { func doSomething() {} }
カラークラスは色を担当します。定義済みの 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)
のように使用します
window
オブジェクトは完全にラップされ、 App.current.window
変数を介してアクセスできます。
完全なリファレンスはMDNで入手できます。
以下で簡単な概要を説明しましょう
App.swift
のLifecycle
で、またはこの方法で直接リッスンできます。
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 })
Foreground フラグと同じですが、 App.current.window.isActive
からアクセスできます。
ユーザーがまだウィンドウ内で対話しているかどうかを検出します。
Foreground フラグと同じですが、 App.current.window.isOnline
からアクセスできます
ユーザーがまだインターネットにアクセスできるかどうかを検出します。
Foreground フラグと同じですが、 App.current.window.isDark
からアクセスできます
ユーザーのブラウザーまたはオペレーティング システムがダーク モードであるかどうかを検出します。
スクロールバーを含むウィンドウのコンテンツ領域 (ビューポート) のサイズ
App.current.window.innerSize
、内部のwidth
とheight
値内のSizeオブジェクトです。
@State
変数としても使用できます。
ツールバー/スクロールバーを含むブラウザ ウィンドウのサイズ。
App.current.window.outerSize
、内部のwidth
とheight
値内のSizeオブジェクトです。
@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
platform
language
cookieEnabled
。
キーと値のペアを Web ブラウザーに保存できます。有効期限なしでデータを保存します。
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") }
キーと値のペアを Web ブラウザーに保存できます。 1 つのセッションだけのデータを保存します。
App.current.window.sessionStorage
または単にSessionStorage.shared
として利用できます。
API は上記のLocalStorageとまったく同じです。
ブラウザーに読み込まれた Web ページを表し、Web ページのコンテンツへのエントリ ポイントとして機能します。
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)") } } } }
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“)
JavaScript のconsole.log('Hello world')
に相当します
コンソールメソッドも愛で包まれています❤️
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() } }
リポジトリページの説明をお読みください。トリッキーですが、完全に機能するソリューションです😎
VSCode内のExtensionsに移動し、 Webberを検索します。
インストールしたら、 Cmd+Shift+P
(Linux/Windows ではCtrl+Shift+P
) を押します。
Webber Live Preview
を見つけて起動します。
右側にライブ プレビュー ウィンドウが表示され、 WebPreviewクラスを含むファイルを保存するたびに更新されます。
SwifWebの基盤であるJavaScriptKitを通じて利用できます。
公式リポジトリにある方法をお読みください。
css
、 js
、 png
、 jpg
、およびプロジェクト内のその他の静的リソースを追加できます。
ただし、デバッグ中または最終リリースファイルでそれらを使用できるようにするには、次のように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") ]),
後で、このImg().src(“/images/logo.png“)
のようにアクセスできるようになります。
次の方法でWebberを起動します
webber serve
すばやく起動するためだけに
webber serve -t pwa -s Service
-v
または--verbose
デバッグ目的でコンソールに詳細情報を表示します
-p 443
または--port 443
デフォルトの 8888 の代わりに 443 ポートで Webber サーバーを起動します
--browser chrome/safari
すると、目的のブラウザが自動的に開きます。デフォルトでは何も開きません
--browser-self-signed
Service Worker をローカルでデバッグするために必要です。そうしないと機能しません
--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
for PWA を実行するだけです。
次に、コンパイルされたファイルを.webber/release
フォルダーから取得し、サーバーにアップロードします。
ファイルを任意の静的ホスティングにアップロードできます。
ホスティングはwasmファイルに正しい Content-Type を提供する必要があります!
はい、 wasmファイルの正しいヘッダーContent-Type: application/wasm
を持つことが非常に重要です。そうしないと、残念ながらブラウザーは WebAssembly アプリケーションをロードできません。
たとえば、GithubPages はwasmファイルの正しい Content-Type を提供しないため、残念ながら WebAssembly サイトをホストすることはできません。
nginx で独自のサーバーを使用している場合は、 /etc/nginx/mime.types
を開き、 application/wasm wasm;
記録。はいの場合は、準備完了です。
少なくとも SwifWeb を試してみて、最大で次の大きな Web プロジェクトで SwifWeb を使い始めるようになることを願っています。
SwifWeb ライブラリのいずれかに自由に貢献し、それらすべてにスター ⭐️ を付けてください!
Discord には素晴らしい SwiftStream コミュニティがあり、大きなサポートを見つけたり、小さなチュートリアルを読んだり、今後の更新について最初に通知を受け取ることができます!私たちと一緒に会えたら最高です!
これはまだ始まったばかりなので、SwifWeb に関する記事を今後もお楽しみに!
友達に教えて!