b
Web-sovelluksen toimintaperiaatteita
Ennen kuin aloitamme ohjelmoinnin, käymme läpi web-sovellusten toimintaperiaatteita tarkastelemalla osoitteessa https://fullstack-exampleapp.herokuapp.com/ olevaa esimerkkisovellusta. Huomaa että sovelluksen toinen versio löytyy osoitteesta https://fullstack-example.now.sh ja voit käyttää kumpaa tahansa. Osoitteessa herokuapp.com olevat sovellukset saattavat lakata toimimasta kuukausien viimeisinä päivinä ilmaisen prosessointiajan loppuessa.
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. Windowsilla konsolin saa auki painamalla F12 tai yhtä aikaa ctrl shift 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-välilehti kertoo miten selain ja palvelin kommunikoivat.
Kun päivität sivun (eli painat F5-näppäintä tai selaimessa olevaa symbolia ↺), kertoo konsoli, että tapahtuu kaksi asiaa:
- selain hakee web-palvelimelta sivun https://fullstack-exampleapp.herokuapp.com/ sisällön
- ja lataa kuvan kuva.png
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:
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 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:
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.
Sivun https://fullstack-exampleapp.herokuapp.com/ avaaminen selaimessa saa siis aikaan allaolevan sekvenssikaavion kuvaaman tapahtumasarjan:
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ä 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 käytetyllä Python Flaskillä 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 eli klikkaat linkkiä notes, 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ö. 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)
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):
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)
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.
JavaScriptia sisältävän sivun lataaminen - kertaus
Kerrataan vielä mitä tapahtuu kun selaimessa avataan sivu https://fullstack-exampleapp.herokuapp.com/notes
- 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 suorittaa hakemaansa JavaScript-koodia, joka tekee HTTP GET -pyynnön muistiinpanot json-muotoisena raakadatana palauttavaan osoitteeseen https://fullstack-exampleapp.herokuapp.com/data.json
- datan saapuessa selain suorittaa tapahtumankäsittelijän, joka renderöi DOM-apia hyväksikäyttäen muistiinpanot ruudulle
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:
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 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ää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ä 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.
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ä:
Lomakkeelle ei ole nyt määritelty ollenkaan action- eikä method-attribuutteja, jotka määrää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:
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:
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, 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 on 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 selainvalmistajasta riippumatta samalla tavalla, eikä sitä käyttäessä ollut enää tarvetta kirjoittaa selainversiospesifisiä ratkaisuja. Nykyisin tavallisen 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 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.
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 Vue.js.
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. 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ä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ä.