Osa 7

Selainohjelmistot ja JavaScript

Olemme tähän mennessä tutustuneet pikaisesti sekä HTML:ään että CSS:ään. Siinä missä HTML on kieli rakenteen ja sisällön määrittelyyn ja CSS on kieli ulkoasun ja asettelun määrittelyyn, JavaScript on ohjelmointikieli, jolla voi määritellä sivuille dynaamista toiminnallisutta.

Tutustutaan tässä lyhyesti selainpuolen toiminnallisuuden toteuttamiseen JavaScriptillä.


JavaScriptin tuominen projektiin

JavaScript-koodin voi tuoda projektiin kirjoittamalla koodin suoraan sivulle tai lataamalla koodin erillisestä tiedostosta. Koodin kirjoittaminen suoraan sivulle tapahtuu lisäämällä koodi script-elementin sisään.

Alla olevassa esimerkissä määritellään sivu, jonka lataaminen näyttää käyttäjälle "hei maailma"-tekstin sisältävän ponnahdusikkunan.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Otsikko</title>
    </head>
    <body>
        <script>
            alert("hei maailma")
        </script>
    </body>
</html>

Lähdekoodin voi myös ladata erillisestä tiedostosta. Tällöinkin käytetään script-elementtiä, mutta elementille määritellään src-attribuutti, jonka arvo kertoo lähdekooditiedoston sijainnin.

Spring-projekteissa JavaScript-tiedostot lisätään usein kansion src/main/resources/public/javascript/ alle. JavaScript-tiedoston pääte on .js. Kansiossa public olevat tiedostot siirtyvät suoraan näkyville web-maailmaan, joten niitä ei tarvitse käsitellä erikseen esimerkiksi Thymeleaf-moottorin toimesta.

Jos lähdekoodi on kansiossa javascript olevassa tiedostossa code.js, käytetään script-elementtiä seuraavasti: <script th:src="@{/javascript/code.js}"></script>.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Otsikko</title>
    </head>
    <body>
        <script th:src="@{/javascript/code.js}"></script>
    </body>
</html>

Elementin script attribuutti th:src kertoo Thymeleafille, että lähdekooditiedoston sijanti tulee suhteuttaa sovelluksen sijaintiin. Näin sovellus voi sijaita myös muualla kuin palvelimen juuriosoitteessa.

Yleinen käytänne JavaScript-lähdekoodien sivulle lisäämiseen on lisätä ne sivun loppuun juuri ennen body-elementin sulkemista. Tämä johtuu siitä, että selain hakee JavaScript-tiedoston sisällön kun selain kohtaa tiedoston määrittelyn HTML-dokumentissa. Tällöin muut toiminnot voivat joutua odottamaan tiedoston latausta ja koodin suorittamista.

Mikäli lähdekooditiedosto ladataan vasta sivun lopussa, käyttäjälle näytetään sivun sisältöä jo ennen JavaScript-lähdekoodin latautumista, sillä selaimet usein näyttävät sivua käyttäjälle sitä mukaa kun se latautuu. Tällä voidaan luoda tunne nopeammin reagoivista ja latautuvista sivuista.

Funktiot

JavaScriptissä funktiot määritellään avainsanalla function, jota seuraa funktion nimi, sulut ja sulkujen sisään mahdollisesti määriteltävät parametrit. Funktion runko aloitetaan ja lopetetaan aaltosuluilla. Alla on määriteltynä funktio sayHello, jonka kutsuminen näyttää käyttäjälle ponnahdusikkunan, joka sisältää tekstin "hello there".

function sayHello() {
    alert("hello there")
}

JavaScript on dynaamisesti tyypitetty kieli, eli parametrien (ja muuttujien) tyyppi päätellään ajonaikaisesti. Parametreja määriteltäessä ei siis kerrota niiden tyyppiä. Alla määritellään funktio sayHelloTo, jolle annetaan parametrina tervehdittävän henkilön nimi. Merkkijonojen yhdistäminen toimii JavaScriptissä samalla tavalla kuin Javassa eli +-merkillä.

function sayHelloTo(name) {
    alert("hello " + name)
}

