Siirry sisältöön
a Yleistä

b

Web-sovelluksen toimintaperiaatteita

Ennen kuin aloitamme ohjelmoinnin, käymme läpi web-sovellusten toimintaperiaatteita tarkastelemalla osoitteessa https://studies.cs.helsinki.fi/exampleapp olevaa esimerkkisovellusta.

Sovelluksen olemassaolon tarkoitus on ainoastaan havainnollistaa kurssin peruskäsitteistöä. Sovellus ei ole missään tapauksessa esimerkki siitä, miten web-sovelluksia kannattaisi kehittää. Päinvastoin se demonstroi eräitä historiallisia web-sovellusten toteutukseen käytettyjä tapoja ja tekniikoita, joiden katsotaan nykyään olevan jopa huonoja käytänteitä.

Kurssin suosittelemaa tyyliä noudattavan koodin kirjoittaminen alkaa osasta 1.

Avataan selaimella esimerkkisovellus. Sivun ensimmäinen lataus kestää joskus hetken.

Kurssimateriaali olettaa, että käytössä on Chrome-selain.

Web-sovelluskehityksen sääntö numero yksi: pidä selaimen Developer-konsoli koko ajan auki. Konsoli avautuu Macilla painamalla yhtä aikaa option cmd ja i. Windowsilla konsolin saa auki painamalla F12 tai yhtä aikaa ctrl shift ja i.

Ennen kuin jatkat eteenpäin, selvitä, miten saat koneellasi konsolin auki (googlaa tarvittaessa), ja muista pitää se auki aina, kun teet web-sovelluksia.

Konsoli näyttää seuraavalta:

fullstack content

Varmista, että välilehti Network on avattuna ja aktivoi valinta Disable cache kuten kuvassa on tehty. Myös Preserve logs on joskus hyödyllinen, sillä se säilyttää sovelluksen tulostamat logit sivujen uudelleenlatauksen yhteydessä.

HUOM: konsolin tärkein välilehti on Console. Käytämme nyt johdanto-osassa kuitenkin ensin melko paljon välilehteä Network.

HTTP GET

Selain ja web-palvelin kommunikoivat keskenään HTTP-protokollaa käyttäen. Avoinna oleva konsolin Network-välilehti kertoo, miten selain ja palvelin kommunikoivat.

Kun päivität sivun (eli painat F5-näppäintä tai selaimessa olevaa symbolia ↺), konsoli kertoo, että tapahtuu kaksi asiaa:

fullstack content

Jos ruutusi on pieni, saatat joutua suurentamaan konsoli-ikkunaa, jotta saat selaimen tekemät haut näkyviin.

Klikkaamalla näistä ensimmäistä paljastuu tarkempaa tietoa siitä, mistä on kyse:

fullstack content

Ylimmästä osasta General selviää, että selain teki GET-metodilla pyynnön osoitteeseen https://studies.cs.helsinki.fi/exampleapp/ ja että pyyntö oli onnistunut, sillä pyyntöön saatiin vastaus, jonka statuskoodi on 200.

Pyyntöön ja palvelimen lähettämään vastaukseen liittyy erinäinen määrä otsakkeita eli headereita:

fullstack content

Ylempänä oleva Response headers kertoo mm. vastauksen koon tavuina ja vastaushetken. Tärkeä headeri Content-Type kertoo, että vastaus on utf-8-muodossa oleva tekstitiedosto, jonka sisältö on muotoiltu HTML:llä. Näin selain tietää, että kyseessä on normaali HTML-sivu, joka tulee renderöidä käyttäjän selaimeen "web-sivun tavoin".

Välilehti Response näyttää, miltä pyyntöön vastauksena lähetetty data näyttää. Kyseessä on siis normaali HTML-sivu, jonka body-osassa määritellään selaimessa näytettävän sivun rakenne:

fullstack content

Sivu sisältää div-elementin, jonka sisällä on otsikko, tieto luotujen muistiinpanojen määrästä, linkki sivulle notes ja kuvaa vastaava img-tagi.

img-tagin ansiosta selain tekee toisenkin HTTP-pyynnön, jonka avulla se hakee kuvan kuva.png palvelimelta. Pyynnön tiedot näyttävät seuraavalta:

fullstack content

Eli pyyntö on tehty osoitteeseen https://studies.cs.helsinki.fi/exampleapp/kuva.png, ja se on tyypiltään HTTP GET. Vastaukseen liittyvät headerit kertovat, että vastauksen koko on 89350 tavua ja vastauksen Content-type on image/png, eli kyseessä on png-tyyppinen kuva. Tämän tiedon ansiosta selain tietää, miten kuva on piirrettävä HTML-sivulle.

Sivun https://studies.cs.helsinki.fi/exampleapp/ avaaminen selaimessa saa siis aikaan alla olevan sekvenssikaavion kuvaaman tapahtumasarjan:

fullstack content

