Henning Koch
Head of Development at
makandra
@triskweline
Entire app runs on a single computer
Entire app is (often) written in a single language
Popular languages: Java, C#, C++, Objective-C
Application runs on multiple computers
Typical: One server, many clients (= web browsers)
Client and server run different parts of the application
and they communicate over HTTP
Application is written in a "stack" of many technologies
Typical stack: HTML, CSS, JavaScript, Ruby/Node/Java/C#, SQL
Language | Purpose | Runs where? |
---|---|---|
HTML | Textual content | Client |
CSS | Layout & Design | Client |
JavaScript | Client-side interaction | Client |
HTTP | Client/server communication | Client/server |
Ruby/Node/Java/C# | Controller & Model | Server |
SQL | Database access | Server |
http://webdev101.makandra.de
https://github.com/makandra/webdev101
<html>
<body>
<h1>Hi there!</h1>
<p>Look at this image:</p>
<img src="image.jpg">
</body>
</html>
View example
(use the inspector)
User enters into her browser's address bar:
http://makandra.com/page.html
What happens?
Browser asks server:
GET /page.html HTTP/1.1 # I want the file /page.html
Host: makandra.com # I want it from makandra.com
Accept: text/html, text/plain # I understand HTML and plain text
Server looks for a local file page.html
and replies:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 114 # What you want has 114 bytes
Content-Type: text/html # And it's in HTML format
# Here it comes:
<html>
<body>
<h1>Hi there!</h1>
<p>Look at this image:</p>
<img src="image.jpg">
</body>
</html>
The browser parses the HTML and encounters this tag:
<img src="image.jpg">
What happens?
Browser asks server:
GET /image.jpg HTTP/1.1 # I want the file /image.jpg
Host: makandra.com # I want it from makandra.com
Accept: image/jpeg, image/png # I understand JPEG and PNG
Server looks for a local file image.jpg
and replies:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 67840 # What you want has 66 KB
Content-Type: image/jpeg # And it's in JPEG format
# Here it comes:
FF D8 FF E0 00 10 4A 46
49 46 00 01 01 01 00 48
00 48 00 00 FF E1 1C 13
45 78 69 66 00 00 49 49
2A 00 08 00 00 00 0D 00
0F 01 02 00 06 00 00 00
...
<html>
<body>
<h1>About Capybaras</h1>
<p>
The capybara is the
largest rodent
in the world.
</p>
</body>
</html>
<html>
<body>
<h1>About Capybaras</h1>
<p>
The capybara is the
largest rodent
in the world.
</p>
</body>
</html>
body {
font-family: 'Arial';
}
h1 {
text-transform: uppercase;
font-style: italic;
}
b {
background-color: #ff9900;
}
#ff0000
#00ff00
#0000ff
#ffff00
#ffffff
#000000
#ff9900
#4488bb
#9900ff
#777777
#22aa88
#885555
red
yellow
fuchsia
rebeccapurple
rgba(255, 0, 0, 1.0)
rgba(255, 0, 0, 0.75)
rgba(255, 0, 0, 0.5)
rgba(255, 0, 0, 0.25)
hsl(0, 100%, 50%)
hsl(30, 100%, 50%)
hsl(30, 50%, 50%)
hsl(30, 50%, 70%)
<html>
<head>
<style>
h1 {
color: #ff0000;
}
</style>
</head>
<body>
...
</body>
</html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
...
</body>
</html>
The browser parses the HTML and encounters this tag:
<link rel="stylesheet" href="styles.css">
What happens?
Browser asks server:
GET /styles.css HTTP/1.1 # I want the file /styles.css
Host: makandra.com # I know you as makandra.com
Accept: text/css # I understand the CSS format
Server looks for a local file styles.css
and replies:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 182 # What you want has 182 bytes
Content-Type: text/css # And it's in CSS format
# Here it comes:
body {
font-family: 'Arial';
}
h1 {
text-transform: uppercase;
font-style: italic;
}
...
body {
font-family: 'Arial';
}
h1 {
text-transform: uppercase;
font-style: italic;
}
b {
background-color: #ff0000;
}
body {
font-family: 'Arial';
}
h1 {
text-transform: uppercase;
font-style: italic;
}
b {
background-color: #ff0000;
}
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
h1 { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p.introduction { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p:last-child { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
h1, p:last-child { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
h1+p { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
b { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
p.introduction b { ... }
<h1>About Capybaras</h1>
<p class="introduction">
The capybara is the largest <b>rodent</b>
in the world.
</p>
<p>
The capybara belongs to the subfamily
<b>Hydrochoerinae</b> along with ...
</p>
.container {
display: block; /* default */
}
.container {
display: flex;
}
.container {
display: flex;
justify-content: flex-start; /* default */
}
.container {
display: flex;
justify-content: flex-end;
}
.container {
display: flex;
justify-content: space-between;
}
.container {
display: flex;
justify-content: space-around;
}
.container {
display: flex;
justify-content: center;
}
.container {
display: flex;
}
.child:nth-child(1) {
margin-right: auto;
}
.container {
display: flex;
}
.child {
flex-grow: 1;
}
.container {
display: flex;
}
.child:nth-child(2) {
flex-grow: 1;
}
.container {
display: flex;
}
.child:nth-child(1) {
flex-grow: 1;
}
.child:nth-child(2) {
flex-grow: 2;
}
.container {
display: flex;
}
.child {
flex-basis: 200px;
}
.container {
display: flex;
}
.child {
flex-basis: 200px;
}
.child:nth-child(2) {
flex-grow: 1;
}
.container {
display: flex;
}
.container {
display: flex;
}
.child {
flex-shrink: 0;
}
.container {
display: flex;
flex-wrap: wrap;
}
.container {
display: flex;
flex-wrap: wrap;
}
.child {
flex-basis: 25%;
}
.container {
display: flex;
flex-wrap: wrap;
}
.child {
flex-basis: 25%;
flex-grow: 1;
}
.container {
display: flex;
flex-direction: row;
}
.container {
display: flex;
flex-direction: row-reverse;
}
.container {
display: flex;
flex-direction: column;
}
.container {
display: flex;
flex-direction: column-reverse;
}
.container {
display: flex;
height: 200px;
align-items: stretch; /* default */
}
.container {
display: flex;
height: 200px;
align-items: flex-start;
}
.container {
display: flex;
height: 200px;
align-items: flex-end;
}
.container {
display: flex;
height: 200px;
align-items: center;
}
.container {
display: flex;
height: 200px;
align-items: center;
}
.child:nth-child(2) {
align-self: stretch;
}
.container {
display: flex;
height: 200px;
align-items: center;
justify-content: center;
}
Instead of starting from scratch you can build upon someone else's CSS.
CSS frameworks like Bootstrap, Bulma or Primer give you:
Let's write a web app that returns the current time:
http://timeapp.com/now.html
Browser asks server:
GET /now.html HTTP/1.1 # I want the file /now.html
Host: timeapp.com # I want it from timeapp.com
Accept: text/html, text/plain # I understand HTML and plain text
Server looks for a local file now.html
and replies:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 22 # What you want has 22 bytes
Content-Type: text/plain # And it's in plain text
# Here it comes:
The time is 15:00:00.
Browser asks server:
GET /now.html HTTP/1.1 # I want the file /now.html
Host: timeapp.com # I want it from timeapp.com
Accept: text/html, text/plain # I understand HTML and plain text
Server runs a program time.exe
.
The program time.exe
writes a HTTP response
with the current time to STDOUT
:
puts "HTTP/1.1 200 OK"
puts "Content-Length: 22"
puts "Content-Type: text/plain"
puts
puts "The time is " + Time.now.to_s
The browser simply makes a request and gets back a string of HTML (wrapped in HTTP).
The browser doesn't even know if the HTML was generated by an app,
or if it was simply read from a static file.
Popular choices:
The following examples are in Ruby, but concepts apply to all languages.
class User
def initialize(name, age)
@name = name
@age = age
end
def adult?
@age >= 18
end
end
user = User.new('Max', 18)
puts user.name
puts user.adult?
class User {
private String name;
private int age;
public User(name, age) {
this.name = name;
this.age = age;
}
public boolean isAdult() {
return age >= 18;
}
}
var user = new User("Max", 18);
System.out.println(user.getName());
System.out.println(user.isAdult());
To run the chat app:
cd examples/2000_chat
bundle install
bundle exec ruby app.rb
You can now access the app at http://localhost:4567/
The user types into her address bar:
http://chatapp.com/
What happens?
Browser asks server:
GET / HTTP/1.1 # I want /
Host: chatapp.com # I want it from chatapp.com
Accept: text/html, text/plain # I understand HTML and plain text
Server asks the program app.rb
for /
.
The program prints a new HTML page with the latest chat messages.
The server takes the program output and sends it the browser:
HTTP/1.1 200 OK # I found what you wanted
Content-Length: 1589 # What you want has 1589 bytes
Content-Type: text/html # And it's in HTML format
# Here it comes:
<html>
...
</html>
The user sends a message to the chat channel:
Hello world
What happens?
Browser asks server:
POST /send HTTP/1.1 # I'm posting data to /send
Host: chatapp.com # I'm posting it to chatapp.com
Content-Type: application/x-www-form-urlencoded # I'm posting key/value pairs
# Here it comes:
message=Hello+world
Server calls the program app.rb
with the posted data.
The program stores the message and prints a HTTP response.
The server takes the program output and sends it the browser:
HTTP/1.1 303 See Other # This conversation continues elsewhere
Location: / # Request / to continue
The browser automatically makes another request to /
and gets a fresh copy of the message list.
These things require code that runs in the browser, not on the server.
// This is a comment
let x = 1
let y = x + 1
console.log(y) // 2
Try it in your browser console! (CTRL+Shift+J
)
function square(n) {
return n * n
}
let x = square(16)
console.log(x) // 256
let z = square
console.log(z) // function { ... }
console.log(z(4)) // 16
one
two
three
let div = document.querySelector('div')
let children = div.children
console.log(children) // [one
, two
, three
]
let parent = children[0].parentNode
console.log(parent) // [...]
console.log(parent === div) // true
children[1].style.color = 'red'
<html>
<body>
Hi world
<script>
var x = 1;
var y = x + y;
</script>
</body>
</html>
<html>
<body>
Hi world
<script src="scripts.js"></script>
</body>
</html>
<button>Click me</button>
<script>
let button = document.querySelector('button')
button.addEventListener('click', function(event) {
alert('Button was clicked!')
})
window.addEventListener(function(event) {
alert('User scrolled!')
})
</script>
Client-side JavaScript that enhances server-rendered HTML is often called Progressive Enhancement or JavaScript Sprinkles.
null
.this.foo()
to call your own methods.this
.(But it's the only language we get in the browser, so deal with it.)
function showLastMessage() {
let lastMessage = document.querySelector('.message:last-child')
if (lastMessage) {
lastMessage.scrollIntoView()
}
}
window.addEventListener('load', showLastMessage)
⇢ User requests a website
⇠ Server sends a fresh copy
⇢ User clicks a link
⇠ Server sends a fresh copy
⇢ User submits a form
⇠ Server sends a fresh copy
⇢ User reloads the page manually
⇠ Server sends a fresh copy
⇣ Another user sends a message
× Server has no way to push the update to the user
fetch()
async function refresh() {
let response = await fetch('/')
let newHTML = await response.text()
let parser = new DOMParser()
let newDocument = parser.parseFromString(newHTML, 'text/html')
let newChannel = newDocument.querySelector('.channel')
let oldChannel = document.querySelector('.channel')
oldChannel.replaceWith(newChannel)
}
setInterval(refresh, 1000)
function search() {
let queryInput = document.querySelector('.search-form input')
let query = queryInput.value.toLowerCase()
let messageElements = document.querySelectorAll('.message')
for (let messageElement of messageElements) {
var text = messageElement.textContent.toLowerCase()
if (text.includes(query)) {
messageElement.style.display = 'block'
} else {
messageElement.style.display = 'none'
}
}
}
window.addEventListener('load', function() {
let queryInput = document.querySelector('.search-form input')
queryInput.addEventListener('input', search)
})
To run the chat app:
cd examples/2010_chat_with_javascript
bundle install
bundle exec ruby src/app.rb
Also known as Single-page apps or SPAs.
To run the chat app:
cd examples/2020_chat_spa
bundle install
bundle exec ruby src/app.rb
Server-side app | Client-side app | |
---|---|---|
Code complexity | low | high |
Language | your choice | JavaScript |
Data access | sync | async via API |
Initial load time | fast | slow |
Optimistic rendering | duplicates logic | view bound to client-side state |
Offline | very hard | hard |
@triskweline
henning.koch@makandra.de
http://webdev101.makandra.de
https://github.com/makandra/webdev101
app.rb
Our current implementation has app.rb
is a single script
with a lot of responsibilities:
As software engineers we always try to separate responsibilities into individual modules.
get '/' do
@messages = File.read('messages.txt').lines
erb(:channel)
end
get '/send' do
message = params['message'] + "\n"
File.write('messages.txt', message, mode: 'a')
redirect('/')
end
db = Mydb::Client.new(host: 'localhost')
get '/' do
@messages = db.select_values('SELECT text FROM messages')
erb(:channel)
end
get '/send' do
message = params['message']
escaped = db.escape(message)
db.execute('INSERT INTO messages VALUES (' + escaped + ')')
redirect('/')
end
channel = Channel.new
get '/' do
@messages = channel.messages
erb(:channel)
end
get '/send' do
message = params['message']
channel.add_message(message)
redirect('/')
end
class Channel
def initialize
@db = Mydb::Client.new
end
def messages
@db.select_values('SELECT text FROM messages')
end
def add_message(message)
escaped = @db.escape(message)
@db.execute('INSERT INTO messages VALUES (' + escaped + ')')
end
end
Layer | Responsibility |
---|---|
Database | Data storage & queries |
Model | O/R mapping, business logic |
Controller | Layer orchestration |
View | Content |
Stylesheets | Optics (optional) |
JavaScript | Behavior enhancements (optional) |
telnet example.com 80