Introduction
Why another web framework?
Kweb is designed to make it easy for developers to create modern websites without having to worry about the complexities of communication between the server and browser. With a unified codebase, you can focus on creating an intuitive and user-friendly interface. Kweb makes it easier to build and maintain functional and beautiful websites.
How does it work?
Kweb is a remote interface for a web browser's DOM (Document Object Model). With Kweb, you can create and manipulate DOM elements, and listen for and handle events, all using an intuitive Kotlin DSL that mirrors the structure of the HTML being created. Kweb is built on the Ktor framework, which handles HTTP, HTTPS, and WebSocket transport, and is optimized to minimize latency and resource usage in both server and browser.
Features
- End-to-end Kotlin - Write your entire web site or user interface in Kotlin, Kweb takes care of browser-server communication
- Real-time synchronization of your back-end data with your web page - Kweb takes care of all the plumbing for you
- Server-side HTML rendering with hydration - Kweb renders your HTML on the server before sending it to the browser
- Efficient instruction preloading - Kweb avoids unnecessary server communication by preloading instructions
- Very lightweight - Kweb is just 5k lines of code
Relevant Links
- GitHub repository
- API documentation
- Example Google Cloud Project
- Questions/Feedback/Bugs
- Chat with us on Matrix
Getting Started
Requirements
To get started with Kweb, you should have some familiarity with Kotlin and Gradle. It is also helpful to have a basic understanding of HTML.
Adding Kweb to your Gradle project
To add Kweb to your Gradle project, you will need to include the following dependencies in your
build.gradle
or build.gradle.kt
files:
Groovy
dependencies {
implementation 'io.kweb:kweb-core:1.4.13'
// This (or another SLF4J binding) is required for Kweb to log errors
implementation 'org.slf4j:slf4j-simple:2.0.3'
}
Kotlin
dependencies {
implementation("io.kweb:kweb-core:1.4.13")
// This (or another SLF4J binding) is required for Kweb to log errors
implementation("org.slf4j:slf4j-simple:2.0.3")
}
Hello world
To create a simple "Hello World" example with Kweb, create a new Kotlin file and enter the following code:
import kweb.*
fun main() {
Kweb(port = 16097) {
doc.body {
h1().text("Hello World!")
}
}
}
Run the file and then visit http://localhost:16097/ in your web browser to see the output. This should display the following HTML body:
<body>
<h1>Hello World!</h1>
</body>
This example demonstrates two important features of Kweb:
- Setting up a kwebsite is easy and does not require servlets or third-party web servers.
- The structure of your Kotlin code will closely match the structure of the HTML it generates.
Hello world²
One way to think of Kweb is as a domain-specific language (DSL) for building and manipulating a DOM in a remote web browser, while also listening for and handing DOM events. It's important to note that this DSL can also do anything that Kotlin can do, including features like for loops, functions, coroutines, and classes.
Here is a simple example using a Kotlin for loop:
Kweb(port = 16097) {
doc.body {
ul {
for (x in 1..5) {
li().text("Hello World $x!")
}
}
}
}
This will produce the following HTML:
<body>
<ul>
<li>Hello World 1!</li>
<li>Hello World 2!</li>
<li>Hello World 3!</li>
<li>Hello World 4!</li>
<li>Hello World 5!</li>
</ul>
</body>
Template Repository
You can find a simple template Kweb project in kwebio/kweb-template.
DOM Basics
Table of Contents
- Creating DOM Elements and Fragments
- Element Attributes
- Adding children to an existing element
- Reading from the DOM
- Supported HTML tags
- Further Reading
Creating DOM Elements and Fragments
Let's create a <button>
as a child of the <body>
element and set its text:
fun main() {
Kweb(port = 16097) {
doc.body {
button().text("Click Me!")
}
}
}
The Kweb DSL can be used to create nested elements:
Kweb(port = 16097) {
doc.body {
table {
tr {
th().text("Name")
th().text("Age")
}
tr {
td().text("Alice")
td().text("21")
}
tr {
td().text("Bob")
td().text("22")
}
}
}
}
Element Attributes
If you assign the button element to a val then you can also set its attributes:
val button = button()
button.text("Click Me!")
button.classes("bigbutton")
button["autofocus"] = true
Or delete it:
button.delete()
Adding children to an existing element
The DSL syntax makes it very easy to create elements and their children together:
ul {
li().text("One")
li().text("Two")
}
The created
Element is passed to the
{block}
as a parameter, which can be used to set attributes on the element, add
listeners, or set the element's text or innerHtml:
button { btnEl ->
with(btnEl) {
classes("bigbutton")
this["autofocus"] = true
text("Click Me!")
}
}
We can also use the new {}
function to add children to a pre-existing Element:
val unorderedList : ULElement = ul()
unorderedList.new {
li().text("One")
li().text("Two")
}
Reading from the DOM
Kweb can also read from the DOM, in this case the value of an <input>
element:
val input: InputElement = input(type = InputType.text)
// A KVar is a mutable value to which you can add listeners
val inputKVar = input.value
inputKVar.addListener { old, new ->
println("Input changed from $old to $new")
}
Events can evaluate a JavaScript expression and send the result to the server, in this case we give it an expression that will retrieve the value of an InputElement, conveniently provided by valueJsExpression.
Note: See the Observer Pattern & State section for another way to read input element values.
Supported HTML tags
Kweb supports a significant subset of HTML tags like button(), p(), a(), table(), and so on. You can find a more complete list in prelude.kt (scroll down to the Functions section). This provides a nice statically-typed HTML DSL, fully integrated with the Kotlin language.
If a tag doesn't have explicit support in Kweb that's not a problem. For example, here is how you might use the infamous and now-obsolete <blink> tag:
element("blink") {
span().text("Blinking Text")
}
Further Reading
The Element class provides many other useful ways to interact with DOM elements.
Event Handling
- Listening for events
- Immediate events
- Querying the DOM when an event is triggered
- Preventing the default event action
Listening for events
You can attach event handlers to DOM elements:
doc.body {
val label = h1()
label.text("Click Me")
label.on.click {
label.text("Clicked!")
}
}
Most if not all JavaScript event types are supported, and you can read event data like which key was pressed:
doc.body {
val input = input(type = InputType.text)
input.on.keypress { keypressEvent ->
logger.info("Key Pressed: ${keypressEvent.key}")
}
}
Immediate events
Since the code to respond to events runs on the server, there may be a short but sometimes noticeable delay before the page updates in response to an event.
Fortunately, Kweb has a solution:
doc.body {
val input = button(type = ButtonType.button)
val label = span().text("Not clicked")
input.onImmediate.click {
label.text("Clicked!")
}
}
Kweb executes this event handler on page render and records the changes it makes to the DOM. It then "pre-loads" these instructions to the browser such that they are executed immediately without a server round trip.
Warning: Due to this pre-loading mechanism, the event handler for an onImmediate must limit itself to simple DOM modifications. Kweb includes some runtime safeguards against this but they can't catch everything so please use with caution.
Using events and immediate events together
A common pattern is to use both types of event handler on a DOM element. The immediate handler might disable a clicked button, or temporarily display some form of spinner. The normal handler would then do what it needs on the server, and then perhaps re-enable the button and remove the spinner.
Querying the DOM when an event is triggered
Sometimes you need to know the state of the DOM when an event is triggered. You could query it from within the event handler but this would add a server round trip which is inefficient.
Alternatively you can use retrieveJs. This will execute the JavaScript expression you provide when the event fires and return the result in the retrieved property of the event:
inputButton.on(retrieveJs = "(new Date()).getTime()").click { event ->
label.text("Clicked at ${event.retrieved.jsonPrimitive.content}")
}
For ValueElements
such as InputElement
there is a convenience property valueJsExpression
that you can use to retrieve
the current value of the element:
val textInput = input(type = InputType.text)
val inputButton = button(type = ButtonType.button)
val label = span().text("Not clicked")
inputButton.on(retrieveJs = textInput.valueJsExpression).click { event ->
label.text("Read textInput: ${event.retrieved.jsonPrimitive.content}")
}
Preventing the default event action
You can also prevent the default action of an event from occurring by setting the
preventDefault
parameter to true - this is the equivalent of JavaScript's Event.preventDefault()
function.
val inputButton = button(type = ButtonType.button)
inputButton.on(preventDefault = true).click {
logger.debug("Clicked!")
}
Rendering State
- Overview
- The KVar Class
- KVars and the DOM
- Binding a KVar to an input element's value
- Rendering state to a DOM fragment
- Rendering lists with renderEach
- Extracting data class properties
- Reversible mapping
Overview
A Kweb app is a function that maps state on the server to the DOM in the user's web browser. Once this mapping is defined, you can change the state and the browser will update automatically.
The KVar Class
A KVar is an observable container. It contains a single typed object, which can change over time, similar to an AtomicReference. You can add listeners to a KVar to be notified immediately when it changes.
For example:
val counter = KVar(0)
Here we create a counter of type KVar<Int> initialized with the value 0.
We can also read and modify the value of a KVar:
println("Counter value ${counter.value}")
counter.value = 1
println("Counter value ${counter.value}")
counter.value++
println("Counter value ${counter.value}")
Will print:
Counter value 0
Counter value 1
Counter value 2
KVars support powerful mapping semantics to create new KVars:
val counterDoubled = counter.map { it * 2 }
counter.value = 5
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")
counter.value = 6
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")
Will print:
counter: 5, doubled: 10
counter: 6, doubled: 12
Note that counterDoubled
updates automatically, because mapped KVars listen to the original for changes.
The KVar
class is a subclass of KVal,
which is a read-only version of KVar
.
Note: KVars should only be used to store values that are themselves immutable, such as an Int, String, or a Kotlin data class with immutable parameters.
KVars and the DOM
Within the Kotlin DSL you can use the kvar()
function to create a KVar
. This has the advantage of calling
KVar.close()
when this DOM fragment is cleaned up which will free up resources, and avoids having to import
the KVar
class.
Kweb(port = 2135) {
doc.body {
val name = kvar("John")
li().text(name)
}
}
The neat part is that if the value of name changes, the DOM element text will update automatically. It may help to think of this as a way of "unwrapping" a KVar.
Numerous other functions on Elements support KVars in a similar manner, including innerHtml() and setAttribute().
Binding a KVar to an input element's value
For <input> elements you can set the value to a KVar, which will connect them bidirectionally.
Any changes to the KVar will be reflected in realtime in the browser, and similarly any changes in the browser by the user will be reflected immediately in the KVar, for example:
Kweb(port = 2395) {
doc.body {
p().text("What is your name?")
val input = input(type = InputType.text)
input.value = KVar("Peter Pan")
val greeting = input.value.map { name -> "Hi $name!" }
p().text(greeting)
}
}
This will also work for <option>
and <textarea>
elements which also have values.
See also: ValueElement.value
Rendering state to a DOM fragment
But what if you want to do more than just modify a single element based on a KVar, what if you want to modify a whole tree of elements?
This is where the render function comes in:
val list = KVar(listOf("one", "two", "three"))
Kweb(port = 16097) {
doc.body {
render(list) { rList ->
ul {
for (item in rList) {
li().text(item)
}
}
}
}
}
Here, if we were to change the list:
list.value = listOf("four", "five", "six")
Then the relevant part of the DOM will be redrawn instantly.
The simplicity of this mechanism may disguise how powerful it is, since render {} blocks can be nested, it's possible to be very selective about what parts of the DOM must be modified in response to changes in state.
Note: Kweb will only re-render a DOM fragment if the value of the KVar actually changes so you should avoid "unwrapping" KVars with a render() or .text() call before you need to.
The KVal.map {} function is a powerful tool for manipulating KVals and KVars without unwrapping them.
Rendering lists with renderEach
The renderEach()
function allows you to render a list of items, while
automatically updating the rendered DOM in response to changes in the list.
While a KVar<List<FooBar>>
could be passed to render()
, it would be
very inefficient because the entire list would be re-rendered every time.
renderEach()
will only re-render the elements that have changed.
The items are provided in an ObservableList, which implements the
MutableList
interface.
doc.body {
data class Pet(val name : String, val age : Int)
val obsList = ObservableList(listOf(
Pet("Sammy", 7),
Pet("Halley", 5),
Pet("Buddy", 3)
))
table {
renderEach(obsList) { item ->
tr {
td().text(item.name)
td().text(item.age.toString())
}
}
}
obsList.add(1, Pet("Bella", 2))
obsList.removeAt(2)
obsList.move(0, 1)
obsList[0] = Pet("Joe", 1)
}
Extracting data class properties
If your KVar contains a data class then you can use Kvar.property() to create a KVar from one of its properties which will update the original KVar if changed:
data class User(val name: String)
val user = KVar(User("Ian"))
val name = user.property(User::name)
name.value = "John"
println(user) // Will print: KVar(User(name = "John"))
Reversible mapping
If you check the type of counterDoubled, you'll notice that it's a KVal rather than a KVar, meaning it cannot be modified directly, only by changing the KVar it was mapped from.
So this will result in a compilation error:
val counter = KVar(0)
val counterDoubled = counter.map { it * 2 }
counterDoubled.value = 20 // <--- This won't compile
The KVar class has a second map() function which takes a ReversibleFunction implementation. This version of map will produce a KVar which can be modified, as follows:
val counterDoubled = counter.map(object : ReversibleFunction<Int, Int>("doubledCounter") {
override fun invoke(from: Int) = from * 2
override fun reverse(original: Int, change: Int) = change / 2
})
counter.value = 5
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")
// output: counter: 5, doubled: 10
counterDoubled.value = 12 // <-- Couldn't do this with a KVal
println("counter: ${counter.value}, doubled: ${counterDoubled.value}")
// output: counter: 6, doubled: 12
If the mapped Kvar is changed the original KVar it was mapped from will also change.
Reversible mappings are an advanced feature that you only need if you want the mapped value to be a mutable KVar. Most of the time the simple KVal.map {} function is what you need.
URL Routing
In a web application, routing is the process of using URLs to drive the user interface (UI). URLs are a prominent feature in every web browser, and have several main functions:
- Bookmarking - Users can bookmark URLs in their web browser to save content they want to come back to later.
- Sharing - Users can share content with others by sending a link to a certain page.
- Navigation - URLs are used to drive the web browser's back/forward functions.
Traditionally, visiting a different URL within the same website would cause a new page to be downloaded from the server, but current state-of-the-art websites are able to modify the page in response to URL changes without a full refresh.
With Kweb's routing mechanism you get this automatically.
A simple example
import kweb.Kweb
import kweb.dom.element.new
import kweb.dom.element.creation.tags.h1
import kweb.routing.route
fun main() {
Kweb(port = 16097) {
doc.body {
route {
path("/users/{userId}") { params ->
val userId = params.getValue("userId")
h1().text(userId.map { "User id: $it" })
}
path("/lists/{listId}") { params ->
val listId = params.getValue("listId")
h1().text(listId.map { "List id: $it" })
}
}
}
}
}
Now, if you visit http://localhost:16097/users/997, you will see:
User id: 997The value of these parameters can then be retrieved from the params
map, but note that the values are wrapped in a KVar<String>
object.
This means that you can use all of Kweb's state
management features to
render parts of the DOM using this value.
The key advantage here is that if the URL changes the page can be updated without a full page refresh, but rather only changing the parts of the DOM that need to change - this is much faster and more efficient.
Handing 404s
You can override the default 404 Page Not Found message in the event that none of the routes match, making it easy to integrate the 404 page with the style of your overall website:
route {
path("/users/{userId}") { params ->
// ...
}
notFound {
h1().text("Page not found!")
}
}
Modifying the URL
You can obtain and modify the URL of the current page using WebBrowser.url.
This returns a KVar<String>
which contains the URL relative to the
origin - so for the page http://foo/bar/z
the url
would be /bar/z
.
This KVar will update accordingly if the URL changes, and can also be
modified itself, which will change the URL.
Here is a more realistic example which demonstrates this:
import kweb.Kweb
import kweb.dom.element.creation.tags.a
import kweb.dom.element.new
import kweb.routing.route
import kweb.state.*
fun main() {
Kweb(port = 16097) {
doc.body {
route {
path("/") {
url.value = "/number/1"
}
path("/number/{num}") { params ->
val num = params.getValue("num").toInt()
a().text(num.map {"Number $it"}).on.click {
num.value++
}
}
}
}
}
}
If you visit http://localhost:16097/
the URL will immediately update
to http://localhost:16097/number/1
without a page refresh, and you'll
see a hyperlink with text Number 1
. If you click on this link you'll
see that the number increments (both in the URL and in the link text),
also without a page refresh.
num.value++
is worthy of additional attention. num
is a KVar<Int>
converted from the KVar<String>
supplied as a parameter. Because of this
it can be incremented via its value
property.
This change will propagate back causing the URL to update, which will in-turn cause the DOM to update to reflect the new URL. All of this is handled for you automatically.
CSS & Style
Kweb can integrate easily with most CSS frameworks, particularly those that don't have a heavy reliance on JavaScript. Injecting custom CSS files is also supported by using the CSSPlugin.
CSS Frameworks
Fomantic UI
Kweb has out-of-the-box support for the excellent Fomantic UI framework, which helps create beautiful, responsive layouts using human-friendly HTML.
Getting started
First tell Kweb to use the Fomantic UI plugin:
import kweb.*
import kweb.plugins.fomanticUI.*
import kweb.plugins.javascript.JavascriptPlugin
fun main() {
Kweb(port = 16097, plugins = listOf(fomanticUIPlugin)) {
// ...
}
}
Now the plugin will add the Fomantic CSS and JavaScript code to your website automatically.
Let's look at one of the simple examples from the Fomantic UI documentation:
<div class="ui icon input">
<input type="text" placeholder="Search...">
<i class="search icon"></i>
</div>
This translates to the Kotlin:
Kweb(port = 16097, plugins = listOf(fomanticUIPlugin)) {
doc.body {
div(fomantic.ui.icon.input) {
input(type = InputType.text, placeholder = "Search...")
i(fomantic.search.icon)
}
}
}
Take a look at the Fomantic UI documentation to see everything else it can do.
Example and Demo
- freenet.org A Kweb website built on Google Cloud Platform with Fomantic styling.
Other UI Frameworks
Kweb is known to work well with a number of other CSS frameworks, including:
Custom CSS files
The CSSPlugin can be used to conveniently add multiple CSS files to your website, just add it to your resources folder as follows:
├── src
│ └─── main
│ └─── resources
│ └─── css
│ └── test.css
Next add the plugin via the plugins constructor parameter.
Kweb(port = 16097, plugins = listOf(CSSPlugin("css", "test.css"))) {
// ...
}
Specify the relative path to the folder inside src/main/resources and the files to include (either a single file name or a list of file names).
The files will be served under /kweb_static/css and linked from the websites HTML head tag, for example:
<link rel="stylesheet" type="text/css" href="/kweb_static/css/test.css">
Components
Managing Complexity
Composable components help manage software complexity by allowing developers to break down a complex problem into smaller, more manageable pieces. Other benefits include reusability, testability, and the ability to reason about a system in isolation.
The Component typealias
We rely on a small amount of syntactic sugar defined in kweb.components.Component:
typealias Component = ElementCreator<*>
And then we can use an extension function on this to create a component:
fun Component.simple(
prompt: String = "Enter Your Name",
name: KVar<String>
) {
div {
h1().text(prompt)
input(type = text).value = name
}
div {
span().text(name.map { "Hello, $it" })
}
}
And we use the component like this:
Kweb(port = 16097) {
doc.body {
simple(name = kvar("World"))
}
}
Components are configured through the extension function parameters, typically through a mixture of:
Parameter Type | For |
---|---|
KVals | Values that can change but not be modified by the component |
KVars | Values that can change or be modified by the component |
ObservableLists | Lists of values that can change or be modified by the component |
Double, String, etc | Values that don't change |
The simplest Component's may have no parameters at all, or just one or two, while the most complex might use a DSL builder.
A more complex example
In this example we create a Component that wraps an <input> element styled using the Bulma CSS framework:
interface BulmaClass {
val cssClassName: String
}
enum class BulmaColor(override val cssClassName: String) : BulmaClass {
PRIMARY("is-primary"), LINK("is-link"),
INFO("is-info"), SUCCESS("is-success"),
WARNING("is-warning"), DANGER("is-danger")
}
enum class BulmaSize(override val cssClassName: String) : BulmaClass {
SMALL("is-small"), NORMAL("is-normal"),
MEDIUM("is-medium"), LARGE("is-large")
}
enum class BulmaStyle(override val cssClassName: String) : BulmaClass {
ROUNDED("is-rounded"),
}
enum class BulmaState(override val cssClassName: String) : BulmaClass {
NORMAL("is-normal"), HOVER("is-hovered"),
FOCUS("is-focused"), LOADING("is-loading"),
}
fun Component.bulmaInput(
type: InputType,
value: KVar<String>,
vararg classes : KVal<out BulmaClass> = arrayOf(),
) {
input(type = type) { inputElement ->
var inputClassList: KVal<List<String>> = kval(listOf("input"))
for (c in classes) {
inputClassList += c.map { listOf(it.cssClassName) }
}
with(inputElement) {
classes(inputClassList.map { it.joinToString(" ") })
this.value = value
}
}
}
This component can then be used like this:
val bulmaUrl = "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css".json
fun shouldWarn(username : String) =
if (username.length < 5)
BulmaColor.WARNING
else
BulmaColor.SUCCESS
Kweb(port = 12354) {
doc.head {
element(
"link",
attributes = mapOf(
"rel" to "stylesheet".json,
"href" to bulmaUrl
)
)
}
doc.body {
val username = kvar("")
val color = username.map { shouldWarn(it) }
bulmaInput(type = text, value = username, color)
}
}
JavaScript Interop
- Introduction
- Calling a JavaScript function
- Calling with parameters
- Function caching and preloading
- Calling a JavaScript function with a result
- Including static JavaScript files
Introduction
Kweb's DOM interaction functionality is build on top of two functions that allow you to interact directly with the browser's JavaScript interpreter:
Note that this is unrelated to Kotlin's ability to compile to JavaScript.
Calling a JavaScript function
doc.body {
browser.callJsFunction("""alert("Hello World!")""")
}
Calling with parameters
You can pass parameters to a JavaScript function by passing them as arguments to the callJsFunction()
function,
using {}
for substitution:
doc.body {
val greeting = "Hello".json
val name = "World".json
browser.callJsFunction("""alert({} + " " + {} + "!")""", greeting, name)
}
Parameters must be converted to a JsonElement,
for example by using the json extension property. Within
the JavaScript code the {}
should be treated like a variable, so alert({})
is ok but alert("{}")
will not work.
Function caching and preloading
Kotlin automatically caches JavaScript functions in the browser for efficiency. If the function is first called during initial page render, it will be parsed and cached as part of the initial page load.
Calling a JavaScript function with a result
You can also retrieve a result from a function call using callJsFunctionWithResult().
Note that the last line of the jsBody
parameter must be a return
statement:
doc.body {
elementScope().launch {
val result : JsonElement = browser.callJsFunctionWithResult("return Date.now()")
println("Result: ${result.jsonPrimitive.intOrNull}")
}
}
callJsFunctionWithResult()
is a suspend function so it must be called inside a CoroutineScope.
You can create one within Kweb's DSL using elementScope(). This scope will be cancelled automatically
when this part of the DOM is no-longer needed.
Including static JavaScript files
The JavascriptPlugin can be used to conveniently add multiple static JavaScript files to your website, just add it to your resources folder as follows:
├── src
│ └─── main
│ └─── resources
│ └─── script
│ └── test.js
Next add the plugin via the plugins constructor parameter.
Kweb(port = 16097, plugins = listOf(JavascriptPlugin("script", "test.js"))) {
// ...
}
Specify the relative path to the folder inside src/main/resources and the files to include (either a single file name or a list of file names).
The files will be served under /kweb_static/js and linked from the websites HTML head tag, for example:
<script type="text/javascript" src="/kweb_static/js/test.js"></script>
Speed & Efficiency
Kweb is designed to be fast and efficient, both in the browser and on the server. This chapter documents some of the techniques Kweb uses to achieve this.
Server-side rendering
Kweb uses the excellent JSoup library to render the initial HTML page, which is supplied to the browser when the page first loads. This leads to a faster initial page load, and allows search engines to index the page even if they aren't JavaScript aware.
Hydration
Kweb uses a technique called hydration to add JavaScript listeners to the DOM elements that were rendered by the server.
WebSockets
After the initial page load Kweb uses WebSockets to communicate with the browser. This is more efficient than HTTP because messages can be initiated by either the browser or the server, and the connection is kept open between messages.
Immediate events
Normal DOM events are reported to the Kweb server which then decides how to handle them. In some situations this can cause a short but noticeable delay. Immediate events address this by allowing the event handler's DOM changes to be "recorded" by Kweb and sent to the browser in advance so it can be executed immediately.
JavaScript caching
After the initial page load, Kweb modifies the DOM by sending JavaScript to the browser. Much of this JavaScript is sent as part of the initial page load, but some is sent dynamically as the user interacts with the page. This JavaScript is parsed using Function (which is more efficient than eval), and then cached in the browser for future use.
Input field diffing
When an <input>
, <select>
, or <textarea>
element value`
is modified, Kweb only sends the change to the browser rather than the entire field.
Frequently Asked Questions
- Won't Kweb be slow relative to client-side web frameworks?
- What's the difference between Kweb and Vaadin?
- Is there a larger working example?
- How do I enable HTTPS?
- Can I embed Kweb within an Android app?
- I want to deploy a Kweb Application behind a load balancer, what do i need to consider?
- What about templates?
- Why risk my project on a framework I just heard of?
- How is "Kweb" pronounced?
- Can Kweb be embedded in an existing Ktor app?
- How do I add a favicon?
- How do I enable auto-reloading?
- I have a question not answered here
Won't Kweb be slow relative to client-side web frameworks?
No, Kweb's immediate events allows you to avoid any server communication delay by responding immediately to DOM-modifying events.
Kweb is designed to be efficient by default, minimizing both browser and server CPU/memory.
If you encounter a situation in which Kweb is slow please submit a bug.
What's the difference between Kweb and Vaadin?
Of all web frameworks we're aware of, Vaadin is the closest in design and philosophy to Kweb, but there are also important differences:
- Kweb is far more lightweight than Vaadin. At the time of writing, kweb-core is about 4,351 lines of code, while vaadin/framework is currently 502,398 lines of code, almost a 100:1 ratio!
- Vaadin doesn't have any equivalent feature to Kweb's immediate events, which has led to frequent complaints of sluggishness from Vaadin users because a server round-trip is required to update the DOM.
- Vaadin brought a more desktop-style of user interface to the web browser, but since then we've realized that users generally prefer their websites to look like websites.
- This is why Kweb's philosophy is to be a thin interface between server logic and the user's browser, leveraging existing tools from the JavaScript ecosystem.
- Kweb was built natively for Kotlin, and takes advantage of all of its language features like coroutines and the flexible DSL-like syntax. Because of this Kweb code can be a lot more concise, without sacrificing readability.
- In Vaadin's favor, it has been a commercial product since 2006, it is extremely mature and has a vast developer ecosystem, while Kweb is still relatively new.
Is there a larger working example?
Yes, freenet.org is running on Kweb, which is in-turn running on Google Cloud Platform with FireStore as a database.
You can see a number of other example Kweb projects here: kwebio/kweb-demos
How do I enable HTTPS?
Very easily, please see this example. You can also rely on a load-balancer to translate between HTTPS and HTTP so that this is handled external to Kweb.
Note: It is highly recommended that you use HTTPS as plain HTTP is vulnerable to session hijacking, and Kweb does not yet (Jan 2023) implement mitigation measures against this. This is a common shortcoming for may web frameworks.
Can I embed Kweb within an Android app?
Yes! Please see kweb-demos/tree/master/android for an example.
I want to deploy a Kweb Application behind a load balancer, what do i need to consider?
Please make sure to enable session affinity so that repeated requests from the same client end up at the same kweb instance. Kweb does not share its internal state between multiple instances, so it is important to make sure that each request from a single user ends up at always the same instance.
If the load balancer uses e.g. round robin strategy for load balancing, repeated requests end up at different backend instances and kweb may not function propery.
Example how to setup HAProxy can be found here.
What about templates?
Kweb replaces templates with something better - a typesafe HTML DSL embedded within a powerful programming language.
If you like you could separate out the code that interfaces directly to the DOM - this would be architecturally closer to a template-based approach, but we view it as a feature that this paradigm isn't forced on the programmer.
Why risk my project on a framework I just heard of?
Picking a framework is stressful. Pick the wrong one and perhaps the company behind it goes out of business, meaning your entire app is now built on something obsolete. We've been there.
Kweb's development is driven by a community of volunteers. We welcome contributions from anyone, but Kweb doesn't depend on any sponsoring company.
Because of the powerful abstractions it's built on, Kweb also has the advantage of simplicity (<5k loc). This makes it easier for people to contribute, and less code means fewer bugs.
How is "Kweb" pronounced?
One syllable, like qu from quick - qu-web.
Can Kweb be embedded in an existing Ktor app?
Yes! Please see this example.
How do I add a favicon?
The easiest way is using the FaviconPlugin:
val faviconPlugin = FaviconPlugin {
respondBytes(getFaviconAsBytes(), ContentType.Image.Any)
}
Kweb(port = 16097, plugins = listOf(faviconPlugin)) {
doc.body {
// ...
}
}
If you don't provide a FaviconPlugin of some kind and KwebConfiguration.handleFavicon is true, Kweb will automatically use FaviconPlugin.notFound(), which will return a 404 response for any favicon requests.
If you need to handle favicon requests yourself, you can set KwebConfiguration.handleFavicon to false.
How do I enable auto-reloading?
See Ktor's auto-reloading documentation.
I have a question not answered here
Feel free to ask us a question on Github Issues, but please search first to see whether it has already been answered. For a more realtime experience you can also chat with us on Matrix.