Sekvenssikaavio kuvaa selaimen (browser) ja palvelimen (server) välisen kommunikaation aikajärjestyksessä "ylhäältä alaspäin", eli ylimpänä on ensin selaimen ensin lähettämä pyyntö, jonka jälkeen tulee palvelimen vastaus tähän pyyntöön ja tämän jälkeen selaimen seuraava pyyntö jne.

Ensin selain tekee palvelimelle HTTP GET ‑pyynnön, jonka avulla se hakee sivun HTML-koodin. HTML-koodissa olevan img-tagin ansiosta selain hakee palvelimelta kuvan kuva.png. Selain renderöi HTML-kielellä muotoillun sivun ja kuvan näytölle. Vaikka käyttäjä ei sitä helposti huomaa, alkaa sivu renderöityä näytölle jo ennen kuvan hakemista.

Perinteinen web-sovellus

Esimerkkisovelluksen pääsivu toimii perinteisen web-sovelluksen tapaan. Mentäessä sivulle selain hakee palvelimelta sivun strukturoinnin ja tekstuaalisen sisällön määrittelevän HTML-dokumentin.

Palvelin on muodostanut dokumentin jollain tavalla. Dokumentti voi olla staattista sisältöä eli palvelimen hakemistossa oleva tekstitiedosto. Dokumentti voi myös olla dynaaminen, eli palvelin voi muodostaa HTML-dokumentit ohjelmakoodin avulla hyödyntäen esim. tietokannassa olevaa dataa. Esimerkkisovelluksessa sivun HTML-koodi on muodostettu dynaamisesti, sillä se sisältää tiedon luotujen muistiinpanojen lukumäärästä.

Etusivun muodostava koodi näyttää seuraavalta:

const getFrontPageHtml = noteCount => {
  return `
    <!DOCTYPE html>
    <html>
      <head>
      </head>
      <body>
        <div class="container">
          <h1>Full stack -esimerkkisovellus</h1>
          <p>muistiinpanoja luotu ${noteCount} kappaletta</p>
          <a href="/notes">muistiinpanot</a>
          <img src="kuva.png" width="200" />
        </div>
      </body>
    </html>
  `
}

app.get('/', (req, res) => {
  const page = getFrontPageHtml(notes.length)
  res.send(page)
})

Koodia ei tarvitse vielä ymmärtää, mutta käytännössä HTML-sivun sisältö on talletettu ns. template stringinä eli merkkijonona, jonka sekaan on mahdollisuus evaluoida esim. muuttujien arvoja. Etusivun dynaamisesti muuttuva osa eli muistiinpanojen lukumäärä (koodissa noteCount) korvataan template stringissä senhetkisellä, konkreettisella lukuarvolla (koodissa notes.length).

HTML:n kirjoittaminen suoraan koodin sekaan ei tietenkään ole järkevää, mutta vanhan liiton PHP-ohjelmoijille se oli arkipäivää.

Perinteisissä websovelluksissa selain on "tyhmä" – se ainoastaan pyytää palvelimelta HTML-muodossa olevia sisältöjä, kaikki sovelluslogiikka on palvelimessa. Palvelin voi olla tehty esim. jo eläkkeelle jääneen kurssin Web-palvelinohjelmointi tapaan Java Springillä, tietokantasovelluksessa käytetyllä Python Flaskilla tai Ruby on Railsilla. Esimerkissä on käytetty Node.js:n Express-kirjastoa. Tulemme käyttämään kurssilla Node.js:ää ja Expressiä web-palvelimen toteuttamiseen.

Selaimessa suoritettava sovelluslogiikka

Pidä konsoli edelleen auki. Tyhjennä konsolin näkymä painamalla vasemmalla olevaa ⦸-symbolia.

Kun menet nyt muistiinpanojen sivulle eli klikkaat linkkiä notes, selain tekee neljä HTTP-pyyntöä:

fullstack content

Kaikki pyynnöt ovat erityyppisiä. Ensimmäinen pyyntö on tyypiltään document. Kyseessä on sivun HTML-koodi, joka näyttää seuraavalta:

fullstack content

Kun vertaamme selaimen näyttämää sivua ja pyynnön palauttamaa HTML-koodia, huomaamme, että koodi ei sisällä ollenkaan muistiinpanoja sisältävää listaa.

HTML-koodin head-osio sisältää script-tagin, jonka ansiosta selain lataa main.js-nimisen JavaScript-tiedoston palvelimelta.

Ladattu JavaScript-koodi näyttää seuraavalta:

var xhttp = new XMLHttpRequest()

xhttp.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    const data = JSON.parse(this.responseText)
    console.log(data)

    var ul = document.createElement('ul')
    ul.setAttribute('class', 'notes')

    data.forEach(function(note) {
      var li = document.createElement('li')

      ul.appendChild(li)
      li.appendChild(document.createTextNode(note.content))
    })

    document.getElementById('notes').appendChild(ul)
  }
}