Kuten huomaat, puolipisteiden käyttö lausekkeen tai rivin jälkeen ei ole pakollista. Puolipisteitä saa kuitenkin käyttää mikäli haluaa.

Tapahtumien kuuntelu

HTML-elementeille voi määritellä tapahtumienkäsittelijöitä. Elementin attribuutille onclick voidaan antaa parametrina elementin klikkauksen yhteydessä suoritettavan funktion kutsu.

Alla olevassa esimerkissä oletetaan, että tiedostossa code.js on aiemmin määritelty funktio sayHelloTo. Esimerkissä lisätään nappiin (button-elementti) tapahtumankuuntelija — kun nappia klikataan, suoritetaan funktiokutsu sayHelloTo('tyyppi').

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" >
        <title>Otsikko</title>
    </head>
    <body>

        <button onclick="sayHelloTo('tyyppi')">Paina tästä</button>
        <script th:src="@{javascript/code.js}" defer></script>
    </body>
</html>

Esimerkki on alla tarkasteltavana. JSFiddle-palvelussa HTML-koodissa ei erikseen näy JavaScript-koodin käyttöönottoa.

Muuttujat ja funktioiden parametrit

JavaScriptissä muuttuja määritellään avainsanalla var, jota seuraa muuttujan nimi sekä arvon asettaminen. Muuttujat ovat JavaScriptissä dynaamisesti tyypitettyjä, joten muuttujien tyypiä ei määritellä erikseen — muuttujan tyyppi päätellään ajonaikaisesti. Alla esitellään kaksi muuttujaa, joista ensimmäisen arvoksi asetetaan luku ja toisen arvoksi asetetaan merkkijono.

var luku = 3
var merkkijono = "heippa"

JavaScriptissä puolipisteiden käyttö on mahdollista. Yllä niitä ei ole käytetty, mutta ohjelman voisi halutessaan kirjoittaa myös seuraavalla tavalla.

var luku = 3;
var merkkijono = "heippa";

Kuten kaikkien muuttujien arvojen tyypit, myös funktioiden parametrien arvojen tyypit ovat dynaamisia. Aiemmin määriteltyä funktiota sayHelloTo voi kutsua antamalla funktiolle parametrina minkä tahansa muuttujan.

var luku = 3
var merkkijono = "heippa"

sayHelloTo(luku)
sayHelloTo(heippa)

Yllä oleva ohjelma näyttäisi käyttäjälle ensin ponnahdusikkunan, joka sisältää tekstin "hello 3". Tämän jälkeen näytetään ponnahdusikkuma, joka sisältää tekstin "hello heippa".

Elementtien sisällön hakeminen ja muokkaaminen

Sivulla oleviin elementteihin pääsee käsiksi komennolla document.getElementById("tunnus") eli "anna dokumentista se elementti, jonka id:n arvo on 'tunnus'". Tässä document viittaa sivua kuvaavaan Document Object Modeliin, eli DOMiin, joka sisältää sekä tiedon sivun rakenteesta että mahdollisuudet sen muokkaamiseen.

Tarkastellaan seuraavaa HTML-dokumenttia, jossa on tekstikenttä ja nappi.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" >
        <title>Otsikko</title>
    </head>
    <body>
        <input type="text" id="tekstikentta"/>
        <script th:src="@{javascript/code.js}" defer></script>
    </body>
</html>

Mikäli haluamme päästä käsiksi elementtiin, jonka tunnus on "tekstikentta", käytämme komentoa document.getElementById("tekstikentta"). Syötteitä sisältävien elementtien kuten input arvo (eli teksti) määritellään attribuutilla value.

Luodaan funktio, joka asettaa tekstikenttään uuden arvon — tämä tapahtuu value-attribuutin arvoa muokkaamalla.

function asetaArvo() {
    document.getElementById("tekstikentta").value = "Heippa!"
}

Kun yllä oleva funktio asetaArvoon määritelty, sivulle voi luoda napin, jonka painaminen kutsuu yllä määriteltyä funktiota.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" >
        <title>Otsikko</title>
    </head>
    <body>
        <input type="text" id="tekstikentta"/>
        <button onclick="asetaArvo()">Paina tästä</button>
        <script th:src="@{javascript/code.js}" defer></script>
    </body>
