Creative Commons -lisenssi

Yleistä

Kurssilla tutustutaan Javascriptilla tapahtuvaan moderniin websovelluskehitykseen. Pääpaino on React-kirjaston avulla toteutettavissa single page -sovelluksissa, ja niitä tukevissa Node.js:llä toteutetuissa REST-rajapinnoissa.

Kurssilla käsitellään myös sovellusten testaamista, konfigurointia ja suoritusympäristöjen hallintaa sekä NoSQL-tietokantoja.

Oletetut esitiedot

Osallistujilta edellytetään vahvaa ohjelmointirutiinia, web-ohjelmoinnin ja tietokantojen perustuntemusta, git-versionhallintajärjestelmän peruskäytön hallintaa, kykyä pitkäjänteiseen työskentelyyn sekä valmiutta omatoimiseen tiedonhakuun ja ongelmanratkaisuun.

Osallistuminen ei kuitenkaan edellytä kurssilla käsiteltävien tekniikoiden tai Javascript-kielen hallintaa.

Kurssimateriaali

Kurssimateriaali on tarkoitettu luettavaksi osa kerrallaan “alusta loppuun”. Materiaalin seassa on tehtäviä, jotka on sijoiteltu siten, että kunkin tehtävän tekemiseen pitäsi olla riittävät tekniset valmiudet sitä edeltävässä materiaalissa. Voit siis tehdä tehtäviä sitä mukaan kun niitä tulee materiaalissa vastaan. Voi myös olla, että koko osan tehtävät kannattaa tehdä vasta luettuaan ensin osan alusta loppuun kertaalleen. Useissa osissa tehtävät ovat samaa ohjelmaa laajentavia pienistä osista koostuvia kokonaisuuksia. Muutamia tehtävien ohjelmia kehitetään eteenpäin useamman osan aikana.

Materiaali perustuu muutamien osasta osaan vaihtuvien esimerkkiohjelmien asteittaiseen laajentamiseen. Materiaali toiminee parhaiten, jos kirjoitat samalla koodin myös itse ja teet koodiin myös pieniä modifikaatioita. Materiaalin käyttämien ohjelmien koodin eri vaiheiden tilanteita on tallennettu GitHubiin.

Suoritustapa

Kurssi koostuu kahdeksasta osasta, joista ensimmäinen on historiallisista syistä numero nolla. Kunkin osan tekemiseen kannattaa varata aikaa noin 5-25 tuntia.

Materiaalissa osasta n osaan n+1 eteneminen ei ole mielekästä ennen kuin riittävä osaaminen osan n asioista on saavutettu. Kurssilla sovelletaankin pedagogisin termein tavoiteoppimista, engl. mastery learning ja on tarkoitus, että etenet seuraavaan osaan vasta, kun riittävä määrä edellisen osan tehtävistä on tehty.

Oletuksena on, että teet kunkin osan tehtävistä ainakin ne jotka eivät ole merkattu tähdellä. Myös tähdellä merkatut tehtävät vaikuttavat arvosteluun, mutta niiden tekemättäjättäminen ei aiheuta liian suuria esteitä seuraavan osan (tähdellä merkkaamattomien) tehtävien tekemiseen.

Etenemisnopeus kurssilla on vapaa, tehtäviä voi palauttaa 15.12. klo 23:59 asti.

Tämän kurssin eri osiin jo tehtyjen palautusten ajankäyttöstatistiikan näet tehtävien palautussivulta.

Arvosteluperusteet

Kurssi voidaan suorittaa joko 3 tai 5-7 opintopisteen laajuisena.

Laajuus ja arvosana määräytyy kaikkien tehtyjen tehtävien perusteella, myös tähdellä merkityt tehtävät siis vaikuttavat arvosanaan. Kurssin lopussa on koe, joka on suoritettava hyväksytysti. Koe ei kuitenkaan vaikuta arvosanaan.

Jos haluat kurssilta suorituksen, tallenna opiskelijanumerosi palautussovelluksen näkymään my submissions.

Kolmen opintopisteen suoritus

Kolmen opintopisteen suorituksen edellytyksenä on osien 0-3 kaikkien tähdellä merkitsemättömien tehtävien tekeminen sekä hyväksytty suoritus 3 opintopisteen laajuuden kokeesta.