xhttp.open('GET', '/data.json', true)
xhttp.send()

Koodin yksityiskohdat eivät ole tässä osassa oleellisia, koodia on kuitenkin liitetty mukaan tekstin ja kuvien mausteeksi. Pääsemme kunnolla koodin pariin vasta osassa 1. Tämän osan esimerkkisovelluksen koodi ei itse asiassa ole ollenkaan relevanttia kurssilla käytettävien ohjelmointitekniikoiden kannalta.

Joku saattaa ihmetellä, miksi käytössä on xhttp-olio eikä modernimpi fetch. Syynä on se, että tässä osassa ei haluta mennä ollenkaan promiseihin ja koodin rooli esimerkissä on muutenkin sekundäärinen. Palaamme osassa 2 uudenaikaisempiin tapoihin tehdä pyyntöjä palvelimelle.

Heti ladattuaan script-tagin sisältämän JavaScriptin selain suorittaa koodin.

Kaksi viimeistä riviä määrittelevät, että selain tekee GET-tyyppisen HTTP-pyynnön palvelimen osoitteeseen /data.json:

xhttp.open('GET', '/data.json', true)
xhttp.send()

Kyseessä on alin Network-välilehden näyttämistä selaimen tekemistä pyynnöistä.

Voimme kokeilla mennä osoitteeseen https://studies.cs.helsinki.fi/exampleapp/data.json suoraan selaimella:

fullstack content

Osoitteesta löytyvät muistiinpanot JSON-muotoisena "raakadatana". Oletusarvoisesti selain ei osaa näyttää JSON-dataa kovin hyvin, mutta on olemassa lukuisia plugineja, jotka hoitavat muotoilun. Asenna nyt Chromeen esim. JSONView ja lataa sivu uudelleen. Data on nyt miellyttävämmin muotoiltua:

fullstack content

Yllä oleva muistiinpanojen sivun JavaScript-koodi siis lataa muistiinpanot sisältävän JSON-muotoisen datan ja muodostaa datan avulla selaimeen "bullet-listan" muistiinpanojen sisällöstä:

Tämän saa aikaan seuraava koodi:

const data = JSON.parse(this.responseText)
console.log(data)

var ul = document.createElement('ul')
ul.setAttribute('class', 'notes')

data.forEach(function(note) {
  var li = document.createElement('li')

  ul.appendChild(li)
  li.appendChild(document.createTextNode(note.content))
})

document.getElementById('notes').appendChild(ul)

Koodi muodostaa ensin järjestämätöntä listaa edustavan ul-tagin:

var ul = document.createElement('ul')
ul.setAttribute('class', 'notes')

ja lisää ul:n sisään yhden li-elementin kutakin muistiinpanoa kohti. Ainoastaan muistiinpanon content-kenttä tulee li-elementin sisällöksi. Raakadatassa olevia aikaleimoja ei käytetä mihinkään.

data.forEach(function(note) {
  var li = document.createElement('li')

  ul.appendChild(li)
  li.appendChild(document.createTextNode(note.content))
})

Avaa nyt konsolin Console-välilehti:

fullstack content

Painamalla rivin alussa olevaa kolmiota saat laajennettua konsolissa olevan rivin:

fullstack content

Konsoliin ilmestynyt tulostus johtuu siitä, että koodiin oli lisätty komento console.log:

const data = JSON.parse(this.responseText)
console.log(data)

eli vastaanotettuaan datan palvelimelta koodi tulostaa datan konsoliin.

Konsolin välilehti Console sekä komento console.log tulevat varmasti erittäin tutuiksi kurssin edetessä.

Tapahtumankäsittelijä ja takaisinkutsu

Koodin rakenne on hieman erikoinen:

var xhttp = new XMLHttpRequest()

xhttp.onreadystatechange = function() {
  // koodi, joka käsittelee palvelimen vastauksen
}

xhttp.open('GET', '/data.json', true)
xhttp.send()

eli palvelimelle tehtävä pyyntö suoritetaan vasta viimeisellä rivillä. Palvelimen vastauksen käsittelyn määrittelevä koodi on kirjoitettu jo aiemmin. Mistä on kyse?

Rivillä

xhttp.onreadystatechange = function () {

kyselyn tekevään xhttp-olioon määritellään tapahtumankäsittelijä (event handler) tilanteelle onreadystatechange. Kun kyselyn tekevän olion tila muuttuu, kutsuu selain tapahtumankäsittelijänä olevaa funktiota. Funktion koodi tarkastaa, että readyState:n arvo on 4 (joka kuvaa tilannetta The operation is complete) ja että vastauksen HTTP-statuskoodi on onnistumisesta kertova 200.

xhttp.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    // koodi, joka käsittelee palvelimen vastauksen
  }
}