</html>

Napin painaminen asettaa tekstikentän arvoksi tekstin "Heippa!". Alla sama esitettynä JSFiddlessä.

Tekstikentän arvon voi tulostaa tai sen voi asettaa vaikkapa muuttujan arvoksi. Alla olevassa esimerkissä haetaan tekstikentän arvo, joka näytetään käyttäjälle ponnahdusikkunassa.

Ohjelmassa voidaan käsitellä luonnollisesti useampia tekstikenttiä sekä muita elementtejä. Alla olevassa esimerkissä dokumentissa on kaksi tekstikenttää, joiden arvot vaihdetaan päittäin nappia painettaessa.

Arvon asettaminen osaksi tekstiä

Tekstikentän arvo asetetaan value-attribuutin arvoa muokkaamalla. Kaikilla elementeillä ei kuitenkaan ole value-attribuuttia, vaan joillain näytetään niiden elementin sisällä oleva arvo tai.

Elementin sisälle asetetaan arvo muuttujaan liittyvällä attribuutilla innerHTML.

Alla olevassa esimerkissä sivulla on tekstielementti p, jossa ei ole lainkaan sisältöä. Jos käyttäjä syöttää tekstikenttään tekstiä ja painaa nappia, asetetaan tekstikentässä oleva teksti tekstielementin arvoksi.

Myös tekstielementin sisälle voi asettaa arvoja. Tämä onnistuu näppärästi span-elementin avulla.

Esimerkki: Laskin

Luodaan laskin. Laskimella on kaksi toiminnallisuutta: pluslasku ja kertolasku. Luodaan ensin laskimelle JavaScriptkoodi, joka on tiedostossa laskin.js. JavaScript-koodissa oletetaan, että on olemassa input-tyyppiset elementit tunnuksilla "eka" ja "toka" sekä span-tyyppinen elementti tunnuksella "tulos". Funktiossa plus haetaan elementtien "eka" ja "toka" arvot, ja asetetaan pluslaskun summa elementin "tulos" arvoksi. Kertolaskussa tehdään lähes sama, mutta tulokseen asetetaan kertolaskun tulos. Koodissa on myös apufunktio, jota käytetään sekä arvojen hakemiseen annetuilla tunnuksilla merkityistä kentistä että näiden haettujen arvojen muuttamiseen numeroiksi.

function haeNumero(tunnus) {
    return parseInt(document.getElementById(tunnus).value)
}

function asetaTulos(tulos) {
    document.getElementById("tulos").innerHTML = tulos
}

function plus() {
    asetaTulos(haeNumero("eka") + haeNumero("toka"))
}

function kerto() {
    asetaTulos(haeNumero("eka") * haeNumero("toka"))
}

Laskimen käyttämä HTML-dokumentti näyttää seuraavalta:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" >
        <title>Laskin</title>
    </head>
    <body>
        <header>
            <h1>Plus- ja Kertolaskin</h1>
        </header>

        <section>
            <p>
                <input type="text" id="eka" value="0" />
                <input type="text" id="toka" value="0" />
            </p>

            <p>
                <input type="button" value="+" onclick="plus()" />
                <input type="button" value="*" onclick="kerto()" />
            </p>


            <p>Laskimen antama vastaus: <span id="tulos"></span></p>
        </section>

        <script src="javascript/laskin.js"></script>
    </body>
</html>

Kokonaisuudessaan laskin näyttää seuraavalta:

Loading

Elementtien valinta ja kokoelmat

Käytämme getElementById-kutsua tietyn elementin hakemiseen. Tässä oletetaan, että id on aina uniikki, eli kahdella elementillä ei ole samaa id-attribuutin arvoa. Mikäli ohjelmoija haluaa hakea useamman elementin samalla kutsulla, löytyy siihenkin välineet. Kaikki sivun elementit voi hakea esimerkiksi getElementsByTagName("*")-kutsulla, joka palauttaisi kaikki sivun elementit. Tämä on kuitenkin hieman kömpelö.

