웹 개발 세계는 광활하며, 매일 새롭게 등장하는 끊임없는 새로운 기술의 흐름 속에서 길을 잃기 쉽습니다. 이러한 새로운 기술의 대부분은 JavaScript 또는 TypeScript를 사용하여 구축되었습니다. 하지만 이 기사에서는 브라우저 내에서 직접 네이티브 Swift를 사용하여 웹 개발을 하는 방법을 소개할 것이며 이것이 여러분에게 깊은 인상을 줄 것이라고 확신합니다. 그게 어떻게 가능해? Swift가 웹 페이지에서 기본적으로 작동하려면 먼저 WebAssembly 바이트 코드로 컴파일되어야 하며 그런 다음 JavaScript가 해당 코드를 페이지에 로드할 수 있습니다. 특수 도구 체인을 사용하고 도우미 파일을 빌드해야 하기 때문에 전체 컴파일 프로세스가 약간 까다롭습니다. 이것이 바로 Carton 및 Webber와 같은 도우미 CLI 도구를 사용할 수 있는 이유입니다. 타사 툴체인을 사용해도 되나요? 커뮤니티는 원래 Swift 툴체인을 패치하여 Swift를 WebAssembly로 컴파일할 수 있도록 엄청난 양의 작업을 수행했습니다. 매일 원래 도구 체인에서 변경 사항을 자동으로 가져오고 테스트가 실패하면 포크를 수정하여 도구 체인을 업데이트합니다. 그들의 목표는 공식 툴체인의 일부가 되는 것이며 가까운 시일 내에 그렇게 되기를 희망합니다. SwiftWasm 카톤 또는 웨버? SwiftWasm 커뮤니티에서 제작되었으며 SwiftUI를 사용하여 웹 사이트를 작성할 수 있는 기능을 제공하는 프레임워크인 Tokamak 프로젝트에 사용할 수 있습니다. Carton은 SwifWeb 프로젝트용으로 만들어졌습니다. SwifWeb은 전체 HTML 및 CSS 표준은 물론 모든 웹 API를 포함한다는 점에서 다릅니다. Webber는 코드 일관성을 위해 SwiftUI를 사용하여 웹 앱을 작성하는 것을 선호할 수도 있지만 웹 개발은 본질적으로 다르며 SwiftUI와 동일한 방식으로 접근할 수 없기 때문에 이것이 잘못된 접근 방식이라고 생각합니다. 이것이 바로 제가 자동 완성 및 문서화 기능이 포함된 아름다운 구문을 사용하여 Swift에서 직접 HTML, CSS 및 웹 API의 모든 기능을 사용할 수 있는 기능을 제공하는 SwifWeb을 만든 이유입니다. 그리고 Webber 도구를 만든 이유는 Carton이 SwifWeb 앱을 위해 만들어지지 않았기 때문에 SwifWeb 앱을 올바른 방식으로 컴파일, 디버깅 및 배포할 수 없기 때문입니다. 제 이름은 Mikhail Isaev이고 SwifWeb의 저자입니다. 이 기사에서는 SwifWeb을 사용하여 웹사이트 구축을 시작하는 방법을 보여 드리겠습니다. 필수 도구 빠른 Swift를 설치해야 합니다. 가장 쉬운 방법은 다음과 같습니다. macOS에서는 Xcode를 설치하는 것입니다 Linux 또는 Windows(WSL2)에서는 의 스크립트를 사용하는 것입니다. Swiftlang.xyz 다른 경우에는 살펴보세요. 공식 웹사이트의 설치 지침을 웨버 CLI 저는 여러분의 앱 구축, 디버그, 배포를 돕기 위해 Webber를 만들었습니다. macOS에서는 HomeBrew를 사용하여 쉽게 설치할 수 있습니다(홈브루 에서 설치). 웹사이트 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 앱 유형 프로그레시브 웹 앱의 경우 -t pwa 단일 웹 앱용 -t spa 서비스 작업자 대상의 이름(일반적으로 PWA 프로젝트에서는 라고 함) Service -s Service 앱 대상 이름(기본적으로 ) App -a App 콘솔에서 추가 정보 인쇄 -v Webber 서버용 포트(기본값은 ) 8888 -p 8080 실제 SSL처럼 테스트하려면 사용하세요(자체 서명된 SSL 설정이 허용됨). -p 443 자동으로 시작할 대상 브라우저 이름 또는 --browser safari --browser chrome 서비스 워커 디버깅을 위해 자체 서명된 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 클래스에서 모든 최상위 경로를 선언해야 합니다. 최상위 수준은 이러한 경로에 선언된 페이지가 창의 전체 공간을 차지한다는 의미입니다. 좋습니다. 예를 들어 루트 경로는 세 가지 방법으로 설정할 수 있습니다. 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 } } } 경로에 둘 이상의 매개변수가 있을 수 있습니다. 같은 방법으로 모두 검색하세요. 쿼리 경로에서 다음으로 흥미로운 점은 사용하기 매우 쉬운 입니다. 예를 들어, 검색 와 쿼리 매개변수가 있을 것으로 예상되는 경로를 고려해 보겠습니다. 쿼리 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 를 UserNotFound페이지 /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 동일하거나 다른 하위 경로를 가진 두 개 이상의 조각이 선언될 수도 있으며 모두 마술처럼 함께 작동합니다! Btw 는 이며 호출하여 구성할 수 있습니다. 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") 선택자 규칙이 영향을 미치는 요소를 설정하려면 선택기를 설정해야 합니다. 선택기는 데이터베이스의 쿼리로 표시되지만 해당 선택기 쿼리의 일부는 포인터라고 합니다. 포인터를 만드는 가장 쉬운 방법은 원시 문자열을 사용하여 포인터를 초기화하는 것입니다. 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로 스타일을 설명하는 것이 더 어렵다고 말할 수도 있습니다. 그렇다면 요점은 무엇입니까? 핵심은 반응성! CSS 속성과 함께 @State를 사용하고 즉시 값을 변경할 수 있습니다! 살펴보세요. 일부 반응성 속성이 포함된 클래스를 생성하고 런타임 중에 언제든지 변경할 수 있으므로 해당 클래스를 사용하는 화면의 모든 요소가 업데이트됩니다! 많은 요소에 대해 클래스를 전환하는 것보다 훨씬 더 효과적입니다! 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에 추가 요소는 즉시 또는 나중에 의 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에 추가된 경우 상위 요소에 대한 액세스를 제공하는 선택적 superview 속성이 있습니다. 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 } } 다음 예제의 값처럼 루프에서 사용하여 일부 값을 한 번만 계산할 수 있습니다. delay ForEach BuilderFunction 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 😌" }) 두 상태 매핑 @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() {} } 또는 상위 알고 있는 경우 요소 그룹입니다. class 부모 클래스가 거의 없습니다. - , 등과 같이 문자열로 초기화할 수 있는 요소용입니다. BaseActiveStringElement a h1 - , 등과 같이 내부에 콘텐츠를 포함할 수 있는 모든 요소에 사용됩니다. BaseContentElement div ul - 모든 요소에 사용됩니다. BaseElement 따라서 모든 요소에 대한 확장은 이런 식으로 작성할 수 있습니다. extension BaseElement { func doSomething() {} } 색상 선언 클래스는 색상을 담당합니다. HTML 색상이 미리 정의되어 있지만 자신만의 색상을 가질 수 있습니다. Color extension Color { var myColor1: Color { .hex(0xf1f1f1) } // which is 0xF1F1F1 var myColor2: Color { .hsl(60, 60, 60) } // which is hsl(60, 60, 60) var myColor3: Color { .hsla(60, 60, 60, 0.8) } // which is hsla(60, 60, 60, 0.8) var myColor4: Color { .rgb(60, 60, 60) } // which is rgb(60, 60, 60) var myColor5: Color { .rgba(60, 60, 60, 0.8) } // which is rgba(60, 60, 60, 0.8) } 그런 다음 처럼 사용하십시오. H1(“Text“).color(.myColor1) 클래스 선언 extension Class { var my: Class { "my" } } 그런 다음 처럼 사용하십시오. Div().class(.my) ID 선언 extension Id { var myId: Id { "my" } } 그런 다음 처럼 사용하십시오. Div().id(.my) 웹 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 로컬스토리지 웹 브라우저에 키/값 쌍을 저장할 수 있습니다. 만료 날짜 없이 데이터를 저장합니다. 또는 로 사용할 수 있습니다. 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 사용자의 언어를 쿠키나 로컬 저장소에 저장한 경우 앱 실행 시 설정해야 합니다. 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")) } 콘솔 간단한 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 의 지침을 읽어보세요. 까다롭지만 완벽하게 작동하는 솔루션입니다 🙂 저장소 페이지 VSCode 내의 로 이동하여 검색하세요. VSCode Extensions Webber를 설치가 완료되면 (또는 Linux/Windows에서는 )를 누르세요. Cmd+Shift+P 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 또는 기본 8888 대신 443 포트에서 Webber 서버를 시작합니다. -p 443 --port 443 원하는 브라우저를 자동으로 엽니다. 기본적으로 어떤 브라우저도 열리지 않습니다. --browser chrome/safari 서비스 워커를 로컬에서 디버그하는 데 필요합니다. 그렇지 않으면 작동하지 않습니다. --browser-self-signed 시크릿 모드에서 브라우저의 추가 인스턴스를 열려면 Chrome에서만 작동합니다. --browser-incognito 따라서 디버그 모드에서 앱을 빌드하려면 Chrome에서 자동으로 앱을 열고 파일을 변경할 때마다 브라우저를 자동으로 새로고침하여 이 방법으로 실행하세요. SPA용 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 배포 방법 모든 정적 호스팅에 파일을 업로드할 수 있습니다. 호스팅은 파일에 대한 올바른 콘텐츠 유형을 제공해야 합니다! wasm 예, 파일에 대한 올바른 헤더 갖는 것이 매우 중요합니다. 그렇지 않으면 불행하게도 브라우저가 WebAssembly 애플리케이션을 로드할 수 없습니다. wasm Content-Type: application/wasm 예를 들어 파일에 대한 올바른 Content-Type을 제공하지 않으므로 불행히도 WebAssembly 사이트를 호스팅하는 것은 불가능합니다. GithubPages는 wasm 엔진스 nginx와 함께 자체 서버를 사용하는 경우 열고 기록. 그렇다면 가셔도 좋습니다! /etc/nginx/mime.types application/wasm wasm; 결론 오늘 제가 여러분을 놀라게 하길 바랍니다. 적어도 SwifWeb을 사용해 보고 다음 대규모 웹 프로젝트에 SwifWeb을 최대로 사용하기 시작할 것입니다! 에 자유롭게 기여하고 ⭐️모두 별표 표시해 주세요! SwifWeb 라이브러리 저는 가지고 있습니다! 우리와 함께 만나면 반가울 것 같아요! Discord에 엄청난 지원을 받고, 작은 튜토리얼을 읽고, 다가오는 업데이트에 대해 먼저 알림을 받을 수 있는 훌륭한 SwiftStream 커뮤니티를 이는 단지 시작일 뿐이므로 SwifWeb에 대한 더 많은 기사를 계속 지켜봐 주시기 바랍니다! 친구들에게 알려주세요!