Tapahtumankäsittelijöihin liittyvä mekanismi koodin suorittamiseen on JavaScriptissä erittäin yleistä. Tapahtumankäsittelijöinä olevia JavaScript-funktioita kutsutaan callback- eli takaisinkutsufunktioiksi, sillä sovelluksen koodi ei kutsu niitä itse vaan suoritusympäristö. Web-selain suorittaa funktion kutsumisen sopivana ajankohtana eli kyseisen tapahtuman tapahduttua.

Document Object Model eli DOM

Voimme ajatella, että HTML-sivut muodostavat implisiittisen puurakenteen


html
  head
    link
    script
  body
    div
      h1
      div
        ul
          li
          li
          li
      form
        input
        input

Sama puumaisuus on nähtävissä konsolin välilehdellä Elements:

fullstack content

Selainten toiminta perustuukin ideaan esittää HTML-elementit puurakenteena.

Document Object Model eli DOM on ohjelmointirajapinta eli API, joka mahdollistaa selaimessa esitettävien web-sivuja vastaavien elementtipuiden muokkaamisen ohjelmallisesti.

Edellisessä luvussa esittelemämme JavaScript-koodi käytti nimenomaan DOM-apia lisätäkseen sivulle muistiinpanojen listan.

Alla oleva koodi luo muuttujaan ul DOM-apin avulla uuden "solmun" ja lisää sille joukon lapsisolmuja:

var ul = document.createElement('ul')

data.forEach(function(note) {
  var li = document.createElement('li')

  ul.appendChild(li)
  li.appendChild(document.createTextNode(note.content))
})

lopulta muuttujassa ul oleva puun palanen yhdistetään sopivaan paikkaan koko sovelluksen HTML-koodia edustavassa puussa:

document.getElementById('notes').appendChild(ul)

document-olio ja sivun manipulointi konsolista

HTML-dokumenttia esittävän DOM-puun ylimpänä solmuna on olio nimeltään document. Olioon pääsee käsiksi Console-välilehdeltä (kirjoita sana document konsoliin ja paina enter):

fullstack content

Voimme suorittaa konsolista käsin DOM-apin avulla erilaisia operaatioita selaimessa näytettävälle web-sivulle hyödyntämällä document-olioa.

Lisätään nyt sivulle uusi muistiinpano suoraan konsolista.

Haetaan ensin sivulta muistiinpanojen lista eli sivun ul-elementeistä ensimmäinen:

list = document.getElementsByTagName('ul')[0]

luodaan uusi li-elementti ja lisätään sille sopiva tekstisisältö:

newElement = document.createElement('li')
newElement.textContent = 'Page manipulation from console is easy'

liitetään li-elementti listalle:

list.appendChild(newElement)
fullstack content

Vaikka selaimen näyttämä sivu päivittyy, muutos ei ole lopullinen. Jos sivu ladataan uudelleen, uusi muistiinpano katoaa, sillä muutos ei mennyt palvelimelle asti. Selaimen lataama JavaScript luo muistiinpanojen listan aina palvelimelta osoitteesta https://studies.cs.helsinki.fi/exampleapp/data.json haettavan JSON-muotoisen raakadatan perusteella.

CSS

Muistiinpanojen sivun HTML-koodin head-osio sisältää link-tagin, joka määrittelee, että selaimen tulee ladata palvelimelta osoitteesta main.css sivulla käytettävä CSS-tyylitiedosto.

Cascading Style Sheets eli CSS on kieli, jonka avulla web-sovellusten ulkoasu määritellään.

Ladattu CSS-tiedosto näyttää seuraavalta:

.container {
  padding: 10px;
  border: 1px solid;
}

.notes {
  color: blue;
}

Tiedosto määrittelee kaksi luokkaselektoria, joiden avulla valitaan tietty sivun alue ja määritellään alueelle sovellettavat tyylisäännöt.

Luokkaselektori alkaa aina pisteellä ja sisältää luokan nimen.

Luokat ovat attribuutteja, joita voidaan liittää HTML-elementeille.

Konsolin Elements-välilehti mahdollistaa class-attribuuttien tarkastelun:

fullstack content

sovelluksen uloimmalle div-elementille on siis liitetty luokka container. Muistiinpanojen listan sisältävä ul-elementin sisällä oleva lista sisältää luokan notes.

CSS-säännön avulla on määritelty, että container-luokan sisältävä elementti ympäröidään yhden pikselin paksuisella border:illa. Elementille asetetaan myös 10 pikselin padding, jonka ansiosta elementin sisällön ja elementin ulkorajan väliin jätetään hieman tilaa.

Toinen määritelty CSS-sääntö asettaa muistiinpanojen kirjainten värin siniseksi.

HTML-elementeillä on muitakin attribuutteja kuin luokkia. Muistiinpanot sisältävä div-elementti sisältää id-attribuutin. JavaScript-koodi hyödyntää attribuuttia elementin etsimiseen.