Koe suoritetaan Moodlessa. Pääset Moodleen Avoimen yliopiston kurssisivun kautta. Kokeen voi suorittaa 1.5. alkaen. Kokeen viimeinen suorituspäivä on 15.12. Voit osallistua kokeeseen vasta siinä vaiheessa kun olet tehnyt kaikki kolmen opintopisteen suorituksen edellyttämät tehtävät.

Kokeen tuloksen näet palautussovelluksen välilehdeltä my submissions viimeistään 4 viikkoa kokeen suorittamisen jälkeen (heinäkuu saatta aiheuttaa kokeen tarkastamiseen pidemmän viiveen). Muista tallettaa opiskelijanumerosi palautussovellukseen.

Suoritus rekisteröidään arvosanalla hyväksytty.

Voit halutessasi laajentaa 3 op suorituksen myöhemmin laajempaan 5-7 op suoritukseen. Jos tähtäät suoraan laajempaan suoritukseen, 3 op:n koetta ei kannata suorittaa.

5-7 opintopisteen suoritus

Laajan suorituksen opintopistemäärä ja arvosana määräytyy kaikkien tehtyjen tehtävien (myös tähdellä merkittyjen) perusteella.

Arvosana/opintopisterajat:

tehtävää   arvosana  op
75 1/5 5
85 2/5 5
95 3/5 5
105 4/5 5
115 5/5 5
128 5/5 6
140 5/5 7

Myös 5-7 op:n suorituksen edellytyksenä on hyväksytysti suoritettu koe. Koe suoritetaan Moodlessa. Pääset Moodleen Avoimen yliopiston kurssisivun kautta. Kokeen voi suorittaa 1.5. alkaen. Kokeen viimeinen suorituspäivä on 15.12.

Voit osallistua kokeeseen vasta siinä vaiheessa kun olet tehnyt riittävästi tehtäviä 5 opintopisteen suoritukseen. Käytännössä kokeeseen ei kuitenkaan kannata osallistua heti kun riittävä tehtäväpistemäärä on suoritettu. Koe on sisällöltään sama riippumatta onko suorituksesi laajuus 5-7 opintopistettä. Koe ei vaikuta kurssilta saamaasi arvosanaan.

Kokeen tuloksen näet palautussovelluksen välilehdeltä my submissions viimeistään 4 viikkoa kokeen suorittamisen jälkeen (heinäkuu saatta aiheuttaa kokeen tarkastamiseen pidemmän viiveen). Muista tallettaa opiskelijanumerosi palautussovellukseen.

Alkutoimet

Tällä kurssilla suositellaan Chrome-selaimen käyttöä, sillä se tarjoaa parhaan välineistön web-sovelluskehitystä ajatellen.

Kurssin tehtävät palautetaan GitHubiin, joten Git tulee olla asennettuna ja sitä on syytä osata käyttää.

Asenna myös joku järkevä webkoodausta tukeva tekstieditori, enemmän kuin suositeltava valinta on Visual studio code. Myös Atom on tarkoitukseen toimiva.

Älä koodaa nanolla, Notepadilla tai Geditillä. NetBeanskaan ei ole omimmillaan Web-koodauksessa ja se on myös turhan raskas verrattuna esim. Visual Studio Codeen.

Asenna koneeseesi heti myös Node.js. Materiaali on tehty versiolla 8.6, älä asenna mitään sitä vanhempaa versiota.

Asennusohjeita on koottu tänne.

Noden myötä koneelle asentuu myös Node package manager npm, jota tulemme tarvitsemaan kurssin aikana aktiivisesti. Tuoreen noden kera asentuu myös npx, jota tarvitaan myös muutaman kerran.

Typoja materiaalissa

Kurssilla on paljon materiaalia ja se on olosuhteiden pakosta kirjoitettu todella nopeasti. Materiaalissa on betatestauksesta ja oikoluvuista huolimatta kirjoitusvirheitä. Jos löydät kirjoitusvirheen tai joku asia on sanottu epäselvästi tai kielioppisääntöjen vastaisesti, tee pull request repositoriossa https://github.com/fullstackopen/fullstackopen.github.io olevaan kurssimateriaaliin.

Pull requestin tekeminen on helppoa. Esim. jos tällä sivulla on typo, mene sivun “lähdekoodiin” https://github.com/fullstackopen/fullstackopen.github.io/blob/master/osa0.md klikkaa kynän kuvaa oikealta, tee muutokset ja klikkaa muutama kerta “vihreää” Ohtu-kurssin ohjeen mukaan.

