Web 開発の世界は広大で、毎日出現する新しいテクノロジの絶え間ない流れの中で迷子になりがちです。これらの新しいテクノロジーのほとんどは、JavaScript または TypeScript を使用して構築されています。しかし、この記事では、ブラウザー内で直接ネイティブ Swift を使用して Web 開発を行う方法を紹介します。 それはどのように可能ですか? Swift が Web ページでネイティブに動作するには、最初に WebAssembly バイトコードにコンパイルする必要があり、その後 JavaScript がそのコードをページにロードできます。特別なツールチェーンを使用してヘルパー ファイルを作成する必要があるため、コンパイルのプロセス全体は少しトリッキーです。そのため、Carton と Webber というヘルパー CLI ツールが用意されています。 サードパーティのツールチェーンを使用しても問題ありませんか? コミュニティは、元の Swift ツールチェーンにパッチを適用することにより、Swift を WebAssembly にコンパイルできるようにするために多大な作業を行いました。毎日、元のツールチェーンから変更を自動的にプルし、テストが失敗した場合はフォークを修正することで、ツールチェーンを更新します。彼らの目標は、公式ツールチェーンの一部になることであり、近い将来それが実現することを望んでいます。 SwiftWasm カートンかウェバーか? は SwiftWasm コミュニティによって作成され、SwiftUI を使用して Web サイトを作成できるフレームワークである Tokamak プロジェクトに使用できます。 Carton SwifWeb プロジェクト用に作られています。 SwifWeb は、すべての Web API だけでなく、HTML および CSS 標準全体をラップするという点で異なります。 Webber は コードの一貫性のために SwiftUI を使用して Web アプリを作成することを好むかもしれませんが、Web 開発は本質的に異なり、SwiftUI と同じ方法でアプローチすることはできないため、これは間違ったアプローチだと思います。 そのため、私は SwifWeb を作成しました。これにより、HTML、CSS、および Web API のすべての機能を Swift から直接使用でき、オートコンプリートとドキュメントを備えた美しい構文を使用できます。また、Webber ツールを作成したのは、Carton が SwifWeb アプリ用に作成されていないため、SwifWeb アプリを正しい方法でコンパイル、デバッグ、デプロイできないためです。 私の名前は Mikhail Isaev で、SwifWeb の作成者です。この記事では、SwifWeb を使用して Web サイトの構築を開始する方法を紹介します。 必要なツール 迅速 Swift をインストールする必要があります。最も簡単な方法は次のとおりです。 macOSではXcodeをインストールすることです Linux または Windows (WSL2) では、 のスクリプトを使用します。 swiftlang.xyz それ以外の場合は をご覧ください。 、公式 Web サイトのインストール手順 ウェバー CLI アプリのビルド、デバッグ、デプロイを支援する 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 for Single Web App -t spa Service Worker ターゲットの名前 (通常、PWA プロジェクトでは という名前) Service -s Service アプリ ターゲットの名前 (既定では ) App -a App コンソールに詳細情報を表示 -v Webber サーバーのポート (デフォルトは ) 8888 -p 8080 を使用して、実際の SSL のようにテストします (自己署名 SSL 設定を許可) -p 443 自動起動先ブラウザ名 または --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 ここで最も役立つメソッドは です。アプリを構成するのに最適な場所だからです。まるで iOS アプリのようです。 😀 didFinishLaunching には便利な便利なメソッドが含まれています。 app を呼び出して、PWA サービス ワーカーを登録します。 registerServiceWorker(“serviceName“) 呼び出しで相対スクリプトまたは外部スクリプトを追加 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 }) } } と 別のファイルまたはたとえば App.swift で宣言できます。 LightStyle DarkStyle は、 class LightStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.white) Rule(H1.pointer).color(.black) } } class DarkStyle: Stylesheet { @Rules override var rules: Rules.Content { Rule(Body.pointer).backgroundColor(.black) Rule(H1.pointer).color(.white) } } そして、あるページの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 には、 、UI メソッド および 、HTML 要素のプロパティ ラッパー変数などのライフサイクル メソッドがあります。 PageController willLoad didLoad willUnload didUnload buildUI body 技術的には、 単なる Div であり、 メソッドでそのプロパティを設定できます。 PageController は buildUI class IndexPage: PageController { // MARK: - Lifecycle override func willLoad(with req: PageRequest) { super.willLoad(with: req) } override func didLoad(with req: PageRequest) { super.didLoad(with: req) // set page title and metaDescription self.title = "My Index Page" self.metaDescription = "..." // also parse query and hash here } override func willUnload() { super.willUnload() } override func didUnload() { super.didUnload() } // MARK: - UI override func buildUI() { super.buildUI() // access any properties of the page's div here // eg self.backgroundcolor(.lightGrey) // optionally call body method here to add child HTML elements body { P("Hello world") } // or alternatively self.appendChild(P("Hello world")) } // the best place to declare any child HTML elements @DOM override var body: DOM.Content { H1("Hello world") P("Text under title") Button("Click me") { self.alert("Click!") print("button clicked") } } } あなたのページが小さい場合は、この短い方法でも宣言できます 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 } それは美しく簡潔ではありませんか? 🥲 ボーナスの便利な方法 - 直接の JS メソッド alert(message: String) alert - URL パスの切り替え changePath(to: String) HTML 要素 最後に、HTML 要素を作成して使用する方法 (!) について説明します。 属性を持つすべての HTML 要素は Swift で利用できます。完全なリストは などにあります。 MDN HTML 要素の短いリストの例: SwifWeb コード HTMLコード Div() <div></div> H1(“text“) <h1>text</h1> A(“Click me“).href(““).target(.blank) <a href=”” target=”_blank”>Click me</a> Button(“Click“).onClick { print(“click“) } <button onclick=”…”>Click</button> InputText($text).placeholder("Title") <input type=”text” placeholder=”title”> InputCheckbox($checked) <input type=”checkbox”> ご覧のとおり、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) } } サブクラス化するときは、スーパー メソッドを呼び出すことが非常に重要です。 これがないと、予期しない動作が発生する可能性があります。 DOM への追加 Element は、すぐにまたは後で または の 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 } そのため、事前に を宣言し、後でいつでも DOM に追加できます。 HTML 要素 DOM からの削除 lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove() 親要素にアクセスする すべての HTML 要素には、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> """ } ForEach 静的な例 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 } } ループで 使用して、次の例の 値のように、値を 1 回だけ計算できます。 ForEach BuilderFunction 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 } は HTML 要素でも利用できます:) BuilderFunction @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 😌" }) 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 拡張機能 HTML 要素を拡張する 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) ID を宣言する extension Id { var myId: Id { "my" } } 次に、 のように使用します Div().id(.my) Web API 窓 オブジェクトは完全にラップされ、 変数を介してアクセスできます。 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)") } } } } XMLHttpRequest import XMLHttpRequest XMLHttpRequest() .open(method: "GET", url: "https://jsonplaceholder.typicode.com/todos/1") .onAbort { print("XHR onAbort") }.onLoad { print("XHR onLoad") }.onError { print("XHR onError") }.onTimeout { print("XHR onTimeout") }.onProgress{ progress in print("XHR onProgress") }.onLoadEnd { print("XHR onLoadEnd") }.onLoadStart { print("XHR onLoadStart") }.onReadyStateChange { readyState in print("XHR onReadyStateChange") } .send() WebSocket import WebSocket let webSocket = WebSocket("wss://echo.websocket.org").onOpen { print("ws connected") }.onClose { (closeEvent: CloseEvent) in print("ws disconnected code: \(closeEvent.code) reason: \(closeEvent.reason)") }.onError { print("ws error") }.onMessage { message in print("ws message: \(message)") switch message.data { case .arrayBuffer(let arrayBuffer): break case .blob(let blob): break case .text(let text): break case .unknown(let jsValue): break } } Dispatch.asyncAfter(2) { // send as simple string webSocket.send("Hello from SwifWeb") // send as Blob webSocket.send(Blob("Hello from SwifWeb")) } コンソール 単純な JavaScript の に相当します print(“Hello world“) 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() } } Xcode の説明をお読みください。トリッキーですが、完全に機能するソリューションです😎 リポジトリページ VSコード 内の に移動し、 を検索します。 VSCode Extensions Webber インストールしたら、 (Linux/Windows では ) を押します。 Cmd+Shift+P Ctrl+Shift+P を見つけて起動します。 Webber Live Preview 右側にライブ プレビュー ウィンドウが表示され、 クラスを含むファイルを保存するたびに更新されます。 WebPreview JavaScript へのアクセス の基盤である を通じて利用できます。 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 または デフォルトの 8888 の代わりに 443 ポートで Webber サーバーを起動します -p 443 --port 443 すると、目的のブラウザが自動的に開きます。デフォルトでは何も開きません --browser chrome/safari Service Worker をローカルでデバッグするために必要です。そうしないと機能しません --browser-self-signed モードでブラウザーの追加のインスタンスを開くには、chrome でのみ機能します。 --browser-incognito したがって、アプリをデバッグ モードでビルドするには、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 建物のリリース または for PWA を実行するだけです。 webber release webber release -t pwa -s Service 次に、コンパイルされたファイルを フォルダーから取得し、サーバーにアップロードします。 .webber/release 導入方法 ファイルを任意の静的ホスティングにアップロードできます。 ホスティングは ファイルに正しい Content-Type を提供する必要があります! wasm はい、 ファイルの正しいヘッダー を持つことが非常に重要です。そうしないと、残念ながらブラウザーは WebAssembly アプリケーションをロードできません。 wasm Content-Type: application/wasm たとえば ファイルの正しい Content-Type を提供しないため、残念ながら WebAssembly サイトをホストすることはできません。 、GithubPages は wasm ニンクス nginx で独自のサーバーを使用している場合は、 を開き、 記録。はいの場合は、準備完了です。 /etc/nginx/mime.types application/wasm wasm; 結論 少なくとも SwifWeb を試してみて、最大で次の大きな Web プロジェクトで SwifWeb を使い始めるようになることを願っています。 のいずれかに自由に貢献し、それらすべてにスター ⭐️ を付けてください! SwifWeb ライブラリ あり、大きなサポートを見つけたり、小さなチュートリアルを読んだり、今後の更新について最初に通知を受け取ることができます!私たちと一緒に会えたら最高です! Discord には素晴らしい SwiftStream コミュニティが これはまだ始まったばかりなので、SwifWeb に関する記事を今後もお楽しみに! 友達に教えて!