Konsolin Elements-välilehdellä on mahdollista manipuloida elementtien tyylejä:

fullstack content

Tehdyt muutokset eivät luonnollisesti jää voimaan, kun selaimen sivu uudelleenladataan, eli jos muutokset halutaan pysyviksi, tulee ne konsolissa tehtävien kokeilujen jälkeen tallettaa palvelimella olevaan tyylitiedostoon.

JavaScriptia sisältävän sivun lataaminen - kertaus

Kerrataan vielä, mitä tapahtuu, kun selaimessa avataan sivu https://studies.cs.helsinki.fi/exampleapp/notes

fullstack content
  • selain hakee palvelimelta sivun sisällön ja rakenteen määrittelevän HTML-koodin HTTP GET ‑pyynnöllä
  • HTML-koodi saa aikaan sen, että selain hakee sivun tyylit määrittelevän tiedoston main.css
  • sekä JavaScript-koodia sisältävän tiedoston main.js
  • selain alkaa suorittamaan hakemaansa JavaScript-koodia, joka tekee HTTP GET ‑pyynnön osoitteeseen https://studies.cs.helsinki.fi/exampleapp/data.json, josta muistiinpanot palautetaan JSON-muotoisena raakadatana
  • datan saapuessa selain suorittaa tapahtumankäsittelijän, joka renderöi muistiinpanot ruudulle käyttäen DOM-apia

Lomake ja HTTP POST

Tutkitaan seuraavaksi sitä, miten uusien muistiinpanojen luominen tapahtuu. Tätä varten muistiinpanojen sivu sisältää lomakkeen eli form-elementin.

fullstack content

Kun lomakkeen painiketta painetaan, lähettää selain lomakkeelle syötetyn datan palvelimelle. Avataan Network-välilehti ja katsotaan, miltä lomakkeen lähettäminen näyttää:

fullstack content

Lomakkeen lähettäminen aiheuttaa yllättäen yhteensä viisi HTTP-pyyntöä. Näistä ensimmäinen vastaa lomakkeen lähetystapahtumaa. Tarkennetaan siihen:

fullstack content

Kyseessä on siis HTTP POST ‑pyyntö, ja se on tehty palvelimen osoitteeseen new_note. Palvelin vastaa pyyntöön HTTP-statuskoodilla 302. Kyseessä on ns. uudelleenohjauspyyntö eli redirectaus, jonka avulla palvelin kehottaa selainta tekemään automaattisesti uuden HTTP GET ‑pyynnön headerin Location viittaamaan paikkaan eli osoitteeseen notes.

Selain siis lataa uudelleen muistiinpanojen sivun. Sivunlataus saa aikaan myös kolme muuta HTTP-pyyntöä: tyylitiedoston (main.css), JavaScript-koodin (main.js) ja muistiinpanojen raakadatan (data.json) lataamisen.

Network-välilehti näyttää myös lomakkeen mukana lähetetyn datan:

fullstack content

Lomakkeen lähettäminen tapahtuu HTTP POST ‑pyyntönä ja osoitteeseen new_note form-tagiin määriteltyjen attribuuttien action ja method ansiosta:

fullstack content

POST-pyynnöstä huolehtiva palvelimen koodi on yksinkertainen (huom: tämä koodi on siis palvelimella eikä näy selaimen lataamassa JavaScript-tiedostossa):

app.post('/new_note', (req, res) => {
  notes.push({
    content: req.body.note,
    date: new Date(),
  })

  return res.redirect('/notes')
})

POST-pyyntöihin liitettävä data lähetetään pyynnön mukana "runkona" eli bodynä. Palvelin saa POST-pyynnön datan pyytämällä sitä pyyntöä vastaavan olion req kentästä req.body.

Tekstikenttään kirjoitettu data on kentässä note, eli palvelin viittaa siihen req.body.note.

Palvelin luo uutta muistiinpanoa vastaavan olion ja laittaa sen muistiinpanot sisältävään taulukkoon nimeltä notes:

notes.push({
  content: req.body.note,
  date: new Date(),
})

Muistiinpano-olioilla on siis kaksi kenttää: varsinaisen sisällön kuvaava content ja luomishetken kertova date.

Palvelin ei talleta muistiinpanoja tietokantaan, joten uudet muistiinpanot katoavat aina, kun palvelin käynnistetään uudelleen.

AJAX

Sovelluksen muistiinpanojen sivu noudattaa vuosituhannen alun tyyliä, ja se "käyttää AJAX:ia" eli on silloisen kehityksen aallonharjalla.

AJAX (Asynchronous JavaScript and XML) on termi, joka lanseerattiin vuoden 2005 helmikuussa kuvaamaan selainten kehityksen mahdollistamaa vallankumouksellista tapaa, jossa HTML-sivulle sisällytetyn JavaScriptin avulla oli mahdollista ladata sivulle lisää sisältöä lataamatta itse sivua uudelleen.