Osan 0 oppimistavoitteet

  • Web-sovellusten toiminnan perusteet
    • HTML:n perusteet
    • HTTP-protokolla: metodit GET ja POST, statuskoodit, headerit
    • palvelimella suoritettavan koodin rooli
    • selaimessa suoritettavan Javascriptin rooli
    • JSON-muotoinen data
    • DOM API
    • sivujen ulkoasun muotoilun periaate CSS:llä
    • single page app -periaate
    • Chrome developer konsolin peruskäyttö

Web-sovelluksen toimintaperiaatteita

Ennen kuin aloitamme ohjelmoinnin, käymme läpi web-sovellusten toimintaperiaatteita tarkastelemalla osoitteessa https://fullstack-exampleapp.herokuapp.com/ olevaa esimerkkisovellusta. Huom. sovelluksen toinen versio on osoitteessa https://fullstack-example.now.sh, voit käyttää kumpaa vaan.

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.

Käytä nyt ja koko ajan tämän kurssin aikana Chrome-selainta.

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

Web-sovelluskehityksen sääntö numero yksi

Pidä selaimen developer-konsoli koko ajan auki

Konsoli avautuu macilla painamalla yhtä aikaa alt cmd ja i.

Ennen kun 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:

Varmista, että välilehti Network on avattuna ja aktivoi valinta Disable cache kuten kuvassa on tehty. Myös Preserve logs on joskus hyödyllinen, 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-tabi kertoo miten selain ja palvelin kommunikoivat.

Kun nyt reloadaat sivun, kertoo konsoli, että tapahtuu kaksi asiaa

  • selain hakee web-palvelimelta sivun fullstack-exampleapp.herokuapp.com/ sisällön
  • ja lataa kuvan kuva.png

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

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

Ylimmästä osasta General selviää, että selain teki GET-metodilla pyynnön osoitteeseen https://fullstack-exampleapp.herokuapp.com/ ja että pyyntö oli onnistunut, sillä pyyntöön saatiin vastaus, jonka Status code on 200.

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

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 “websivun tavoin”.

Välilehti Preview 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:

Sivu sisältää div-elementin, jonka sisällä on otsikko sekä tieto luotujen muistiinpanojen määrästä, linkki sivulle muistiinpanot 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:

eli pyyntö on tehty osoitteeseen https://fullstack-exampleapp.herokuapp.com/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.

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ä sen hetkisellä 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. kurssin Web-palvelinohjelmointi tapaan Java Springillä tai tietokantasovelluksessa PHP:llä tai Ruby on Railsilla. Esimerkissä on käytetty Node.js:n Express-sovelluskehystä. 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, selain tekee 4 HTTP-pyyntöä:

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

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 itseasiassa 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://fullstack-exampleapp.herokuapp.com/data.json suoraan selaimella:

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:

Ylläoleva muistiinpanojen sivun Javascript-koodi siis lataa muistiinpanot sisältävän JSON-muotoisen datan ja muodostaa datan avulla selaimeen “bulletlistan” 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:

Painamalla rivin alussa olevaa kolmiota saat laajennettua konsolissa olevan rivin:

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 kuluessa.

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ö, eli 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

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.

Allaoleva 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)

Toisena esimerkkinä HTML-dokumentin

DOM:a havainnollistava kuva Wikipedian sivulta:

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ä:

Tiedot saa esiin komennolla:

document

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:

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

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

uusi = document.createElement('li')
uusi.textContent = 'Sivun manipulointi konsolista on helppoa'

liitetään li-elementti listalle:

lista.appendChild(uusi)

Vaikka selaimen näyttämä sivu päivittyy, ei muutos ole lopullinen. Jos sivu uudelleenladataan, katoaa uusi muistiinpano, sillä muutos ei mennyt palvelimelle asti. Selaimen lataama Javascript luo muistiinpanojen listan aina palvelimelta osoitteesta https://fullstack-exampleapp.herokuapp.com/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:

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ä:

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

Lomake ja HTTP POST

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

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ää:

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

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, minkä avulla palvelin kehottaa selainta tekemään automaattisesti uuden HTTP GET -pyynnön headerin Location kertomaan 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:

Jos käytät normaalia Chrome-selainta, ei konsoli ehkä näytä lähetettävää dataa. Kyseessä on eräissä Chromen versioissa oleva bugi. Bugi on korjattu Chromen uusimpaan versioon.

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

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 avaimen note alla, eli palvelin viittaa siihen req.body.note.