W3C DOM-määrittely sisältää myös ohjelmointirajapinnan elementtien läpikäyntiin. Selectors API sisältää mm. querySelector-kutsun, joka tarjoaa kyselytoiminnallisuuden elementtien hakemiseen — toiminnallisuus muistuttaa.

Selector APIn tarjoamien querySelector (yksittäisen osuman haku) ja querySelectorAll (kaikkien osumien haku) -komentojen avulla kyselyn rajoittaminen vain esimerkiksi nav-elementissä oleviin a-elementteihin on helppoa.

var linkit = document.querySelectorAll("nav a")
// linkit-muuttuja sisältää nyt kaikki
// a-elementit, jotka ovat nav-elementin sisällä

Vastaavasti header-elementin sisällä olevat linkit voi hakea seuraavanlaisella kyselyllä.

var linkit = document.querySelectorAll("header a")
// linkit-muuttuja sisältää nyt kaikki
// a-elementit, jotka ovat header-elementin sisällä

Yllä linkit on kokoelma tietoa. Kokoelmien läpikäynti onnistuu perinteisellä for-toistolauseella. JavaScriptissä sen muoto on seuraava.

var linkit = document.querySelectorAll("header a")
// linkit-muuttuja sisältää nyt kaikki
// a-elementit, jotka ovat header-elementin sisällä

for (var i = 0; i < linkit.length; i++) {
    alert(i + ": " + linkit[i]);
}

Yllä oleva näyttää kullekin linkille ponnahdusikkunan. Ponnahdusikkunassa on linkin indeksi kokoelmassa sekä linkkiin liittyvän elementin tiedot.

Elementtien lisääminen

HTML-dokumenttiin lisätään uusia elementtejä document-olion createElement-metodilla. Alla luodaan p-elementti sekä siihen liitettävä tekstisolmu (kutsu createTextNode), joka asetetaan tekstielementin lapseksi.

var tekstiElementti = document.createElement("p")
var tekstiSolmu = document.createTextNode("o-hai")

tekstiElementti.appendChild(tekstiSolmu)

Ylläoleva esimerkki ei luonnollisesti muuta HTML-dokumentin rakennetta sillä uutta elementtiä ei lisätä osaksi HTML-dokumenttia. Olemassaoleviin elementteihin voidaan lisätä sisältöä elementin appendChild-metodilla. Alla olevan tekstialue sisältää article-elementin, jonka tunnus on osio. Voimme lisätä siihen elementtejä elementin appendChild-metodilla.

var tekstiElementti = document.createElement("p")
var tekstiSolmu = document.createTextNode("o-noes!")

tekstiElementti.appendChild(tekstiSolmu)

var alue = document.getElementById("osio")
alue.appendChild(tekstiElementti)

Artikkelielementin sekä sen sisältämien tekstielementtien lisääminen onnistuu vastaavasti. Alla olevassa esimerkissä käytössämme on seuraavanlainen section-elementti.

<!-- .. dokumentin alkuosa .. -->
<section id="osio"></section>
<!-- .. dokumentin loppuosa .. -->

Uusien artikkelien lisääminen onnistuu helposti aiemmin näkemällämme createElement-metodilla.

var artikkeli = document.createElement("article")

var teksti1 = document.createElement("p")
teksti1.appendChild(document.createTextNode("Lorem ipsum... 1"))
artikkeli.appendChild(teksti1)

var teksti2 = document.createElement("p")
teksti2.appendChild(document.createTextNode("Lorem ipsum... 2"))
artikkeli.appendChild(teksti2)

document.getElementById("osio").appendChild(artikkeli)

Alla olevassa esimerkissä napin painaminen johtaa uuden elementin lisäämiseen. Mukana on myös laskuri, joka pitää kirjaa elementtien lukumäärästä.

Kommunikointi palvelimen kanssa