Ennen AJAX:in aikakautta jokainen sivu toimi aiemmassa luvussa olevan perinteisen web-sovelluksen tapaan eli yleisesti ottaen kaikki sivuilla näytettävä data tuli palvelimen generoimasta HTML-koodista.

Muistiinpanojen sivu siis lataa näytettävän datan AJAX:illa. Lomakkeen lähetys sen sijaan tapahtuu perinteisen web-lomakkeen lähetysmekanismin kautta.

Sovelluksen urlit heijastavat vanhaa, huoletonta aikaa. JSON-muotoinen data haetaan urlista https://studies.cs.helsinki.fi/exampleapp/data.json, ja uuden muistiinpanon tiedot lähetetään urliin https://studies.cs.helsinki.fi/exampleapp/new_note. Nykyään näin valittuja urleja ei pidetä ollenkaan hyvinä – ne eivät noudata ns. RESTful-apien yleisesti hyväksyttyjä konventioita. Käsittelemme asiaa tarkemmin osassa 3.

AJAXiksi kutsuttu asia on arkipäiväistynyt ja muuttunut itsestäänselvyydeksi. Koko termi on hiipunut unholaan, ja nuori polvi ei ole sitä edes ikinä kuullut.

Single Page App

Esimerkkisovelluksemme pääsivu toimii perinteisten web-sivujen tapaan: kaikki sovelluslogiikka on palvelimella, ja selain ainoastaan renderöi palvelimen lähettämää HTML-koodia.

Muistiinpanoista huolehtivassa sivussa osa sovelluslogiikasta eli olemassaolevien muistiinpanojen HTML-koodin generointi on siirretty selaimen vastuulle. Selain hoitaa tehtävän suorittamalla palvelimelta lataamansa JavaScript-koodin. Selaimessa suoritettava koodi hakee ensin muistiinpanot palvelimelta JSON-muotoisena raakadatana ja lisää sivulle muistiinpanoja edustavat HTML-elementit DOM-apia hyödyntäen. Viime vuosien aikana on noussut esiin tyyli tehdä web-sovelluksia käyttäen Single-page application (SPA) ‑tyyliä, jossa sovelluksilla ei enää ole esimerkkisovelluksemme tapaan erillisiä, palvelimen sille lähettämiä sivuja, vaan sovellus koostuu ainoastaan yhdestä palvelimen lähettämästä HTML-sivusta, jonka sisältöä manipuloidaan selaimessa suoritettavalla JavaScriptillä.

Sovelluksemme muistiinpanosivu muistuttaa jo hiukan SPA-tyylistä sovellusta. Sitä se ei kuitenkaan vielä ole, sillä vaikka muistiinpanojen renderöintilogiikka on toteutettu selaimessa, käyttää sivu vielä perinteistä mekanismia uusien muistiinpanojen luomiseen, eli se lähettää uuden muistiinpanon tiedot lomakkeen avulla ja palvelin pyytää uudelleenohjauksen avulla selainta lataamaan muistiinpanojen sivun uudelleen.

Osoitteessa https://studies.cs.helsinki.fi/exampleapp/spa on sovelluksen Single Page App ‑versio.

Sovellus näyttää ensivilkaisulta täsmälleen samalta kuin edellinen versio.

HTML-koodi on lähes samanlainen. Erona on ladattava JavaScript-tiedosto (spa.js) ja pieni muutos form-tagin määrittelyssä:

fullstack content

Lomakkeelle ei ole nyt määritelty ollenkaan action- eikä method-attribuutteja, jotka määrittävät, minne ja miten selain lähettää lomakkeelle syötetyn datan.

Avaa nyt Network-välilehti ja tyhjennä se ⦸-symbolilla. Kun luot uuden muistiinpanon, huomaat, että selain lähettää ainoastaan yhden pyynnön palvelimelle:

fullstack content

Pyyntö kohdistuu osoitteeseen new_note_spa, on tyypiltään POST ja se sisältää JSON-muodossa olevan uuden muistiinpanon, johon kuuluu sekä sisältö (content) että aikaleima (date):

{
  content: "Single Page App ei tee turhia sivunlatauksia",
  date: "2019-01-03T15:11:22.123Z"
}

Pyyntöön liitetty headeri Content-Type kertoo palvelimelle, että pyynnön mukana tuleva data on JSON-muotoista:

fullstack content

Ilman headeria palvelin ei osaisi parsia pyynnön mukana tulevaa dataa oikein.

Palvelin vastaa kyselyyn statuskoodilla 201 created. Tällä kertaa palvelin ei pyydä uudelleenohjausta kuten aiemmassa versiossamme. Selain pysyy samalla sivulla, ja muita HTTP-pyyntöjä ei suoriteta.