Palvelin luo uutta muistiinpanoa vastaavan olion ja laittaa sen muistiinpanot sisältävään taulukkoon nimeltään 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 Herokun uudelleenkäynnistäessä palvelun.

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, missä 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 oleellisesti ottaen kaikki sivuilla näytettävä data tuli palvelimen generoimassa HTML-koodissa.

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://fullstack-exampleapp.herokuapp.com/data.json ja uuden muistiinpanon tiedot lähetetään urliin https://fullstack-exampleapp.herokuapp.com/new_note. Nykyään näin valittuja urleja ei pidettäisi 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. Selaimella suoritettava koodi hakee ensin muistiinpanot palvelimelta JSON-muotoisena raakadatana ja lisää sivulle muistiinpanoja edustavat HTML-elementit DOM-apia hyödyntäen.

Viimeisten vuosien aikana on noussut esiin tyyli tehdä web-sovellukset käyttäen Single-page application (SPA) -tyyliä, missä sovelluksille ei enää tehdä 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ä mekanisimia uusien muistiinpanojen luomiseen, eli se lähettää uuden muistiinpanon tiedot lomakkeen avulla ja palvelin pyytää uudelleenohjauksen avulla selainta lataamaan muistiinpanojen sivun uudelleen.

Osoitteesta https://fullstack-exampleapp.herokuapp.com/spa löytyy 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ä:

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

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 sivun latauksia",
  date: "2017-12-11T10:51:29.025Z"
}

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

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

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, missä 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, sitä emme single page -sovelluksissa halua tapahtuvan.

Tämän jälkeen se 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 seuraavassa:

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ä, määritellään headerin Content-type avulla lähetettävän datan tyypiksi JSON, ja lähetetään data JSON-merkkijonona.

Sovelluksen koodi on nähtävissä 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älläinen kirjasto on edelleenkin hyvin suosittu JQuery.

JQuery on kehitetty aikana, jolloin 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 selain valmistajasta riippumatta samalla tavalla, eikä sitä käyttäessä ollut enää tarvetta kirjoittaa selainversiospesifisiä ratkaisuja. Nykyisin perus JQueryn käyttö ei ole enää yhtä perusteltua kuin aikaisemmin, sillä vanillaJS on kehittynyt paljon ja käytetyimmät selaimet tukevat yleisesti ottaen hyvin perustoiminnallisuuksia.

Single page app -tyylin noustua suosioon on ilmestynyt useita JQueryä “modernimpia” tapoja sovellusten kehittämiseen. Ensimmäisen aallon suosikki oli BackboneJS. 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 tuki 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.

Nykyisin 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. Viime aikoina kiinnostusta on alkanut herättää mm. uudempi tulokas VueJS.

Full stack -websovelluskehitys

Mitä tarkoitetaan kurssin nimellä Full stack -websovelluskehitys? 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 websovellukset sisältävät (ainakin) kaksi “kerrosta”, ylempänä, eli lähempänä loppukäyttäjää olevan selaimen ja alla olevan palvelimen. Palvelimen alapuolella on usein vielä tietokanta. Näin websovelluksen arkkitehtuurin voi ajatella muodostavan pinon, englanniksi stack.

Websovelluskehityksen 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 kontekstissa full stack -sovelluskehitys tarkoittaa sitä, että fokus on kaikissa sovelluksen osissa, niin frontendissä kuin backendissä sekä taustalla olevassa tietokannassa.

Ohjelmoimme myös palvelinpuolta, eli backendia Javascriptilla, käyttäen Node.js-suoritusympäristöä. Näin full stack -sovelluskehitys saa vielä uuden ulottuvuuden, käytämme samaa kieltä pinon kaikissa osissa. Full stack -sovelluskehitys ei välttämättä edellytä sitä, että kaikissa “sovelluspinon” kerroksissa on käytössä sama kieli (Javascript). Termi “full stack” on kuitenkin (todennäköisesti) lanseerattu vasta sen jälkeen kun Node.js mahdollisti Javascriptin käyttämisen kaikkialla.

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än on myös omattava riittävä määrä konfiguraatio- ja ylläpito-osaamista, jotta kehittäjä 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. Hyvä 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ä.

Tehtäviä web-sovelluksen perusteista

Tee nyt tehtävät 0.1-0.6. Niiden jälkeen olet valmiina siirtymään osaan 1 ja itse asiaan eli Web-sovellusten toteuttamiseen.