Thế giới phát triển web rất rộng lớn và thật dễ dàng để cảm thấy lạc lõng trong dòng công nghệ mới liên tục xuất hiện mỗi ngày. Hầu hết các công nghệ mới này được xây dựng bằng JavaScript hoặc TypeScript. Tuy nhiên, trong bài viết này, tôi sẽ giới thiệu với bạn cách phát triển web bằng Swift gốc, trực tiếp bên trong trình duyệt của bạn và tôi tin rằng nó sẽ gây ấn tượng với bạn.
Để Swift hoạt động tự nhiên trên trang web, trước tiên, nó cần được biên dịch thành mã byte WebAssembly và sau đó JavaScript có thể tải mã đó lên trang. Toàn bộ quá trình biên dịch hơi phức tạp một chút vì chúng ta cần sử dụng chuỗi công cụ đặc biệt và xây dựng các tệp trợ giúp, đó là lý do tại sao có sẵn các công cụ CLI trợ giúp: Carton và Webber.
Cộng đồng SwiftWasm đã thực hiện rất nhiều công việc để có thể biên dịch Swift thành WebAssugging bằng cách vá chuỗi công cụ Swift gốc. Họ cập nhật chuỗi công cụ bằng cách tự động lấy các thay đổi từ chuỗi công cụ ban đầu mỗi ngày và sửa bản fork của họ nếu thử nghiệm không thành công. Mục tiêu của họ là trở thành một phần của chuỗi công cụ chính thức và họ hy vọng điều đó sẽ xảy ra trong tương lai gần.
Thùng carton được tạo bởi cộng đồng SwiftWasm và có thể được sử dụng cho các dự án Tokamak, đây là một khung cung cấp cho bạn khả năng viết trang web bằng SwiftUI.
Webber được tạo cho các dự án SwifWeb. SwifWeb khác ở chỗ nó bao hàm toàn bộ các tiêu chuẩn HTML và CSS, cũng như tất cả các API web.
Mặc dù bạn có thể thích viết các ứng dụng web bằng SwiftUI để đảm bảo tính nhất quán của mã, nhưng tôi tin rằng đây là cách tiếp cận sai lầm vì quá trình phát triển web vốn dĩ đã khác và không thể tiếp cận theo cách giống như SwiftUI.
Đó là lý do tại sao tôi đã tạo SwifWeb, cung cấp cho bạn khả năng sử dụng tất cả sức mạnh của HTML, CSS và API web trực tiếp từ Swift, sử dụng cú pháp đẹp mắt với tính năng tự động hoàn thành và tài liệu. Và tôi đã tạo công cụ Webber vì Carton không thể biên dịch, gỡ lỗi và triển khai các ứng dụng SwifWeb đúng cách vì công cụ này không được tạo cho nó.
Tên tôi là Mikhail Isaev và tôi là tác giả của SwifWeb. Trong bài viết này, tôi sẽ chỉ cho bạn cách bắt đầu xây dựng một trang web bằng SwifWeb.
Bạn cần cài đặt Swift, cách dễ nhất để có nó:
Trong các trường hợp khác, hãy xem hướng dẫn cài đặt trên trang web chính thức
Tôi đã tạo Webber để giúp bạn xây dựng, gỡ lỗi và triển khai các ứng dụng của mình.
Trên macOS, thật dễ dàng để cài đặt với HomeBrew (cài đặt nó từ trang web của họ)
brew install swifweb/tap/webber
để cập nhật lên phiên bản mới nhất sau đó chỉ cần chạy
brew upgrade webber
Trên Ubuntu hoặc Windows (Ubuntu trong WSL2) sao chép và biên dịch thủ công 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
để cập nhật lên phiên bản cuối cùng sau đó chạy
cd /opt/webber sudo git pull sudo swift build -c release
nhánh chính luôn chứa mã ổn định, vì vậy hãy lấy các bản cập nhật từ nó
Mở terminal và thực thi
webber new
Trong menu tương tác, chọn pwa
hoặc spa
và nhập tên dự án.
Thay đổi thư mục thành dự án mới tạo và thực thi webber serve
.
Lệnh này sẽ biên dịch dự án của bạn thành WebAssugging, đóng gói tất cả các tệp cần thiết bên trong một thư mục .webber
đặc biệt và bắt đầu phục vụ dự án của bạn trên tất cả các giao diện sử dụng cổng 8888
theo mặc định.
Đối số bổ sung cho webber serve
loại ứng dụng
-t pwa
cho Ứng dụng web lũy tiến
-t spa
cho Ứng dụng web đơn
Tên của mục tiêu nhân viên dịch vụ (thường được đặt tên là Service
trong dự án PWA)
-s Service
Tên của mục tiêu ứng dụng ( App
theo mặc định)
-a App
In thêm thông tin trong bảng điều khiển
-v
Cổng cho máy chủ Webber (mặc định là 8888
)
-p 8080
Sử dụng -p 443
để kiểm tra giống như SSL thực (với cài đặt SSL tự ký được phép)
Tên trình duyệt đích để tự động khởi chạy trong
--browser safari
hoặc --browser chrome
Phiên bản bổ sung của trình duyệt với cài đặt SSL tự ký được phép để gỡ lỗi nhân viên dịch vụ
--browser-self-signed
Phiên bản bổ sung của trình duyệt ở chế độ ẩn danh
--browser-incognito
Ứng dụng bắt đầu trong 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() } }
Nó hoạt động theo cách giống như iOS:
didFinishLaunching
khi ứng dụng mới bắt đầu
willTerminate
khi ứng dụng sắp chết
willResignActive
khi cửa sổ sắp không hoạt động
didBecomeActive
khi cửa sổ đang hoạt động
didEnterBackground
khi cửa sổ đang ở chế độ nền
willEnterForeground
khi cửa sổ đi vào nền trước
Phương pháp hữu ích nhất ở đây là didFinishLaunching
vì đây là nơi tuyệt vời để định cấu hình ứng dụng. Bạn thấy nó thực sự giống như một ứng dụng iOS! 😀
app
này chứa các phương pháp tiện lợi hữu ích:
registerServiceWorker(“serviceName“)
gọi để đăng ký nhân viên dịch vụ PWA
lệnh gọi addScript(“path/to/script.js“)
để thêm tập lệnh tương đối hoặc bên ngoài
addStylesheet(“path/to/style.css“)
gọi để thêm kiểu tương đối hoặc kiểu bên ngoài
addFont(“path/to/font.woff”, type:)
gọi để thêm phông chữ tương đối hoặc phông chữ bên ngoài, tùy chọn đặt loại
addIcon(“path/to/icon“, type:color:)
gọi để thêm biểu tượng, tùy chọn đặt loại và màu
Ngoài ra, nó còn là nơi để cấu hình các thư viện bổ sung như Autolayout , Bootstrap , Materialize , v.v.
Định tuyến là cần thiết để hiển thị trang thích hợp dựa trên URL hiện tại.
Để hiểu cách sử dụng định tuyến, bạn phải hiểu URL là gì
https://website.com/hello/world - đây /hello/world là đường dẫn
Như bạn đã thấy, ban đầu, trong lớp Ứng dụng, chúng ta nên khai báo tất cả các tuyến cấp cao nhất.
Cấp cao nhất có nghĩa là các trang được khai báo trong các tuyến này sẽ chiếm toàn bộ không gian trong cửa sổ.
Ok, ví dụ, tuyến gốc có thể được đặt theo ba cách
Page("/") { IndexPage() } Page("") { IndexPage() } Page { IndexPage() }
Mình thấy tấm cuối đẹp nhất 🙂
Các tuyến đăng nhập hoặc đăng ký có thể được đặt như thế này
Page("login") { LoginPage() } Page("registration") { RegistrationPage() }
Các tuyến liên quan đến tham số
Page("article/:id") { ArticlePage() }
:id trong ví dụ trên là một phần động của route. Chúng ta có thể truy xuất mã định danh này trong lớp ArticlePage để hiển thị bài viết được liên kết với nó.
class ArticlePage: PageController { override func didLoad(with req: PageRequest) { if let articleId = req.parameters.get("id") { // Retrieve article here } } }
Bạn có thể có nhiều tham số trong đường dẫn. Truy xuất tất cả chúng theo cùng một cách.
Điều thú vị tiếp theo trong đường dẫn là truy vấn , cũng rất dễ sử dụng. Ví dụ: hãy xem xét tuyến đường /search , dự kiến sẽ có các tham số truy vấn age
và text
tìm kiếm.
https://website.com/search**?text=Alex&age=19** - phần cuối cùng là truy vấn
Chỉ cần khai báo lộ trình tìm kiếm
Page("search") { SearchPage() }
Và lấy dữ liệu truy vấn trong lớp SearchPage như thế này
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)") } } }
Bạn cũng có thể sử dụng *
để khai báo tuyến đường chấp nhận mọi thứ trong phần đường dẫn cụ thể như thế này
Page("foo", "*", "bar") { SearchPage() }
Tuyến đường trên sẽ chấp nhận mọi thứ giữa foo và bar, ví dụ: /foo/aaa/bar, /foo/bbb/bar, v.v.
Với dấu **
bạn có thể đặt một tuyến đường bắt tất cả đặc biệt sẽ xử lý mọi thứ chưa khớp với các tuyến đường khác tại một đường dẫn cụ thể.
Sử dụng nó để tạo tuyến đường 404 toàn cầu
Page("**") { NotFoundPage() }
hoặc cho đường dẫn cụ thể, ví dụ: khi người dùng không tìm thấy
Page("user", "**") { UserNotFoundPage() }
Hãy làm rõ các tình huống với các route được khai báo ở trên
/user/1 - nếu có một lộ trình cho /user/:id thì nó sẽ trả về UserPage . Nếu không sẽ rơi vào…
Người dùngNotFoundPage
/user/1/hello - nếu có route cho /user/:id/hello thì nó sẽ rơi vào UserNotFoundPage
/something - nếu không có route cho /something thì nó sẽ rơi vào NotFoundPage
Chúng tôi có thể không muốn thay thế toàn bộ nội dung trên trang cho lộ trình tiếp theo mà chỉ một số khối nhất định. Đây là nơi mà FragmentRouter có ích!
Hãy xem xét rằng chúng ta có các tab trên trang /user . Mỗi tab là một tuyến con và chúng tôi muốn phản ứng với những thay đổi trong tuyến con bằng cách sử dụng FragmentRouter .
Khai báo top-level route trong lớp App
Page("user") { UserPage() }
Và khai báo FragmentRouter trong lớp 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() } } } }
Trong ví dụ trên, FragmentRouter xử lý các định tuyến con /user/profile và /user/friends và hiển thị nó bên dưới Thanh điều hướng , vì vậy trang không bao giờ tải lại toàn bộ nội dung mà chỉ tải lại các đoạn cụ thể.
Cũng có thể được khai báo nhiều hơn một đoạn với các chương trình con giống hoặc khác nhau và tất cả chúng sẽ hoạt động cùng nhau như ma thuật!
Btw FragmentRouter là một Div và bạn có thể định cấu hình nó bằng cách gọi
FragmentRouter(self) .configure { div in // do anything you want with the div }
Bạn có thể sử dụng các tệp CSS truyền thống, nhưng bạn cũng có khả năng kỳ diệu mới để sử dụng biểu định kiểu được viết bằng Swift!
Để khai báo quy tắc CSS bằng Swift, chúng ta có đối tượng Quy tắc .
Nó có thể được xây dựng một cách khai báo bằng cách gọi các phương thức của nó
Rule(...selector...) .alignContent(.baseline) .color(.red) // or rgba/hex color .margin(v: 0, h: .auto)
hoặc cách giống như SwiftUI bằng cách sử dụng @resultBuilder
Rule(...selector...) { AlignContent(.baseline) Color(.red) Margin(v: 0, h: .auto) }
Cả hai cách đều như nhau, tuy nhiên, tôi thích cách thứ nhất hơn vì tính năng tự động hoàn thành ngay sau khi tôi nhập .
😀
Tất cả các phương thức CSS được mô tả trên MDN đều khả dụng.
Hơn thế nữa, nó tự động xử lý các tiền tố của trình duyệt!
Tuy nhiên, bạn có thể đặt thuộc tính tùy chỉnh theo cách này trong một số trường hợp cụ thể
Rule(...selector...) .custom("customKey", "customValue")
Để thiết lập những yếu tố Quy tắc sẽ ảnh hưởng, chúng ta phải thiết lập một bộ chọn. Tôi thấy bộ chọn là truy vấn trong cơ sở dữ liệu, nhưng các phần của truy vấn bộ chọn đó tôi gọi là con trỏ.
Cách dễ nhất để xây dựng một con trỏ là khởi tạo nó bằng chuỗi thô
Pointer("a")
Nhưng cách nhanh chóng đúng đắn là xây dựng nó bằng cách gọi .pointer
tại thẻ HTML cần thiết như thế này
H1.pointer // h1 A.pointer // a Pointer.any // * Class("myClass").pointer // .myClass Id("myId").pointer // #myId
Đó là về các con trỏ cơ bản, nhưng chúng cũng có các công cụ sửa đổi như :hover
:first
:first-child
, v.v.
H1.pointer.first // h1:first H1.pointer.firstChild // h1:first-child H1.pointer.hover // h1:hover
Bạn có thể khai báo bất kỳ công cụ sửa đổi hiện có nào, tất cả chúng đều có sẵn.
Nếu thiếu một cái gì đó, đừng ngần ngại tạo một tiện ích mở rộng để thêm nó!
Và đừng quên gửi pull request trên github để add cho mọi người nhé.
Bạn cũng có thể nối các con trỏ
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
Cách sử dụng bộ chọn trong Quy tắc
Rule(Pointer("a")) // or Rule(A.pointer)
Cách sử dụng nhiều bộ chọn trong Quy tắc
Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer))
Nó tạo ra mã CSS sau
a, h1#myId, div > p { }
Hãy khai báo các kiểu tối và sáng cho ứng dụng của chúng ta và sau này, chúng ta sẽ có thể dễ dàng chuyển đổi giữa chúng.
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 và DarkStyle có thể được khai báo trong các tệp riêng biệt hoặc ví dụ: trong 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) } }
Và sau đó ở đâu đó trong giao diện người dùng của một số trang, chỉ cần gọi
App.current.theme = .light // to switch to light theme // or App.current.theme = .dark // to switch to dark theme
Và nó sẽ kích hoạt hoặc hủy kích hoạt các bảng định kiểu liên quan! Điều đó không tuyệt sao? 😎
Nhưng bạn có thể nói rằng mô tả phong cách trong Swift thay vì CSS khó hơn, vậy vấn đề là gì?
Điểm chính là phản ứng! Chúng ta có thể sử dụng @State với các thuộc tính CSS và thay đổi giá trị một cách nhanh chóng!
Chỉ cần xem qua, chúng ta có thể tạo một lớp với một số thuộc tính phản ứng và thay đổi nó bất cứ lúc nào trong thời gian chạy, do đó, bất kỳ phần tử nào trên màn hình sử dụng lớp đó sẽ được cập nhật! Nó hiệu quả hơn nhiều so với việc chuyển lớp cho nhiều phần tử!
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) } }
Sau này từ bất kỳ nơi nào trong mã chỉ cần gọi
App.current.reactiveColor = .yellow // or any color you want
và nó sẽ cập nhật màu trong Biểu định kiểu và trong tất cả các phần tử sử dụng nó 😜
Ngoài ra, có thể thêm CSS thô vào Biểu định kiểu
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; } """ } }
bạn có thể trộn các chuỗi CSS thô nhiều lần nếu cần
Bộ định tuyến đang hiển thị các trang trên mỗi tuyến. Trang là bất kỳ lớp nào được kế thừa từ PageController .
PageController có các phương thức vòng đời như willLoad
didLoad
willUnload
didUnload
, các phương thức giao diện người dùng buildUI
và body
và biến bao bọc thuộc tính cho các phần tử HTML.
Về mặt kỹ thuật, PageController chỉ là một Div và bạn có thể đặt bất kỳ thuộc tính nào của nó trong phương thức 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") } } }
Nếu trang của bạn rất nhỏ, bạn có thể khai báo nó theo cách ngắn gọn này
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 }
Nó không đẹp và ngắn gọn sao? 🥲
Phương pháp tiện lợi tiền thưởng
alert(message: String)
- phương thức alert
JS trực tiếp
changePath(to: String)
- chuyển đổi đường dẫn URL
Cuối cùng, tôi sẽ cho bạn biết cách (!) để xây dựng và sử dụng các phần tử HTML!
Tất cả các phần tử HTML với các thuộc tính của chúng đều có sẵn trong Swift, danh sách đầy đủ ví dụ như trên MDN .
Chỉ là một ví dụ về danh sách ngắn các phần tử HTML:
mã swifWeb | Mã HTML |
---|---|
| |
| |
| |
| |
| |
| |
Như bạn có thể thấy, rất dễ dàng truy cập vào bất kỳ thẻ HTML nào trong Swift vì tất cả chúng đều được thể hiện dưới cùng một tên, ngoại trừ các đầu vào. Điều này là do các loại đầu vào khác nhau có các phương pháp khác nhau và tôi không muốn trộn lẫn chúng.
Div
đơn giản
Div()
chúng ta có thể truy cập tất cả các thuộc tính và thuộc tính kiểu của nó như thế này
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;">
Phân lớp phần tử HTML để xác định trước kiểu dáng cho nó hoặc để tạo một phần tử tổng hợp có nhiều phần tử con được xác định trước và một số phương thức thuận tiện có sẵn bên ngoài hoặc để đạt được các sự kiện vòng đời như didAddToDOM
và didRemoveFromDOM
.
Hãy tạo một phần tử Divider
chỉ là một Div
nhưng với lớp .divider
được xác định trước
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) } }
Điều rất quan trọng là gọi các siêu phương thức khi phân lớp.
Nếu không có nó, bạn có thể gặp hành vi không mong muốn.
Phần tử có thể được thêm vào DOM của PageController hoặc phần tử HTML ngay hoặc sau này.
Ngay lập tức
Div { H1("Title") P("Subtitle") Div { Ul { Li("One") Li("Two") } } }
Hoặc sau này sử dụng lazy var
lazy var myDiv1 = Div() lazy var myDiv2 = Div() Div { myDiv1 myDiv2 }
Vì vậy, bạn có thể khai báo trước một phần tử HTML và thêm nó vào DOM bất kỳ lúc nào sau đó!
lazy var myDiv = Div() Div { myDiv } // somewhere later myDiv.remove()
Bất kỳ phần tử HTML nào cũng có thuộc tính giám sát tùy chọn cấp quyền truy cập cho cha mẹ của nó nếu nó được thêm vào DOM
Div().superview?.backgroundColor(.red)
Chúng ta thường chỉ cần hiển thị các phần tử trong một số điều kiện nhất định, vì vậy hãy sử dụng if/else
cho điều đó
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") } }
Nhưng nó không phản ứng. Nếu bạn cố đặt showDiv2
thành false
thì không có gì xảy ra.
ví dụ phản ứng
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 }
Tại sao chúng ta nên sử dụng $showDiv2.map {…}
?
Sắp xếp: bởi vì nó không phải là SwiftUI. Ở tất cả.
Đọc thêm về @State
bên dưới.
Bạn cũng có thể cần thêm HTML thô vào trang hoặc phần tử HTML và có thể dễ dàng
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"] }
Tương tự như ví dụ trên, nhưng cũng có 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 } }
Bạn có thể sử dụng BuilderFunction
trong vòng lặp ForEach
để tính toán một số giá trị chỉ một lần như giá trị delay
trong ví dụ sau
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) } }
Nó cũng có thể lấy chức năng như một đối số
BuilderFunction(calculate) { calculatedValue in // CSS rule or DOM element } func calculate() -> Int { return 1 + 1 }
BuilderFunction cũng có sẵn cho các phần tử HTML :)
@State
là điều mong muốn nhất hiện nay đối với lập trình khai báo.
Như tôi đã nói với bạn ở trên: nó không phải là SwiftUI, vì vậy không có máy trạng thái toàn cầu nào theo dõi và vẽ lại mọi thứ. Và các phần tử HTML không phải là các cấu trúc tạm thời mà là các lớp, vì vậy chúng là các đối tượng thực và bạn có quyền truy cập trực tiếp vào chúng. Nó tốt hơn và linh hoạt hơn nhiều, bạn có toàn quyền kiểm soát.
Nó là một trình bao bọc thuộc tính thông báo cho tất cả những người đăng ký về những thay đổi của nó.
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)") }
Ví dụ văn bản đơn giản
@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
Ví dụ số đơn giản
@State var height = 20.px Div().height($height) // whenever height var changes it updates height of the Div
Ví dụ boolean đơn giản
@State var hidden = false Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div
ví dụ ánh xạ
@State var isItCold = true H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })
Ánh xạ hai trạng thái
@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 })
Ánh xạ nhiều hơn hai trạng thái
@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 })
Tất cả các thuộc tính HTML và CSS có thể xử lý các giá trị @State
Bạn có thể thêm một số phương thức tiện lợi vào các phần tử cụ thể như Div
extension Div { func makeItBeautiful() {} }
Hoặc các nhóm phần tử nếu bạn biết class
cha của chúng.
Có ít lớp cha.
BaseActiveStringElement
- dành cho các phần tử có thể được khởi tạo bằng chuỗi, như a
, h1
, v.v.
BaseContentElement
- dành cho tất cả các phần tử có thể chứa nội dung bên trong nó, như div
, ul
, v.v.
BaseElement
- dành cho tất cả các phần tử
Vì vậy, phần mở rộng cho tất cả các yếu tố có thể được viết theo cách này
extension BaseElement { func doSomething() {} }
Lớp màu chịu trách nhiệm về màu sắc. Nó có màu HTML được xác định trước, nhưng bạn có thể có màu của riêng mình
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) }
Sau đó, sử dụng nó như H1(“Text“).color(.myColor1)
extension Class { var my: Class { "my" } }
Sau đó, sử dụng nó như Div().class(.my)
extension Id { var myId: Id { "my" } }
Sau đó, sử dụng nó như Div().id(.my)
đối tượng window
được bao bọc hoàn toàn và có thể truy cập thông qua biến App.current.window
.
Tham khảo đầy đủ có sẵn trên MDN .
Hãy làm tổng quan ngắn dưới đây
Bạn có thể nghe nó trong Lifecycle
trong App.swift
hoặc trực tiếp theo cách này
App.current.window.$isInForeground.listen { isInForeground in // foreground flag changed }
hoặc chỉ cần đọc nó mọi lúc mọi nơi
if App.current.window.isInForeground { // do somethign }
hoặc phản ứng với nó bằng phần tử HTML
Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white })
Nó giống như cờ Foreground, nhưng có thể truy cập qua App.current.window.isActive
Nó phát hiện nếu người dùng vẫn đang tương tác bên trong cửa sổ.
Tương tự như cờ Foreground, nhưng có thể truy cập qua App.current.window.isOnline
Nó phát hiện nếu người dùng vẫn có quyền truy cập vào internet.
Tương tự như cờ Foreground, nhưng có thể truy cập qua App.current.window.isDark
Nó phát hiện xem trình duyệt hoặc hệ điều hành của người dùng có ở chế độ tối hay không.
Kích thước của khu vực nội dung của cửa sổ (khung nhìn) bao gồm thanh cuộn
App.current.window.innerSize
là đối tượng Kích thước trong các giá trị width
và height
bên trong.
Cũng có sẵn dưới dạng biến @State
.
Kích thước của cửa sổ trình duyệt, bao gồm thanh công cụ/thanh cuộn.
App.current.window.outerSize
là đối tượng Kích thước trong các giá trị width
và height
bên trong.
Cũng có sẵn dưới dạng biến @State
.
Đối tượng đặc biệt để kiểm tra các thuộc tính của màn hình mà cửa sổ hiện tại đang được hiển thị. Có sẵn qua App.current.window.screen
.
Thuộc tính thú vị nhất thường là pixelRatio
.
Chứa các URL được người dùng truy cập (trong cửa sổ trình duyệt).
Có sẵn qua App.current.window.history
hoặc chỉ History.shared
.
Nó có thể truy cập dưới dạng biến @State
, vì vậy bạn có thể lắng nghe những thay đổi của nó nếu cần.
App.current.window.$history.listen { history in // read history properties }
Nó cũng có thể truy cập dưới dạng biến đơn giản
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
Thông tin chi tiết có sẵn trên MDN .
Chứa thông tin về URL hiện tại.
Có sẵn qua App.current.window.location
hoặc chỉ Location.shared
.
Nó có thể truy cập dưới dạng biến @State
, vì vậy bạn có thể lắng nghe những thay đổi của nó nếu cần.
Đây là cách bộ định tuyến hoạt động chẳng hạn.
App.current.window.$location.listen { location in // read location properties }
Nó cũng có thể truy cập như một biến đơn giản
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
Thông tin chi tiết có sẵn trên MDN .
Chứa thông tin về trình duyệt.
Có sẵn qua App.current.window.navigator
hoặc chỉ Navigator.shared
Các thuộc tính thú vị nhất thường là cookieEnabled
language
platform
userAgent
.
Cho phép lưu các cặp khóa/giá trị trong trình duyệt web. Lưu trữ dữ liệu không có ngày hết hạn.
Có sẵn dưới dạng App.current.window.localStorage
hoặc chỉ 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()
Theo dõi thay đổi
LocalStorage.onChange { key, oldValue, newValue in print("LocalStorage: key \(key) has been updated") }
Theo dõi tất cả các mục loại bỏ
LocalStorage.onClear { print("LocalStorage: all items has been removed") }
Cho phép lưu các cặp khóa/giá trị trong trình duyệt web. Lưu trữ dữ liệu chỉ trong một phiên.
Có sẵn dưới dạng App.current.window.sessionStorage
hoặc chỉ SessionStorage.shared
.
API hoàn toàn giống như trong LocalStorage được mô tả ở trên.
Đại diện cho bất kỳ trang web nào được tải trong trình duyệt và phục vụ như một điểm vào nội dung của trang web.
Có sẵn qua 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]
Bản địa hóa cổ điển là tự động và phụ thuộc vào ngôn ngữ hệ thống của người dùng
H1(String( .en("Hello"), .fr("Bonjour"), .ru("Привет"), .es("Hola"), .zh_Hans("你好"), .ja("こんにちは")))
Nếu bạn muốn thay đổi các chuỗi được bản địa hóa trên màn hình khi đang di chuyển (không cần tải lại trang)
Bạn có thể thay đổi ngôn ngữ hiện tại bằng cách gọi
Localization.current = .es
Nếu bạn đã lưu ngôn ngữ của người dùng ở đâu đó trong cookie hoặc bộ lưu trữ cục bộ thì bạn phải đặt ngôn ngữ đó khi khởi chạy ứng dụng
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“)
tương đương với console.log('Hello world')
trong JavaScript
Các phương pháp điều khiển cũng được bao bọc bằng tình yêu ❤️
Console.dir(...) Console.error(...) Console.warning(...) Console.clear()
Để thực hiện công việc xem trước trực tiếp, hãy khai báo lớp WebPreview trong mỗi tệp bạn muốn.
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() } }
Vui lòng đọc hướng dẫn trên trang kho lưu trữ . Đó là giải pháp phức tạp nhưng hoàn toàn hiệu quả 😎
Chuyển đến Tiện ích mở rộng bên trong VSCode và tìm kiếm Webber .
Khi nó được cài đặt, nhấn Cmd+Shift+P
(hoặc Ctrl+Shift+P
trên Linux/Windows)
Tìm và khởi chạy Webber Live Preview
.
Ở bên phải, bạn sẽ thấy cửa sổ xem trước trực tiếp và cửa sổ này sẽ làm mới bất cứ khi nào bạn lưu tệp chứa lớp WebPreview .
Nó có sẵn thông qua JavaScriptKit , nền tảng của SwifWeb .
Read how to you nằm trong kho lưu trữ chính thức .
Bạn có thể thêm css
, js
, png
, jpg
và bất kỳ tài nguyên tĩnh nào khác bên trong dự án.
Nhưng để chúng có sẵn trong quá trình gỡ lỗi hoặc trong các tệp phát hành cuối cùng, bạn phải khai báo tất cả chúng trong Package.swift như thế này
.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") ]),
Sau này bạn sẽ có thể truy cập chúng, ví dụ như thế này Img().src(“/images/logo.png“)
Khởi chạy Webber theo cách sau
webber serve
chỉ để nhanh chóng khởi chạy nó
webber serve -t pwa -s Service
để khởi chạy nó ở chế độ PWA
-v
hoặc --verbose
để hiển thị thêm thông tin trong bảng điều khiển cho mục đích gỡ lỗi
-p 443
hoặc --port 443
để khởi động máy chủ webber trên cổng 443 thay vì 8888 mặc định
--browser chrome/safari
để tự động mở trình duyệt mong muốn, theo mặc định, nó không mở bất kỳ trình duyệt nào
--browser-self-signed
cần thiết để gỡ lỗi nhân viên dịch vụ cục bộ, nếu không thì chúng không hoạt động
--browser-incognito
để mở phiên bản bổ sung của trình duyệt ở chế độ ẩn danh, chỉ hoạt động với chrome
Vì vậy, để xây dựng ứng dụng của bạn ở chế độ gỡ lỗi, hãy tự động mở ứng dụng đó trong Chrome và tự động làm mới trình duyệt bất cứ khi nào bạn thay đổi bất kỳ tệp nào, hãy khởi chạy ứng dụng đó theo cách này
cho SPA
webber serve --browser chrome
để thử nghiệm PWA thực
webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito
Bạn có thể muốn cải thiện quá trình tải ban đầu.
Đối với điều đó, chỉ cần mở thư mục .webber/entrypoint/dev
bên trong dự án và chỉnh sửa tệp index.html
.
Nó chứa mã HTML ban đầu với các trình lắng nghe rất hữu ích: WASMLoadingStarted
WASMLoadingStartedWithoutProgress
WASMLoadingProgress
WASMLoadingError
.
Bạn có thể tự do chỉnh sửa mã đó thành bất kỳ thứ gì bạn muốn để triển khai phong cách tùy chỉnh của mình 🔥
Khi bạn hoàn thành việc triển khai mới, đừng quên lưu vào thư mục .webber/entrypoint/release
Chỉ cần thực hiện webber release
hoặc webber release -t pwa -s Service
cho PWA.
Sau đó lấy các tệp đã biên dịch từ thư mục .webber/release
và tải chúng lên máy chủ của bạn.
Bạn có thể tải tệp của mình lên bất kỳ dịch vụ lưu trữ tĩnh nào.
Dịch vụ lưu trữ phải cung cấp Loại nội dung chính xác cho các tệp wasm !
Có, điều rất quan trọng là phải có đúng tiêu đề Content-Type: application/wasm
cho các tệp wasm , nếu không, rất tiếc, trình duyệt sẽ không thể tải ứng dụng WebAssugging của bạn.
Ví dụ: GithubPages không cung cấp Loại nội dung chính xác cho các tệp wasm nên rất tiếc là không thể lưu trữ các trang WebAssembly trên đó.
Nếu bạn sử dụng máy chủ của riêng mình với nginx, hãy mở /etc/nginx/mime.types
và kiểm tra xem nó có chứa application/wasm wasm;
ghi. Nếu có thì bạn tốt để đi!
Tôi hy vọng hôm nay tôi đã làm bạn ngạc nhiên và ít nhất bạn sẽ dùng thử SwifWeb và tối đa sẽ bắt đầu sử dụng nó cho dự án web lớn tiếp theo của bạn!
Vui lòng đóng góp cho bất kỳ thư viện SwifWeb nào và đồng thời gắn dấu sao ⭐️tất cả chúng!
Tôi có một cộng đồng SwiftStream tuyệt vời trong Discord, nơi bạn có thể tìm thấy sự hỗ trợ to lớn, đọc các hướng dẫn nhỏ và được thông báo trước về mọi bản cập nhật sắp tới! Sẽ rất tuyệt khi gặp bạn với chúng tôi!
Đây mới chỉ là bước khởi đầu, vì vậy hãy theo dõi các bài viết khác về SwifWeb!
Hãy nói với bạn bè của bạn!