Ohjelman Single Page App ‑versiossa lomakkeen tietoja ei lähetetä selaimen normaalin lomakkeiden lähetysmekanismin avulla. Lähettämisen hoitaa selaimen lataamassa JavaScript-tiedostossa määritelty koodi. Katsotaan hieman koodia, vaikka yksityiskohdista ei tarvitse nytkään välittää liikaa.

var form = document.getElementById('notes_form')
form.onsubmit = function(e) {
  e.preventDefault()

  var note = {
    content: e.target.elements[0].value,
    date: new Date(),
  }

  notes.push(note)
  e.target.elements[0].value = ''
  redrawNotes()
  sendToServer(note)
}

Komennolla document.getElementById('notes_form') koodi hakee sivulta lomake-elementin ja rekisteröi sille tapahtumankäsittelijän hoitamaan tilanteen, jossa lomake "submitoidaan" eli lähetetään. Tapahtumankäsittelijä kutsuu heti metodia e.preventDefault(), jolla se estää lomakkeen lähetyksen oletusarvoisen toiminnan. Oletusarvoinen toiminta aiheuttaisi lomakkeen lähettämisen ja sivun uudelleen lataamisen, joita emme single page ‑sovelluksissa halua tapahtuvan.

Tämän jälkeen koodi luo muistiinpanon, lisää sen muistiinpanojen listalle komennolla notes.push(note), piirtää ruudun sisällön eli muistiinpanojen listan uudelleen ja lähettää uuden muistiinpanon palvelimelle.

Palvelimelle muistiinpanon lähettävä koodi on seuraava:

var sendToServer = function(note) {
  var xhttpForPost = new XMLHttpRequest()
  // ...

  xhttpForPost.open('POST', '/new_note_spa', true)
  xhttpForPost.setRequestHeader(
    'Content-type', 'application/json'
  )
  xhttpForPost.send(JSON.stringify(note))
)

Koodissa siis määritellään, että kyse on HTTP POST ‑pyynnöstä, että headerin Content-type avulla lähetettävän datan tyyppi on JSON ja että data lähetetään JSON-merkkijonona.

Sovelluksen koodi on osoitteessa https://github.com/mluukkai/example_app. Kannattaa huomata, että sovellus on tarkoitettu ainoastaan kurssin käsitteistöä demonstroivaksi esimerkiksi. Koodi on osin tyyliltään huonoa, ja siitä ei tule ottaa mallia omia sovelluksia tehdessä. Sama koskee käytettyjä urleja, Single Page App ‑tyyliä noudattavan sivun käyttämä uusien muistiinpanojen kohdeosoite new_note_spa ei noudata nykyisin suositeltavia käytäntöjä.

JavaScript-kirjastot

Kurssin esimerkkisovellus on tehty ns. Vanilla JavaScriptillä eli käyttäen pelkkää DOM-apia ja JavaScript-kieltä sivujen rakenteen manipulointiin.

Pelkän JavaScriptin ja DOM-apin käytön sijaan web-ohjelmoinnissa hyödynnetään yleensä kirjastoja, jotka sisältävät DOM-apia helpommin käytettäviä työkaluja sivujen muokkaamiseen. Eräs tällainen kirjasto on edelleenkin hyvin suosittu jQuery.

jQuery on kehitetty aikana, jona web-sivut olivat vielä suurimmaksi osaksi perinteisiä, eli palvelin muodosti HTML-sivuja, joiden toiminnallisuutta rikastettiin selaimessa jQueryllä kirjoitetun JavaScript-koodin avulla. Yksi syy jQueryn suosion taustalla oli niin sanottu cross-browser yhteensopivuus, eli kirjasto toimi selaimesta ja selainvalmistajasta riippumatta samalla tavalla, eikä sitä käytettäessä ollut enää tarvetta kirjoittaa selainversiokohtaisia ratkaisuja. Nykyisin tavallisen jQueryn käyttö ei ole enää yhtä perusteltua kuin aikaisemmin – JavaScript on kehittynyt paljon, ja käytetyimmät selaimet tukevat perustoiminnallisuuksia yleisesti ottaen hyvin.

Single page app ‑tyylin noustua suosioon on ilmestynyt useita jQueryä "modernimpia" tapoja sovellusten kehittämiseen. Ensimmäisen trendiaallon suosikki oli Backbone.js. Googlen kehittämä AngularJS nousi 2012 tapahtuneen julkaisun jälkeen erittäin nopeasti lähes de facto ‑standardin asemaan modernissa web-sovelluskehityksessä.

Angularin suosio kuitenkin romahti siinä vaiheessa, kun Angular-tiimi ilmoitti lokakuussa 2014, että version 1 kehitys lopetetaan ja Angular 2 ei tule olemaan taaksepäin yhteensopiva ykkösversion kanssa. Angular 2 ja uudemmat versiot eivät ole saaneet kovin innostunutta vastaanottoa mutta ovat kohtuullisen laajalti käytettyjä.