Kommunikointin palvelimen kanssa tapahtuu JavaScriptin XMLHttpRequest-olion avulla. Oliolla on kaksi tärkeää metodia:

  • open, jolle määritellään pyynnön tyyppi (GET/POST) ja pyynnön osoite.

  • send, jota käytetään parametrittomana GET-pyynnön yhteydessä — POST-pyynnön yhteydessä metodille annetaan parametrina palvelimelle lähetettävä data.

Tämän lisäksi olion muuttujaan onreadystatechange määritellään funktio, jolla käsitellään palvelimelta saatu data. Funktiossa tarkastellaan pyynnön tilakoodia readyState sekä HTTP-statuskoodia status — mikäli ensimmäisen arvo on 4 ja toisen arvo on 200, on pyyntö onnistunut ja vastausta voi halutessaan käyttää.

Alla olevassa esimerkissä esitellään JSON-muotoista dataa tuottavan verkkopalvelun käyttöä. JavaScript-koodissa määritellään osoite, XMLHttpRequest-olio ja olioon liittyvä palvelimelta saadun tiedon käsittelevä funktio. Funktiossa käytetään JavaScriptin valmista JSON.parse-funktiota tekstimuotoisen JSON-vastauksen olioksi muuntamiseen. Tämän jälkeen dokumenttiin asetetaan palvelimelta saatu arvo.

Itse napin painamiseen liittyvä toiminnallisuus on suoraviivainen. Nappia painettaessa kutsutaan funktiota, jossa tehdään GET-tyyppinen asynkroninen pyyntö annettuun osoitteeseen.

Mikäli Spring-sovelluksissa haluaa tehdä pyyntöjä palvelimelle, tulee palvelimen osoitteen mahdollinen muuttuminen huomioida pyynnöissä. Tämä onnistuu luomalla sovelluksen osoitetta kuvaava muuttuja, jonka arvon asettaminen annetaan Thymeleafin vastuulle. Esimerkki alla.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Otsikko</title>
    </head>
    <body>
        <!-- content -->
        <script th:inline="javascript"> var contextRoot = /*[[@{/}]]*/ '';</script>
        <script th:src="@{/javascript/code.js}" defer></script>
    </body>
</html>

Nyt lähdekooditiedostossa code.js voidaan käyttää muuttujaan contextRoot asetettua arvoa osoitteen määrittelyssä.

var url = contextRoot + "path/to/random"
Loading

Vastaavasti JSON-muotoisen datan lähettäminen onnistuu liittämällä JSON-muotoinen data send-metodin parametriksi. Alla olevassa esimerkissä lähetetään JSON-muotoista tietoa JSONPlaceholder-palvelulle.

Loading

CORS: Rajoitettu pääsy resursseihin

Palvelinohjelmiston tarjoamiin tietoihin kuten kuviin ja videoihin pääsee käsiksi lähes mistä tahansa palvelusta. Palvelinohjelmiston toiminnallisuus voi rakentua toisen palvelun päälle. On myös mahdollista toteuttaa sovelluksia siten, että ne koostuvat pääosin selainpuolen kirjastoista, jotka hakevat tietoa palvelimilta.

Selainpuolella JavaScriptin avulla tehdyt pyynnöt ovat oletuksena rajoitettuja. Jos palvelimelle ei määritellä erillistä CORS-tukea, eivät sovelluksen osoitteen ulkopuolelta tehdyt JavaScript pyynnöt onnistu ellei niitä erikseen salli.

Palvelinohjelmistot määrittelevät voiko niihin tehdä pyyntöjä myös palvelimen osoitteen ulkopuolelta ("Cross-Origin Resource Sharing"-tuki). Yksinkertaisimmillaan CORS-tuen saa lisättyä palvelinohjelmistoon lisäämällä kontrollerimetodille annotaatio @CrossOrigin. Annotaatiolle määritellään osoitteet, joissa sijaitsevista osoitteista pyyntöjä saa tehdä.

@CrossOrigin(origins = "/**")
@GetMapping("/books")
@ResponseBody
public List<Book> getBooks() {
    return bookRepository.findAll();
}

Koko sovelluksen tasolla vastaavan määrittelyn voi tehdä erillisen konfiguraatiotiedoston avulla.

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**");
    }
}
Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan:

Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!