visit
You need to have Swift installed, the easiest way to have it:
In other cases take a look at installation instructions on the official website
brew install swifweb/tap/webber
to update to the latest version later just run
brew upgrade webber
sudo apt-get install binaryen
curl //get.wasmer.io -sSfL | sh
apt-get install npm
cd /opt
sudo git clone //github.com/swifweb/webber
cd webber
sudo swift build -c release
sudo ln -s /opt/webber/.build/release/Webber /usr/local/bin/webber
to update to the last version later run
cd /opt/webber
sudo git pull
sudo swift build -c release
main branch always contains stable code so feel free to pull updates from it
webber new
In the interactive menu choose pwa
or spa
, and enter the project name.
Change the directory to the newly created project and execute webber serve
.
This command will compile your project into WebAssembly, package all necessary files inside a special .webber
folder, and start serving your project on all interfaces using port 8888
by default.
Additional arguments for webber serve
-t pwa
for Progressive Web App
-t spa
for Single Web App
Name of service worker target (usually named Service
in PWA project)
-s Service
Name of app target (App
by default)
-a App
-v
Port for the Webber server (default is 8888
)
-p 8080
Use -p 443
to test like real SSL (with allowed self-signed SSL setting)
--browser safari
or --browser chrome
--browser-self-signed
--browser-incognito
The app begins in 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()
}
}
It works in an iOS-like manner:
didFinishLaunching
when the app just started
willTerminate
when the app is going to die
willResignActive
when the window is going to be inactive
didBecomeActive
when the window is active
didEnterBackground
when the window is going into the background
willEnterForeground
when the window is going into the foreground
The most useful method here is didFinishLaunching
because it is a great place to configure the app. You see it feels really like an iOS app! 😀
Here app
contains useful convenience methods:
registerServiceWorker(“serviceName“)
call to register PWA service worker
addScript(“path/to/script.js“)
call to add relative or external script
addStylesheet(“path/to/style.css“)
call to add relative or external style
addFont(“path/to/font.woff”, type:)
call to add relative or external font, optionally set type
addIcon(“path/to/icon“, type:color:)
call to add icon, optionally set type and color
//website.com/hello/world - here /hello/world is the path
Page("/") { IndexPage() }
Page("") { IndexPage() }
Page { IndexPage() }
I think the last one is the most beautiful 🙂
Page("login") { LoginPage() }
Page("registration") { RegistrationPage() }
Parameter-related routes
Page("article/:id") { ArticlePage() }
The :id in the example above is a dynamic part of the route. We can retrieve this identifier in the ArticlePage class to display the article associated with it.
class ArticlePage: PageController {
override func didLoad(with req: PageRequest) {
if let articleId = req.parameters.get("id") {
// Retrieve article here
}
}
}
The next interesting thing in the path is the query, which is also very easy to use. For example, let's consider the /search route, which expects to have the search text
and age
query parameters.
//website.com/search**?text=Alex&age=19** - the last part is the query
Page("search") { SearchPage() }
And retrieve query data in the SearchPage class like this
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)")
}
}
}
You also can use *
to declare route which accept anything in the specific path part like this
Page("foo", "*", "bar") { SearchPage() }
The route above will accept anything between foo and bar, e.g. /foo/aaa/bar, /foo/bbb/bar, etc.
With **
sign you may set a special catch-all route that will handle anything that hasn’t been matched to other routes at a specific path.
Page("**") { NotFoundPage() }
or for specific path e.g. when user not found
Page("user", "**") { UserNotFoundPage() }
/user/1 - if there is a route for /user/:id then it will return UserPage. Otherwise, it will fall into…
UserNotFoundPage
/user/1/hello - if there are route for /user/:id/hello then it will fall into UserNotFoundPage
/something - if there are no route for /something then it will fall into NotFoundPage
We may not want to replace the entire content on the page for the next route, but only certain blocks. This is where the FragmentRouter comes in handy!
Let's consider that we have tabs on the /user page. Each tab is a subroute, and we want to react to changes in the subroute using the FragmentRouter.
Declare the top-level route in the App class
Page("user") { UserPage() }
And declare FragmentRouter in the UserPage class
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() }
}
}
}
In the example above, FragmentRouter handles /user/profile and /user/friends subroutes and renders it under the Navbar, so the page never reloads the whole content but only specific fragments.
Btw FragmentRouter is a Div and you may configure it by calling
FragmentRouter(self)
.configure { div in
// do anything you want with the div
}
To declare a CSS rule using Swift we have Rule object.
Rule(...selector...)
.alignContent(.baseline)
.color(.red) // or rgba/hex color
.margin(v: 0, h: .auto)
or SwiftUI-like way using @resultBuilder
Rule(...selector...) {
AlignContent(.baseline)
Color(.red)
Margin(v: 0, h: .auto)
}
Both ways are equal, however, I prefer the first one because of the autocompletion right after I type .
😀
Rule(...selector...)
.custom("customKey", "customValue")
Pointer("a")
But the right swifty way is to build it by calling .pointer
at needed HTML tag like this
H1.pointer // h1
A.pointer // a
Pointer.any // *
Class("myClass").pointer // .myClass
Id("myId").pointer // #myId
It is about basic pointers, but they also have modifiers like :hover
:first
:first-child
etc.
H1.pointer.first // h1:first
H1.pointer.firstChild // h1:first-child
H1.pointer.hover // h1:hover
You can declare any existing modifier, they are all available.
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
How to use the selector in the Rule
Rule(Pointer("a"))
// or
Rule(A.pointer)
How to use more than one selector in the Rule
Rule(A.pointer, H1.id(.myId), Div.pointer.parent(P.pointer))
It produces the following CSS code
a, h1#myId, div > p {
}
import Web
@main
class App: WebApp {
enum Theme {
case light, dark
}
@State var theme: Theme = .light
@AppBuilder
override var app: Configuration {
// ... Lifecycle, Routes ...
LightStyle().disabled($theme.map { $0 != .happy })
DarkStyle().disabled($theme.map { $0 != .sad })
}
}
LightStyle and DarkStyle may be declared in separate files or e.g. in the 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)
}
}
App.current.theme = .light // to switch to light theme
// or
App.current.theme = .dark // to switch to dark theme
And it will activate or deactivate the related stylesheets! Isn't that cool? 😎
But you may say that describing styles in Swift instead of CSS is harder, so what’s the point?
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
and it will update the color in the Stylesheet and in all the elements that uses it 😜
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;
}
"""
}
}
you can mix-in raw CSS strings as many times as needed
Router is rendering pages on each route. Page is any class inherited from the PageController.
PageController has lifecycle methods like willLoad
didLoad
willUnload
didUnload
, UI methods buildUI
and body
, and property wrapper variable for HTML elements.
Technically PageController is just a Div and you may set any its properties in buildUI
method.
class IndexPage: PageController {
// MARK: - Lifecycle
override func willLoad(with req: PageRequest) {
super.willLoad(with: req)
}
override func didLoad(with req: PageRequest) {
super.didLoad(with: req)
// set page title and metaDescription
self.title = "My Index Page"
self.metaDescription = "..."
// also parse query and hash here
}
override func willUnload() {
super.willUnload()
}
override func didUnload() {
super.didUnload()
}
// MARK: - UI
override func buildUI() {
super.buildUI()
// access any properties of the page's div here
// e.g.
self.backgroundcolor(.lightGrey)
// optionally call body method here to add child HTML elements
body {
P("Hello world")
}
// or alternatively
self.appendChild(P("Hello world"))
}
// the best place to declare any child HTML elements
@DOM override var body: DOM.Content {
H1("Hello world")
P("Text under title")
Button("Click me") {
self.alert("Click!")
print("button clicked")
}
}
}
PageController { page in
H1("Hello world")
P("Text under title")
Button("Click me") {
page.alert("Click!")
print("button clicked")
}
}
.backgroundcolor(.lightGrey)
.onWillLoad { page in }
.onDidLoad { page in }
.onWillUnload { page in }
.onDidUnload { page in }
Isn’t it beautiful and laconic? 🥲
Bonus convenience methods
alert(message: String)
- direct JS alert
method
changePath(to: String)
- switching URL path
SwifWeb code | HTML Code |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Simple Div
Div()
we can access all its attributes and style properties like this
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;">
Subclass HTML element to predefine style for it, or to make a composite element with a lot of predefined child elements and some convenient methods available outside, or to achieve lifecycle events like didAddToDOM
and didRemoveFromDOM
.
Let’s create a Divider
element which is just a Div
but with predefined .divider
class
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)
}
}
The Element can be appended to the DOM of PageController or HTML element right away or later.
Div {
H1("Title")
P("Subtitle")
Div {
Ul {
Li("One")
Li("Two")
}
}
}
Or later using lazy var
lazy var myDiv1 = Div()
lazy var myDiv2 = Div()
Div {
myDiv1
myDiv2
}
So you can declare an HTML element in advance and add it to the DOM any time later!
lazy var myDiv = Div()
Div {
myDiv
}
// somewhere later
myDiv.remove()
Div().superview?.backgroundColor(.red)
We often need to show elements only in certain conditions so let’s use if/else
for that
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")
}
}
But it is not reactive. If you try to set showDiv2
to false
nothing happens.
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
}
Why should we use $showDiv2.map {…}
?
Read more about @State
below.
Div {
"""
<a href="//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"]
}
Same as in examples above, but also BuilderFunction
is available
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
}
}
You can use BuilderFunction
in ForEach
loops to calculate some value one time only like a delay
value in the following example
ForEach(1...20) { index in
BuilderFunction(9999.asRandomMax()) { delay in
CSSRule(Pointer(".shooting_star").nthChild("\(index)"))
.custom("top", "calc(50% - (\(400.asRandomMax() - 200)px))")
.custom("left", "calc(50% - (\(300.asRandomMax() + 300)px))")
.animationDelay(delay.ms)
CSSRule(Pointer(".shooting_star").nthChild("\(index)").before)
.animationDelay(delay.ms)
CSSRule(Pointer(".shooting_star").nthChild("\(index)").after)
.animationDelay(delay.ms)
}
}
BuilderFunction(calculate) { calculatedValue in
// CSS rule or DOM element
}
func calculate() -> Int {
return 1 + 1
}
BuilderFunction is available for HTML elements too :)
@State
is the most desirable thing nowadays for declarative programming.
enum Countries {
case usa, australia, mexico
}
@State var selectedCounty: Countries = .usa
$selectedCounty.listen {
print("country changed")
}
$selectedCounty.listen { newValue in
print("country changed to \(newValue)")
}
$selectedCounty.listen { oldValue, newValue in
print("country changed from \(oldValue) to \(newValue)")
}
@State var text = "Hello world!"
H1($text) // whenever text changes it updates inner-text in H1
InputText($text) // while user is typing text it updates $text which updates H1
Simple number example
@State var height = 20.px
Div().height($height) // whenever height var changes it updates height of the Div
Simple boolean example
@State var hidden = false
Div().hidden($hidden) // whenever hidden changes it updates visibility of the Div
Mapping example
@State var isItCold = true
H1($isItCold.map { $0 ? "It is cold 🥶" : "It is not cold 😌" })
Mapping two states
@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
})
Mapping more than two states
@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
})
All HTML and CSS properties can handle @State
values
extension Div {
func makeItBeautiful() {}
}
Or groups of elements if you know their parent class
.
There are few parent classes.
BaseActiveStringElement
- is for elements which can be initialized with string, like a
, h1
, etc.
BaseContentElement
- is for all elements that can have content inside of it, like div
, ul
, etc.
BaseElement
- is for all elements
extension BaseElement {
func doSomething() {}
}
Color class is responsible for colors. It has predefined HTML-colors, but you can have your own
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)
}
Then use it like H1(“Text“).color(.myColor1)
extension Class {
var my: Class { "my" }
}
Then use it like Div().class(.my)
extension Id {
var myId: Id { "my" }
}
Then use it like Div().id(.my)
window
object is fully wrapped and accessible via App.current.window
variable.
You can listen for it in Lifecycle
in the App.swift
or directly this way
App.current.window.$isInForeground.listen { isInForeground in
// foreground flag changed
}
or just read it anytime anywhere
if App.current.window.isInForeground {
// do somethign
}
or react on it with HTML element
Div().backgroundColor(App.current.window.$isInForeground.map { $0 ? .grey : .white })
It is the same as Foreground flag, but accessible via App.current.window.isActive
Same as the Foreground flag, but accessible via App.current.window.isOnline
Same as the Foreground flag, but accessible via App.current.window.isDark
App.current.window.innerSize
is Size object within width
and height
values inside.
Also available as @State
variable.
App.current.window.outerSize
is Size object within width
and height
values inside.
Also available as @State
variable.
Special object for inspecting properties of the screen on which the current window is being rendered. Available via App.current.window.screen
.
The most interesting property is usually pixelRatio
.
Available via App.current.window.history
or just History.shared
.
It is accessible as @State
variable, so you can listen for its changes if needed.
App.current.window.$history.listen { history in
// read history properties
}
It is also accessible as simple variable
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
More details are available on .
Available via App.current.window.location
or just Location.shared
.
It is accessible as @State
variable, so you can listen for its changes if needed.
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
More details are available on .
Available via App.current.window.navigator
or just Navigator.shared
The most interesting properties usually userAgent
platform
language
cookieEnabled
.
Available as App.current.window.localStorage
or just 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()
Tracking changes
LocalStorage.onChange { key, oldValue, newValue in
print("LocalStorage: key \(key) has been updated")
}
Tracking all items removal
LocalStorage.onClear { print("LocalStorage: all items has been removed") }
Available as App.current.window.sessionStorage
or just SessionStorage.shared
.
API is absolutely same as in LocalStorage described above.
Available via 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
If you saved the user's language somewhere in cookies or localstorage then you have to set it on the app launch
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("//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: "//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"))
}
Simple print(“Hello world“)
is equivalent of console.log(‘Hello world‘)
in JavaScript
Console methods are also wrapped with love ❤️
Console.dir(...)
Console.error(...)
Console.warning(...)
Console.clear()
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()
}
}
Go to Extensions inside VSCode and search Webber.
Once it is installed press Cmd+Shift+P
(or Ctrl+Shift+P
on Linux/Windows)
Find and launch Webber Live Preview
.
On the right side, you will see the live preview window and it refreshes whenever you save the file which contains WebPreview class.
It is available through JavaScriptKit which is the foundation of the SwifWeb.
Read how to you is in .You can add css
, js
, png
, jpg
, and any other static resources inside of the project.
But to have them available during debug or in final release files you have to declare them all in the Package.swift like this
.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")
]),
Later you will be able to access them e.g. like this Img().src(“/images/logo.png“)
Launch Webber the following way
webber serve
just to quickly launch it
webber serve -t pwa -s Service
to launch it in PWA mode
-v
or --verbose
to show more info in console for debugging purposes
-p 443
or --port 443
to start webber server on 443 port instead of default 8888
--browser chrome/safari
to automatically open desired browser, by default it doesn't open any
--browser-self-signed
needed to debug service workers locally, otherwise they doesn't work
--browser-incognito
to open additional instance of browser in incognito mode, works only with chrome
webber serve --browser chrome
webber serve -t pwa -s Service -p 443 --browser chrome --browser-self-signed --browser-incognito
For that just open .webber/entrypoint/dev
folder inside of the project and edit index.html
file.
It contains initial HTML code with very useful listeners: WASMLoadingStarted
WASMLoadingStartedWithoutProgress
WASMLoadingProgress
WASMLoadingError
.
When you finish the new implementation don’t forget to save same into .webber/entrypoint/release
folder
Simply execute webber release
or webber release -t pwa -s Service
for PWA.
Then grab compiled files from the .webber/release
folder and upload them to your server.
Hosting should provide the correct Content-Type for wasm files!
Yes, it is very important to have the correct header Content-Type: application/wasm
for wasm files, otherwise unfortunately browser will not be able to load your WebAssembly application.
For example GithubPages doesn’t provide the correct Content-Type for wasm files so so unfortunately it is impossible to host WebAssembly sites on it.
If you use your own server with nginx, then open /etc/nginx/mime.types
and check if it contains application/wasm wasm;
record. If yes then you are good to go!
Please feel free to contribute to any of and also to star ⭐️them all!