Nykyisin, tai oikeastaan jo lähes puolen vuosikymmenen ajan, suosituin tapa toteuttaa web-sovellusten selainpuolen logiikka on Facebookin kehittämä React-kirjasto. Tulemme tutustumaan kurssin aikana Reactiin ja sen kanssa yleisesti käytettyyn Redux-kirjastoon.

Reactin asema näyttää tällä hetkellä vahvalta, mutta JavaScript-maailma ei lepää koskaan. Muutama vuosi sitten hieman uudempi tulokas Vue.js herätti jossain määrin kiinnostusta, mutta ei sekään näytä nousseen uhkaamaan Reactin asemaa.

Full stack ‑websovelluskehitys

Mitä tarkoitetaan kurssin nimellä Full stack ‑web-sovelluskehitys? Full stack on hypenomainen termi – kaikki puhuvat siitä, mutta kukaan ei oikein tiedä, mitä se tarkoittaa, tai ainakaan mitään yhteneväistä määritelmää termille ei ole.

Käytännössä kaikki web-sovellukset sisältävät (ainakin) kaksi "kerrosta". Ylin kerros on lähempänä loppukäyttäjää oleva selain, joka suorittaa JavaScript-koodin ja renderöi sovelluksen HTML:n. Alempi kerros taas on sivuston tiedostot sisältämä palvelin. Palvelimen alapuolella on usein vielä tietokanta. Näin websovelluksen arkkitehtuurin voi ajatella muodostavan pinon, englanniksi stack.

Web-sovelluskehityksen yhteydessä puhutaan usein myös "frontista" (frontend) ja "backistä" (backend). Selain on frontend, ja selaimessa suoritettava JavaScript on frontend-koodia. Palvelimella taas pyörii backend-koodi.

Tämän kurssin yhteydessä full stack ‑sovelluskehitys tarkoittaa sitä, että fokus on kaikissa sovelluksen osissa: niin frontendissä kuin backendissä sekä taustalla olevassa tietokannassa. Myös palvelimen käyttöjärjestelmä ja sen ohjelmistot lasketaan usein osaksi stackia. Niihin emme kuitenkaan tällä kurssilla puutu.

Ohjelmoimme myös palvelinpuolta eli backendia JavaScriptilla käyttäen Node.js-suoritusympäristöä. Näin full stack ‑sovelluskehitys saa vielä uuden ulottuvuuden, kun voimme käyttää samaa ohjelmointikieltä pinon useammassa kerroksessa. Full stack ‑sovelluskehitys ei välttämättä edellytä sitä, että kaikissa "sovelluspinon" kerroksissa on käytössä sama kieli (JavaScript).

Aiemmin on ollut yleisempää, että sovelluskehittäjät ovat erikoistuneet tiettyyn sovelluksen osaan, esim. backendiin. Tekniikat backendissa ja frontendissa ovat saattaneet olla hyvin erilaisia. Full stack ‑trendin myötä on tullut tavanomaiseksi, että sovelluskehittäjä hallitsee riittävästi kaikkia sovelluksen tasoja ja tietokantaa. Usein full stack ‑kehittäjällä on myös oltava riittävä määrä konfiguraatio- ja ylläpito-osaamista, jotta tämä pystyy operoimaan sovellustaan esim. pilvipalveluissa.

JavaScript fatigue

Full stack ‑sovelluskehitys on monella tapaa haastavaa. Asioita tapahtuu monessa paikassa ja mm. debuggaaminen on oleellisesti normaalia työpöytäsovellusta hankalampaa. JavaScript ei toimi aina niin kuin sen olettaisi toimivan (verrattuna moniin muihin kieliin), ja sen suoritusympäristöjen asynkroninen toimintamalli aiheuttaa monenlaisia haasteita. Verkon yli tapahtuva kommunikointi edellyttää HTTP-protokollan tuntemusta. On tunnettava myös tietokantoja ja hallittava palvelinten konfigurointia ja ylläpitoa. Toivottavaa olisi myös hallita riittävästi CSS:ää, jotta sovellukset saataisiin edes siedettävän näköisiksi.

Oman haasteensa tuo vielä se, että JavaScript-maailma etenee koko ajan todella kovaa vauhtia eteenpäin. Kirjastot, työkalut ja itse kielikin ovat jatkuvan kehityksen alla. Osa alkaa kyllästyä nopeaan kehitykseen, ja sitä kuvaamaan on lanseerattu termi JavaScript fatigue eli JavaScript-väsymys.

JavaScript-väsymys tulee varmasti iskemään myös tällä kurssilla. Onneksi nykyään on olemassa muutamia tapoja loiventaa oppimiskäyrää, ja voimme aloittaa keskittymällä konfiguraation sijaan koodaamiseen. Konfiguraatioita ei voi välttää, mutta seuraavat viikot voimme edetä iloisin mielin vailla pahimpia konfiguraatiohelvettejä.