Ymmärtää REST-arkkitehtuurimallin perusperiaatteet. Osaa toteuttaa palvelun, joka tarjoaa dataa REST-muotoisen rajapinnan yli. Osaa toteuttaa palvelun, joka hyödyntää REST-rajapintaa. Tietää, että Javascript-koodia voi suorittaa selaimessa käyttäjän koneella. Osaa tehdä selainohjelmistosta Javascript-pyynnön palvelimelle. Osaa päivittää näkymää Javascript-pyynnön vastauksen perusteella.
REST-Arkkitehtuurimalli
REST (representational state transfer) on ohjelmointirajapintojen toteuttamiseen tarkoitettu arkkitehtuurimalli (tai tyyli). REST-malli määrittelee sovellukset tietoa käsittelevien osien (komponentit), tietokohteiden (resurssit), sekä näiden yhteyksien kautta.
Tietoa käsittelevät osat ovat selainohjelmisto, palvelinohjelmisto, ym. Resurssit ovat sovelluksen käsitteitä (henkilöt, kirjat, laskentaprosessit, laskentatulokset -- käytännössä mikä tahansa voi olla resurssi) sekä niitä yksilöiviä osoitteita. Resurssikokoelmat ovat löydettävissä ja navigoitavissa: resurssikokoelma voi löytyä esimerkiksi osoitteesta /persons
, /books
, /processes
tai /results
. Yksittäisille resursseille määritellään uniikit osoitteet (esimerkiksi /persons/1
), ja niillä on myös esitysmuoto (esimerkiksi HTML, JSON tai XML); dataa voi tyypillisesti lähettää ja vastaanottaa samassa muodossa.
Resursseja ja tietoa käsittelevien osien yhteys perustuu tyypillisesti asiakas-palvelin -malliin, missä asiakas tekee pyynnön ja palvelin kuuntelee ja käsittelee vastaanottamiaan pyyntöjä sekä vastaa niihin.
Tutustu Roy T. Fieldingin ja Richard N. Taylorin artikkeliin "Principled Design of the Modern Web Architecture", jossa REST määritellään sekä Fieldingin väitöskirjan viidenteen lukuun. Vaikka emme tässä kappaleessa täytä kaikkia REST-rajapintoihin liittyviä vaatimuksia -- ainakaan aluksi -- on Roy Fielding sitä mieltä, että oleellista on mahdollisuus resurssien välillä navigointiin.
"A truly RESTful API looks like hypertext. Every addressable unit of information carries an address, either explicitly (e.g., link and id attributes) or implicitly (e.g., derived from the media type definition and representation structure). Query results are represented by a list of links with summary information, not by arrays of object representations (query is not a substitute for identification of resources)."
REST-rajapinnat web-sovelluksissa
HTTP-protokollan yli käsiteltävillä REST-rajapinnoilla on tyypillisesti seuraavat ominaisuudet:
-
Juuriosoite resurssien käsittelyyn (esimerkiksi
/books
) -
Resurssien esitysmuodon määrittelevä mediatyyppi (esimerkiksi
HTML
,JSON
, ...), joka kertoo asiakkaalle miten resurssiin liittyvä data tulee käsitellä. - Resursseja voidaan käsitellä HTTP-protokollan metodeilla (GET, POST, DELETE, ..)
Kirjojen käsittelyyn ja muokkaamiseen määriteltävä rajapinta voisi olla esimerkiksi seuraavanlainen:
-
GET osoitteeseen
/books
palauttaa kaikkien kirjojen tiedot. -
GET osoitteeseen
/books/{id}
, missä{id}
on yksittäisen kirjan yksilöivä tunniste, palauttaa kyseisen kirjan tiedot. -
PUT osoitteeseen
/books/{id}
, missä{id}
on yksittäisen kirjan yksilöivä tunniste, muokkaa kyseisen kirjan tietoja. Kirjan uudet tiedot lähetetään osana pyyntöä. -
DELETE osoitteeseen
/books/{id}
poistaa kirjan tietyllä tunnuksella. -
POST osoitteeseen
/books
luo uuden kirjan pyynnössä lähetettävän datan pohjalta.
Osoitteissa käytetään tyypillisesti substantiivejä -- ei books?id={id}
vaan /books/{id}
. HTTP-pyynnön tyyppi määrittelee operaation. DELETE-tyyppisellä pyynnöllä poistetaan, POST-tyyppisellä pyynnöllä lisätään, PUT-tyyppisellä pyynnöllä päivitetään, ja GET-tyyppisellä pyynnöllä haetaan tietoja.
Datan muoto on toteuttajan päätettävissä. Tällä hetkellä eräs suosituista datamuodoista on JSON, sillä sen käyttäminen osana selainohjelmistoja on suoraviivaista JavaScriptin kautta. Myös palvelinohjelmistot tukevat olioiden muuttamista JSON-muotoon.
Oletetaan että edelläkuvattu kirjojen käsittelyyn tarkoitettu rajapinta käsittelee JSON-muotoista dataa. Kirjaa kuvaava luokka on seuraavanlainen:
// pakkaus ja importit
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Book extends AbstractPersistable<Long> {
private String name;
}
Kun luokasta on tehty olio, jonka id
-muuttujan arvo on 2
ja nimi "Harry Potter and the Chamber of Secrets"
, on sen JSON-esitys (esimerkiksi) seuraavanlainen:
{ "id":2, "name":"Harry Potter and the Chamber of Secrets" }
JSON-notaatio määrittelee olion alkavalla aaltosululla {
, jota seuraa oliomuuttujien nimet ja niiden arvot. Lopulta olio päätetään sulkevaan aaltosulkuun }
. Oliomuuttujien nimet ovat hipsuissa "
sillä ne käsitellään merkkijonoina. Muuttujien arvot ovat arvon tyypistä riippuen hipsuissa. Tarkempi kuvaus JSON-notaatiosta löytyy sivulta json.org.
Pyynnön rungossa lähetettävän JSON-muotoisen datan muuntaminen olioksi tapahtuu annotaation @RequestBody avulla. Annotaatio @RequestBody edeltää kontrollerimetodin parametrina olevaa oliota, johon JSON-muotoisen datan arvot halutaan asettaa.
@PostMapping("/books")
public String postBook(@RequestBody Book book) {
bookRepository.save(book);
return "redirect:/books";
}
Vastauksen saa lähetettyä käyttäjälle JSON-muodossa lisäämällä pyyntöä käsittelevään metodiin annotaatio @ResponseBody. Annotaatio @ResponseBody pyytää Spring-sovelluskehystä asettamaan palvelimen tuottaman datan selaimelle lähetettävän vastauksen runkoon. Jos vastaus on olio, muutetaan se (oletuksena) automaattisesti JSON-muotoiseksi vastaukseksi.
@GetMapping("/books/{id}")
@ResponseBody
public Book getBook(@PathVariable Long id) {
return bookRepository.getOne(id);
}
Alla oleva esimerkki sekä tallentaa olion tietokantaan, että palauttaa tietokantaan tallennetun olion pääavaimineen.
@PostMapping("/books")
@ResponseBody
public Book postBook(@RequestBody Book book) {
return bookRepository.save(book);
}
Palvelulle voi nyt lähettää JSON-muotoista dataa; vastaus on myös JSON-muotoinen, mutta luotavaan kirjaan on liitetty tietokantaan tallennuksen yhteydessä saatava kirjan yksilöivä tunnus.
Voimme lisätä annotaatioille @GetMapping
, @PostMapping
, jne lisätietoa metodin käsittelemästä datasta. Attribuutti consumes
kertoo minkälaista dataa metodin kuuntelema osoite hyväksyy. Metodi voidaan rajoittaa vastaanottamaan JSON-muotoista dataa merkkijonolla "application/json"
. Vastaavasti metodille voidaan lisätä tietoa datasta, jota se tuottaa. Attribuutti produces
kertoo tuotettavan datatyypin. Alla määritelty metodi sekä vastaanottaa että tuottaa JSON-muotoista dataa.
@PostMapping(path="/books", consumes="application/json", produces="application/json")
@ResponseBody
public Book postBook(@RequestBody Book book) {
return bookStorage.create(book);
}
Jos toteutat omaa REST-rajapintaa, kannattanee määritellä kontrolleriluokan annotaatioksi @RestController
. Tämä asettaa jokaisen luokan metodiin annotaation @ResponseBody
sekä sopivan datatyypin -- tässä tapauksessa "application/json".
Toteutetaan seuraavaksi kaikki tarvitut metodit kirjojen tallentamiseen. Kontrolleri hyödyntää erillistä luokkaa, joka tallentaa kirjaolioita tietokantaan ja tarjoaa tuen aiemmin määrittelemiemme books-osoitteiden ja pyyntöjen käsittelyyn -- PUT-metodi on jätetty omaa kokeilua varten.
// importit
@RestController
public class BookController {
@Autowired
private BookRepository bookRepository;
@GetMapping("/books")
public List<Book> getBooks() {
return bookRepository.findAll();
}
@GetMapping("/books/{id}")
public Book getBook(@PathVariable Long id) {
return bookRepository.getOne(id);
}
@DeleteMapping("/books/{id}")
public Book deleteBook(@PathVariable Long id) {
return bookRepository.deleteById(id);
}
@PostMapping("/books")
public Book postBook(@RequestBody Book book) {
return bookRepository.save(book);
}
}
Avoimen rajapinnan kolmannen osapuolen ohjelmistoille tarjoavat palvelinohjelmistot eivät aina sisällä erillistä käyttöliittymää. Niiden testaaminen tapahtuu sekä automaattisilla testeillä, että erilaisilla selainohjelmistoilla. Yksi hyvin hyödyllinen apuväline on Postman, jonka saa työpöytäsovelluksena ja Chromen liitännäisenä.
Postmanin hyödyntäminen on erittäin suositeltavaa -- kannattaa katsoa sen johdatusvideo, joka löytyy Postmanin sivulta. Katso myös RESTiä käsittelevä Youtube-video, missä Postmania käytetään hieman.
Tässä tehtävässä toteutetaan pelitulospalvelu, joka tarjoaa REST-rajapinnan pelien ja tuloksien käsittelyyn. Huom! Kaikki syötteet ja vasteet ovat JSON-muotoisia olioita. Tehtäväpohjassa on toteutettu valmiiksi luokat Game
ja Score
sekä käytännölliset Repository
-rajapinnat.
GameController
Pelejä käsitellään luokan Game
avulla.
Toteuta pakkaukseen wad.controller
luokka GameController
, joka tarjoaa REST-rajapinnan pelien käsittelyyn:
-
POST
/games
luo uuden pelin sille annetun pelin tiedoilla ja palauttaa luodun pelin tiedot. (Huom. vieläkin! Pyynnön rungossa oleva data on aina JSON-muotoista. Vastaukset tulee myös palauttaa JSON-muotoisina.) -
GET
/games
listaa kaikki talletetut pelit. -
GET
/games/{name}
palauttaa yksittäisen pelin tiedot pelin nimen perusteella. -
DELETE
/games/{name}
poistaa nimen mukaisen pelin. Palauttaa poistetun pelin tiedot.
ScoreController
Jokaiselle pelille voidaan tallettaa pelikohtaisia tuloksia (luokka Score
). Jokainen pistetulos kuuluu tietylle pelille, ja tulokseen liittyy aina pistetulos points
numerona sekä pelaajan nimimerkki nickname
.
Toteuta luokka wad.controller.ScoreController
, joka tarjoaa REST-rajapinnan tuloksien käsittelyyn:
-
POST
/games/{name}/scores
luo uuden tuloksen pelillename
ja asettaa tulokseen pelin tiedot. Tuloksen tiedot lähetetään kyselyn rungossa. -
GET
/games/{name}/scores
listaa pelinname
tulokset. -
GET
/games/{name}/scores/{id}
palauttaa tunnuksellaid
löytyvän tuloksenname
-nimiselle pelille. -
DELETE
/games/{name}/scores/{id}
poistaa avaimenid
mukaisen tuloksen peliltäname
(pelin tietoja ei tule pyynnön rungossa). Palauttaa poistetun tuloksen tiedot.
Valmiin palvelun käyttäminen
Toisen sovelluksen tarjoamaan REST-rajapintaan pääsee kätevästi käsiksi RestTemplate-luokan avulla. Voimme luoda oman komponentin kirjojen hakemiseen.
// importit
@Service
public class BookService {
private RestTemplate restTemplate;
public BookService() {
this.restTemplate = new RestTemplate();
}
// tänne luokan tarjoamat palvelut
}
Alla on kuvattuna RestTemplaten käyttö tiedon hakemiseen, päivittämiseen, poistaamiseen ja lisäämiseen. Esimerkeissä merkkijono osoite vastaa palvelimen osoitetta, esimerkiksi http://www.google.com
.
- GET osoitteeseen /books palauttaa kaikkien kirjojen tiedot tai osajoukon kirjojen tiedoista -- riippuen toteutuksesta.
// kirjojen hakeminen
List<Book> books = restTemplate.getForObject("osoite/books", List.class);
// tunnuksella 5 määritellyn kirjan hakeminen
Book book = restTemplate.getForObject("osoite/books/{id}", Book.class, 5);
// tunnuksella 5 määritellyn kirjan hakeminen
Book book = restTemplate.getForObject("osoite/books/{id}", Book.class, 5);
book.setName(book.getName() + " - DO NOT BUY!");
// kirjan tietojen muokkaaminen
restTemplate.put("osoite/books/{id}", book, 5);
// tunnuksella 32 määritellyn kirjan poistaminen
restTemplate.delete("osoite/books/{id}", 32);
Book book = new Book();
book.setName("Harry Potter and the Goblet of Fire");
// uuden kirjan lisääminen
book = restTemplate.postForObject("osoite/books", book, Book.class);
Usein sovellukset hyödyntävät kolmannen osapuolen tarjoamaa palvelua omien toiminnallisuuksiensa toteuttamiseen. Harjoitellaan tätä seuraavaksi.
Palvelu GameRater lisää aiempaan tulospalveluun mahdollisuuden arvostella yksittäisiä pelejä antamalla niille numeroarvosanan 0-5. Arvostelu tehdään kuitenkin erilliseen palveluun, emmekä siis laajenna edellistä palvelua suoraan.
GameRater-palvelun tulee käyttää ScoreService-palvelun REST-rajapintaa, jonka avulla se tarjoaa samanlaisen rajapinnan pelien ja tulosten käsittelyyn. Ainoastaan pelien arvostelut käsitellään ja talletetaan tässä palvelussa! Arvosteluihin käytettävä entiteetti Rating
ja siihen liittyvät palveluluokat on valmiina tehtäväpohjassa.
Huom! Joudut tutkimaan tehtäväpohjassa annettua koodia, jotta voit hyödyntää sitä. Joudut myös lukemaan tehtävän ScoreService kuvausta tämän tehtävän toteutuksessa.
Huom! Valmis ScoreService-palvelu löytyy osoitteesta http://wepa-scoreservice-heroku.herokuapp.com/games
, joten voit tehdä tämän tehtävän täysin riippumatta tulospalvelu-tehtävästä.
Ja asiaan..
Tee luokka wad.service.GameRestClient
, joka toteuttaa rajapinnan GameService
. Luokan tulee käyttää ScoreService-palvelua kaikissa rajapinnan määrittelemissä toiminnoissa. REST-rajapinnan käyttö onnistuu Springin RestTemplate
-luokan avulla.
Huom! GameRestClient
-luokan setUri
-metodi ottaa parametriksi yllä annetun URL-osoitteen valmiiseen ScoreService-palveluun.
Luo luokka wad.controller.GameController
, joka tarjoaa täsmälleen samanlaisen JSON/REST-rajapinnan kuin Tulospalvelu-palvelun GameController
, mutta siten, että jokainen toiminto käyttää valmista ScoreService-palvelua rajapinnan GameService
kautta.
Huom! Muista asettaa GameService
-rajapinnan kautta URL-osoite valmiiseen http://wepa-scoreservice-heroku.herokuapp.com/games
-osoitteeseen ohjelman käynnistyessä, esimerkiksi controller-luokan @PostConstruct
-metodissa.
RatingController
Jokaiselle pelille voidaan tallettaa pelikohtaisia arvosteluja entiteetin Rating
avulla. Arvosteluun liittyy numeroarvosana rating
(0-5).
Arvostelut liittyvät peleihin, jotka on talletettu eri palveluun, joten entiteetin Rating
viittaus peliin täytyy tallettaa suoraan avaimena. Koska peleihin viitataan REST-rajapinnassa pelin nimellä, talletetaan jokaiseen Rating
-entiteettiin pelin nimi attribuuttiin gameName
. Tämän attribuutin avulla voidaan siis löytää arvosteluja pelin nimen perusteella.
Toteuta luokka wad.controller.RatingController
, joka tarjoaa REST-rajapinnan arvostelujen käsittelyyn:
-
POST /games/{name}/ratings
luo uuden arvostelun pelillename
- ainoa vastaanotettava attribuutti onrating
-
GET /games/{name}/ratings
listaa talletetut arvostelut pelillename
-
GET /games/{name}/ratings/{id}
palauttaa yksittäisen arvostelun tiedot pelin nimenname
ja avaimenid
perusteella
Jos osoitteessa http://wepa-scoreservice-heroku.herokuapp.com/games olevan palvelun tietokanta on "täynnä", voi tietokannan tyhjentää tekemällä kutsun osoitteeseen http://wepa-scoreservice-heroku.herokuapp.com/games
.
REST-toteutuksen kypsyystasot
Martin Fowler käsittelee artikkelissaan Richardson Maturity Model REST-rajapintojen kypsyyttä. Richardson Maturity Model (RMM) jaottelee REST-toteutuksen kolmeen tasoon, joista kukin tarkentaa toteutusta.
Aloituspiste on tason 0 palvelut, joita ei pidetä REST-palveluina. Näissä palveluissa HTTP-protokollaa käytetään lähinnä väylänä viestien lähettämiseen ja vastaanottamiseen, ja HTTP-protokollan käyttötapaan ei juurikaan oteta kantaa. Esimerkki tason 0 palvelusta on yksittäinen kontrollerimetodi, joka päättelee toteutettavan toiminnallisuuden pyynnössä olevan sisällön perusteella.
Tason 1 palvelut käsittelevät palveluita resursseina. Resurssit kuvataan palvelun osoitteena (esimerkiksi /books
-resurssi sisältää kirjoja), ja resursseja voidaan hakea tunnisteiden perusteella (esim. /books/nimi
). Edelliseen tasoon verrattuna käytössä on nyt konkreettisia resursseja; olio-ohjelmoijan kannalta näitä voidaan pitää myös olioina joilla on tila.
Tasolla 2 resurssien käsittelyyn käytetään kuvaavia HTTP-pyyntötyyppejä. Esimerkiksi resurssin pyyntö tapahtuu GET-metodilla, ja resurssin tilan muokkaaminen esimerkiksi PUT, POST, tai DELETE-metodilla. Näiden lisäksi palvelun vastaukset kuvaavat tapahtuneita toimintoja. Esimerkiksi jos palvelu luo resurssin, vastauksen tulee olla statuskoodi 201
, joka viestittää selaimelle resurssin luomisen onnistumisesta. Oleellista tällä tasolla on pyyntötyyppien erottaminen sen perusteella että muokkaavatko ne palvelimen dataa vai ei (GET vs. muut).
Kolmas taso sisältää tasot 1 ja 2, mutta lisää käyttäjälle mahdollisuuden ymmärtää palvelun tarjoama toiminnallisuus palvelimen vastausten perusteella. Webissä huomiota herättänyt termi HATEOAS käytännössä määrittelee miten web-resursseja tulisi löytää webistä.
Roy Fielding kokee vain tason 3 sovelluksen oikeana REST-sovelluksena. Ohjelmistosuunnittelun näkökulmasta jokainen taso parantaa sovelluksen ylläpidettävyyttä -- Level 1 tackles the question of handling complexity by using divide and conquer, breaking a large service endpoint down into multiple resources; Level 2 introduces a standard set of verbs so that we handle similar situations in the same way, removing unnecessary variation; Level 3 introduces discoverability, providing a way of making a protocol more self-documenting. (lähde)
Huom! Sovellusta suunniteltaessa ja toteuttaessa ei tule olettaa että RMM-tason 3 sovellus olisi parempi kuin RMM-tason 2 sovellus. Sovellus voi olla huono riippumatta toteutetusta REST-rajapinnan muodosta -- jossain tapauksissa rajapintaa ei oikeasti edes tarvita; asiakkaan tarpeet ja toiveet määräävät mitä sovelluskehittäjän kannattaa tehdä.
Spring Data REST
Spring-sovelluskehys sisältää projektin Spring Data REST, minkä avulla REST-palveluiden tekeminen helpottuu hieman. Lisäämällä projektin pom.xml
-konfiguraatioon riippuvuus spring-boot-starter-data-rest
saamme Spring Boot-paketoidun version kyseisestä projektista käyttöömme.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
Nyt Repository-luokkamme tarjoavat automaattisesti REST-rajapinnan, jonka kautta resursseihin pääsee käsiksi. REST-rajapinta luodaan oletuksena sovelluksen juureen, ja tehdään luomalla monikko domain-olioista. Esimerkiksi, jos käytössä on luokka Book
, sekä sille määritelty BookRepository
, joka perii Spring Data JPA:n rajapinnan, generoidaan rajapinnan /books
alle toiminnallisuus kirja-olioiden muokkaamiseen.
Usein sovelluksemme kuitenkin toimivat jo palvelun juuripalvelussa, ja haluemme tarjota rajapinnan erillisessä osoitteesssa. Spring Data REST-projektin konfiguraatiota voi muokata erillisen RepositoryRestMvcConfiguration
-luokan kautta. Alla olevassa esimerkissä REST-rajapinta luodaan osoitteen /api/v1
-alle. Annotaatio @Component
kertoo Springille että luokka tulee ladata käyttöön käynnistysvaiheessa; rajapinta kertoo mistä luokasta on kyse.
// pakkaus ja importit
@Component
public class CustomizedRestMvcConfiguration extends RepositoryRestConfigurerAdapter {
@Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.setBasePath("/api/v1");
}
}
Nyt jos sovelluksessa on entiteetti Book
sekä siihen sopiva BookRepository
, on Spring Data REST-rajapinta osoitteessa /api/v1/books
.
Käytännössä sovelluksen kehittäjä ei kuitenkaan halua kaikkia HTTP-protokollan metodeja kaikkien käyttöön. Käytössä olevien metodien rajaaminen onnistuu käytettävää Repository
-rajapintaa muokkaamalla. Alla olevassa esimerkissä BookRepository
-rajapinnan olioita ei pysty poistamaan automaattisesti luodun REST-rajapinnan yli.
// pakkaus
import wad.domain.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RestResource;
public interface BookRepository extends JpaRepository<Message, Long> {
@RestResource(exported = false)
@Override
public void delete(Long id);
}
Spring Data REST ja RestTemplate
Spring Data RESTin avulla luotavien rajapintojen hyödyntäminen onnistuu RestTemplaten avulla. Esimerkiksi yllä luotavasta rajapinnasta voidaan hakea Resource
-olioita, jotka sisältävät kirjoja. RestTemplaten metodin exchange
palauttaa vastausentiteetin, mikä sisältää hakemamme olion tiedot. Kyselyn mukana annettava ParameterizedTypeReference
taas kertoo minkälaiseksi olioksi vastaus tulee muuntaa.
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Resource<Book>> response =
restTemplate.exchange("osoite/books/1", // osoite
HttpMethod.GET, // metodi
null, // pyynnön runko; tässä tyhjä
new ParameterizedTypeReference<Resource<Book>>() {}); // vastaustyyppi
if (response.getStatusCode() == HttpStatus.OK) {
Resource<Book> resource = response.getBody();
Book book = resource.getContent();
}
Verkko on täynnä avoimia (ja osittain avoimia) ohjelmointirajapintoja, jotka odottavat niiden hyödyntämistä. Tällaisia kokoelmia löytyy muunmuassa osoitteista https://www.avoindata.fi/fi, https://data.europa.eu/euodp/en/home, https://index.okfn.org/dataset/, https://github.com/toddmotto/public-apis, jne.
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. Käytännössä siis, jos käyttäjä on sivulla hs.fi
, selaimessa toimiva Javascript-ohjelmisto voi tehdä pyyntöjä vain osoitteeseen hs.fi
.
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("/**");
}
}
Nyt sovellukseen voi tehdä Javascript-pyynnön missä tahansa sijaitsevasta sovelluksesta.
Selainohjelmistot
Tutustutaan seuraavaksi selainpuolen toiminnallisuuden peruspalasiin.
Web-sivujen rakenne
Web-sivut määritellään HTML-kielen avulla. Yksittäinen HTML-dokumentti koostuu sisäkkäin ja peräkkäin olevista elementeistä, jotka määrittelevät sivun rakenteen sekä sivun sisältävän tekstin. Rakenteen määrittelevät elementit erotellaan pienempi kuin (<) ja suurempi kuin (>) -merkeillä. Elementti avataan elementin nimen sisältävällä pienempi kuin -merkillä alkavalla ja suurempi kuin -merkkiin loppuvalla merkkijonolla, esim. <html>
, ja suljetaan merkkijonolla jossa elementin pienempi kuin -merkin jälkeen on vinoviiva, esim </html>
. Yksittäisen elementin sisälle voi laittaa muita elementtejä.
Tyypillisen HTML-dokumentin runko näyttää seuraavalta. Kun klikkaat allaolevassa iframe
-elementissä Result
-tekstiä, näet HTML-sivun, ja kun painat HTML
-tekstiä, näet HTML-koodin. Klikkaamalla elementin oikeassa ylälaidassa olevasta Edit in JSFiddle-linkistä, pääset muokkaamaan elementtiä suoraan JSFiddlessä.
Yllä olevassa HTML-dokumentissa on dokumentin tyypin kertova erikoiselementti <!DOCTYPE html>
, joka kertoo dokumentin olevan HTML-sivu. Tätä seuraa elementti <html>
, joka aloittaa HTML-dokumentin. Elementti <html>
sisältää yleensä kaksi elementtiä, elementit <head>
ja <body>
. Elementti <head>
sisältää sivun otsaketiedot, eli esimerkiksi sivun käyttämän merkistön <meta charset="utf-8" />
ja otsikon <title>
. Elementti <body>
sisältää selaimessa näytettävän sivun rungon. Ylläolevalla sivulla on ensimmäisen tason otsake-elementti h1
(header 1) ja tekstielementti p
(paragraph).
Elementit voivat sisältää tekstisolmun. Esimerkiksi yllä olevat elementit title
, h1
ja p
kukin sisältävät tekstisolmun eli tekstiä. Tekstisolmulle ei ole erillistä elementtiä tai määrettä, vaan se näkyy käyttäjälle sivulla olevana tekstinä.
Puhe tekstisolmuista antaa viitettä jonkinlaisesta puurakenteesta. HTML-dokumentit ovat rakenteellisia dokumentteja, joiden rakenne on usein helppo ymmärtää puumaisena kaaviona. Ylläolevan web-sivun voi esittää esimerkiksi seuraavanlaisena puuna.
html / \ / \ head body / \ / \ meta title h1 p : : : tekstiä tekstiä tekstiä
Koska HTML-dokumentti on rakenteellinen dokumentti, on elementtien sulkemisjärjestyksellä väliä. Elementit tulee sulkea samassa järjestyksessä kuin ne on avattu. Esimerkiksi, järjestys <body><p>whoa, minttutee!</body></p>
on väärä, kun taas järjestys <body><p>whoa, minttutee!</p></body>
on oikea.
Kaikki elementit eivät kuitenkaan sisällä tekstisolmua, eikä niitä suljeta erikseen. Yksi näistä poikkeuksista on link-elementti.
Kun selaimet lataavat HTML-dokumenttia ja muodostavat sen perusteella muistissa säilytettävää puuta, ne käyvät sen läpi ylhäältä alas, vasemmalta oikealle. Kun selain kohtaa elementin, se luo sille uuden solmun. Seuraavista elementeistä luodut solmut menevät aiemmin luodun solmun alle kunnes aiemmin kohdattu elementti suljetaan. Aina kun elementti suljetaan, puussa palataan ylöspäin edelliselle tasolle.
Elementit, attribuutit, nimet ja luokat
Elementit voivat sisältää attribuutteja, joilla voi olla yksi tai useampi arvo. Edellä nähdyssä HTML-dokumentissa elementille meta
on määritelty erillinen attribuutti charset
, joka kertoo dokumentissa käytettävän merkistön: "utf-8". Vastaavasti tiedon syöttämiseen käytettävien lomakkeiden input
ym. kentissä käyttämämme attribuutti name
määrittelee nimen, jota käytetään palvelimelle lähetettävän kentän sisällön tunnistamisessa.
Muita yleisesti käytettäviä attribuuttityyppejä ovat id
, joka määrittelee elementille uniikin tunnisteen sekä class
, jonka avulla elementille voidaan määritellä tyyppiluokitus. Uudehkossa HTML5-määritelmässä elementit voivat sisältää myös data
-attribuutteja, joiden toiminnallisuutta ei ole ennalta määritelty, ja joita käytetään tyypillisesti sovelluksen toiminnallisuuden takaamiseksi.
Kun elementtejä haetaan id-attribuutin perusteella, vastaukseksi pitäisi tulla tyypillisesti vain yksi elementti, mutta class-attribuutin perusteella hakuvastauksia voi olla useampi.
W3Schools-sivusto sisältää hyvän yhteenvedon käytössä olevista attribuuteista: http://www.w3schools.com/tags/ref_attributes.asp. Lisätietoa data-attribuuteista löytyy osoitteesta http://www.w3schools.com/tags/att_global_data.asp.
Javascript
Siinä missä HTML on kieli web-sivujen rakenteen ja sisällön luomiseen, JavaScript on kieli dynaamisen toiminnan lisäämiselle. JavaScript on ohjelmakoodia, jota suoritetaan komento kerrallaan -- ylhäältä alas, vasemmalta oikealle.
JavaScript-koodi suoritetaan käyttäjän omassa selaimessa. Samalla on hyvä kuitenkin mainita, että nykyään myös palvelinohjelmistoja ohjelmoidaan Javascriptillä -- tästä esimerkkinä NodeJs.
JavaScript-tiedoston pääte on yleensä .js
ja siihen viitataan elementillä script
. Elementillä script
on attribuutti src
, jolla kerrotaan lähdekooditiedoston sijainti. Kun lisäämme Javascript-koodia web-projektiimme, lisätään se tyypillisesti kansion src/main/resources/public/javascript/
alle. 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>
.
Yleinen käytänne JavaScript-lähdekoodien sivulle lisäämiseen on lisätä ne sivun loppuun juuri ennen body
-elementin sulkemista. Tämä johtuu mm. siitä, että selain lähtee hakemaan JavaScript-tiedostoa kun se kohtaa sen määrittelyn HTML-dokumentissa, jolloin kaikki muut toiminnot odottavat latausta. Jos 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ä luodaan tunne nopeammin reagoivista ja latautuvista sivuista.
Nykyään script
-elementille voi lisätä määreen defer
, jonka olemassaolo kertoo että elementin src
-attribuutin määrittelemä tiedosto tulee suorittaa vasta kun html-sivu on käsitelty.
...
<script th:src="@{/javascript/code.js}" defer></script>
...
Defer-määre on kuitenkin uudehko lisä, eikä se toimi kaikissa selaimissa. Lisätietoa täältä...
Luodaan kansioon javascript
lähdekooditiedosto code.js
. Tiedostossa code.js
on funktio sayHello
. Funktio luo ponnahdusikkunan, missä on teksti "hello there".
function sayHello() {
alert("hello there");
}
HTML-dokumentti, jossa lähdekooditiedosto ladataan, näyttää seuraavalta. Attribuutille onclick
määritellään elementin klikkauksen yhteydessä suoritettava koodi.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" >
<title>Sivun otsikko (näkyy selaimen palkissa)</title>
</head>
<body>
<header>
<h1>Sivulla näkyvä otsikko</h1>
</header>
<article>
<p>Sivuilla näytettävä normaali teksti on p-elementin sisällä. Alla on nappi,
jota painamalla kutsutaan funktiota "sayHello".</p>
<input type="button" value="Tervehdi" onclick="sayHello();" />
</article>
<!-- ladataan JavaScript-koodit tiedoston lopussa! -->
<script th:src="@{javascript/code.js}"></script>
</body>
</html>
Alla sama JSFiddlessä -- siellä kuitenkin code.js
samassa kansiossa HTML-tiedoston kanssa:
Jos Javascript ei ole ennalta tuttu kieli, kannattaa tutustua W3Schools-sivuston tarjoamaan Javascript-oppaaseen sekä kurssin Web-selainohjelmointi (jo hieman hapantuneeseen) materiaaliin.
Sivujen rakenteen muokkaaminen Javascriptin avulla
JavaScriptiä käytetään ennenkaikkea dynaamisen toiminnallisuuden lisäämiseksi web-sivuille. Esimerkiksi web-sivuilla oleviin elementteihin tulee pystyä asettamaan arvoja, ja niitä tulee myös pystyä hakemaan. JavaScriptissä pääsee käsiksi dokumentissa oleviin elementteihin komennolla document.getElementById("tunnus")
, joka palauttaa elementin, jonka id
-attribuutti on "tunnus". Muita attribuutti- ja elementtityyppejä pääsee käsittelemään esimerkiksi querySelector-metodin avulla.
Alla on tekstikenttä, jonka HTML-koodi on <input type="text" id="tekstikentta"/>
. Kentän tunnus on siis tekstikentta
. Jos haluamme päästä käsiksi elementtiin, jonka tunnus on "tekstikentta", käytämme komentoa document.getElementById("tekstikentta")
. Tekstikenttäelementillä on attribuutti value
, joka voidaan tulostaa.
Tekstikentälle voidaan asettaa arvo kuten muillekin muuttujille. Alla olevassa esimerkissä haetaan edellisen esimerkin tekstikenttä, ja asetetaan sille arvo 5
.
Tehdään vielä ohjelma, joka kysyy käyttäjältä syötettä, ja asettaa sen yllä olevan tekstikentän arvoksi.
Arvon asettaminen osaksi tekstiä
Yllä tekstikentälle asetettiin arvo sen value
-attribuuttiin. Kaikilla elementeillä ei ole value
-attribuuttia, vaan joillain näytetään niiden elementin sisällä oleva arvo. Elementin sisälle asetetaan arvo muuttujaan liittyvällä attribuutilla innerHTML
.
Alla olevassa esimerkissä sivulla on tekstielementti, jossa ei ole lainkaan sisältöä. Jos tekstielementtiin lisätään sisältöä, tulee se näkyville.
Vastaavasti tekstin keskelle -- sisäelementtiin -- voi asettaa arvoja. Elementti span
sopii tähän hyvin.
Case: 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:
Toteuta edellisen esimerkin perusteella laskin, jossa on plus-, miinus-, kerto- ja jakolaskutoiminnallisuus. Keskity vain selainpuolen toiminnallisuuteen: älä muokkaa palvelinpuolen toiminnallisuutta. Varmista myös, että sivu on käytettävä ilman erillistä ohjetekstiä, eli että käyttämäsi napit ja tekstit kertovat käyttäjälle kaiken oleellisen.
Tehtävään ei ole TMC:ssä testejä -- kun sovellus toimii oikein, lähetä se palvelimelle.
Elementtien valinta
Käytimme getElementById
-kutsua tietyn elementin hakemiseen. Kaikki sivun elementit voi taas hakea esimerkiksi getElementsByTagName("*")
-kutsulla. Molemmat ovat kuitenkin hieman kömpelöjä jos tiedämme mitä haluamme hakea.
W3C DOM-määrittely sisältää myös paremman ohjelmointirajapinnan elementtien läpikäyntiin. Selectors API sisältää mm. querySelector
-kutsun, jolla saadaan CSS-valitsinten kaltainen kyselytoiminnallisuus.
Selector APIn tarjoamien querySelector
(yksittäisen osuman haku) ja querySelectorAll
(kaikkien osumien haku) -komentojen avulla kyselyn rajoittaminen vain header
-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ä
Elementtien lisääminen
HTML-dokumenttiin lisätään uusia elementtejä document
-olion createElement
-metodilla. Esimerkiksi alla luodaan p
-elementti (tekstisolmu; createTextNode
), joka asetetaan muuttujaan tekstiElementti
. Tämän jälkeen luodaan tekstisolmu, joka sisältää tekstin "o-hai". Lopulta tekstisolmun lisätään tekstielementtiin.
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 dom-esim-3
. 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("dom-esim-3");
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ä elementtejä lisätään yksitellen. Mukana on myös laskuri, joka pitää kirjaa elementtien lukumäärästä.
jQuery
jQuery on JavaScript-kirjasto, jonka tavoitteena on helpottaa selainohjelmistojen toteutusta. Se tarjoaa apuvälineitä mm. DOM-puun muokkaamiseen, tapahtumien käsittelyyn sekä palvelimelle tehtävien kyselyiden toteuttamiseen, ja sen avulla toteutettu toiminnallisuus toimii myös useimmissa selaimissa.
Uusimman jQuery-version saa ladattua täältä. Käytännössä jQuery on JavaScript-tiedosto, joka ladataan sivun latautuessa. Tiedoston voi asettaa esimerkiksi head
-elementin sisään, tai ennen omia lähdekooditiedostoja.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Selaimen palkissa ja suosikeissa näkyvä otsikko</title>
</head>
<body>
<!-- sivun sisältö -->
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
<script src="javascript/koodi.js"></script>
</body>
</html>
Valitsimet
Käytimme edellisissä osioissa valmiita JavaScriptin DOM-toiminnallisuuksia. Elementtien etsimiseen on käytetty mm. getElementById
-kutsua. JQuery käyttää Sizzle-kirjastoa elementtien valinnan helpottamiseen. Esimerkiksi elementti, jonka attribuutin "id" arvo on "nimi", löytyy seuraavalla komennolla.
var elementti = $("#nimi");
Kyselyt ovat muotoa$("kysely")
. Jos elementtia haetaan id
-attribuutin perusteella, lisätään kyselyn alkuun risuaita. Jos elementtiä haetaan luokan (class
) perusteella, lisätään kyselyn alkuun piste. Jos taas elementtiä halutaan hakea esimerkiksi nimen perusteella, muodostetaan kysely sekä elementin että attribuutin kautta, esim. $("input[name=nimi]")
palauttaa kaikki input-tyyppiset elementit, joissa name
-attribuutin arvo on nimi
.
Tarkempi kuvaus jQueryn valitsimista löytyy osoitteesta http://api.jquery.com/category/selectors/.
Elementtien lisääminen
JQuery tekee elementtien lisäämisestä hieman suoraviivaisempaa. Voimme kutsun document.createElement
sijaan määritellä elementin tyypin sanomalla $("<article />");
. Myös tekstielementin luominen on hieman helpompaa: $("<p/>").text("test");
. Aiempi koodimme:
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);
Voidaan kirjoittaa myös hieman suoraviivaisemmin:
var artikkeli = $("<article/>");
var teksti1 = $("<p/>");
teksti1.text("Lorem ipsum... 1");
artikkeli.append(teksti1);
var teksti2 = $("<p/>");
teksti2.text("Lorem ipsum... 2");
artikkeli.append(teksti2);
$("#osio").append(artikkeli);
Tarkempi kuvaus operaatioista DOM-puun muokkaamiseen löytyy osoitteesta http://api.jquery.com/category/Manipulation/.
Tapahtumien käsittely
JQuery rakentaa JavaScriptin valmiiden komponenttien päälle, joten sillä on toiminnallisuus myös tapahtumankäsittelijöiden rekisteröimiseen sivun komponenteille. Eräs hyvin hyödyllinen tapahtumankäsittelijä liittyy sivun latautumiseen: komennolla $(document).ready(function() {});
voidaan määritellä funktion runko, joka suoritetaan kun sivun latautuminen on valmis.
Kun sivun latautuminen on valmis, voimme olla varmoja siitä, että sivulla on kaikki siihen kuuluvat elementit. Tällöin on näppärää tehdä myös kyselyjä palvelimelle. Jos haluaisimme että id-attribuutin arvolla "osio" määriteltyyn elementtiin lisättäisiin kaksi tekstielementtiä sisältävä artikkelielementti kun sivu on latautunut, olisi tarvittava Javascript-koodi seuraavanlainen:
$(document).ready(function() {
var artikkeli = $("<article/>");
var teksti1 = $("<p/>");
teksti1.text("Lorem ipsum... 1");
artikkeli.append(teksti1);
var teksti2 = $("<p/>");
teksti2.text("Lorem ipsum... 2");
artikkeli.append(teksti2);
$("#osio").append(artikkeli);
});
JSON, eli JavaScript Object Notation
, on tiedon esitysmuoto. Olion määrittely alkaa aaltosululla {
, jota seuraa muuttujan nimi ja sille annettava arvo. Arvon asetus oliomuuttujalle tapahtuu kaksoispisteellä, esimerkiksi nimi: "Arvo"
. Useampia muuttujia voi määritellä pilkulla eroteltuna. Olion määrittely lopetetaan sulkevaan aaltosulkuun }
.
var olio = {nimi: "Arvo", tieto: 2000};
Olion muuttujiin pääsee käsiksi piste-notaatiolla. Esimerkiksi olio
-olion muuttuja nimi
löytyy komennolla olio.nimi
.
var olio = {nimi: "Arvo", tieto: 2000};
alert(olio.nimi);
Myös uusien oliomuuttujien lisääminen on suoraviivaista. Uuden muuttujan lisääminen tapahtuu myös pistenotaatiolla -- harrastuksen lisääminen tapahtuu olio
-oliolle sanomalla olio.harrastus = "koodaus";
.
var olio = {nimi: "Arvo", tieto: 2000};
alert(olio.nimi);
olio.harrastus = "koodaus";
alert(olio.harrastus);
Olioiden rakennetta ei siis ole lyöty ennalta lukkoon.
Kyselyt palvelimelle
JQuery tarjoaa myös tuen kyselyjen tekemiseen erilliselle palvelinkomponentille.
Kyselyt hoituvat kätevästi JQueryn $.getJSON
-funktiolla. Alla olevassa esimerkissä haemme ICNDb.comista oleellista dataa.
Kyselyn palauttama data ohjataan $.getJSON
-funktion toisena parametrina määriteltävään funktioon. Alla olevassa esimerkissä kutsumme vain alert
-komentoa kaikelle palautettavalle datalle.
$.getJSON("http://api.icndb.com/jokes/random/5",
function(data) {
alert(data);
}
);
Ylläoleva esimerkki tulostaa vastaukset konsoliin -- huomaa, että jQuery muuntaa merkkijonomuotoiset vastaukset automaattisesti JSON-olioksi. Käytetään JQueryn each
-komentoa listassa olevien elementtien iterointiin. Komennolle each
voi antaa parametrina iteroitavan listan, sekä funktion, jota kutsutaan jokaisella listassa olevalla oliolla.
$.getJSON("http://api.icndb.com/jokes/random/5",
function(data) {
$.each(data.value, function(i, item) {
alert(i);
alert(item);
alert("-----");
});
}
);
Nyt ylläoleva komento tulostaa vastauksen value-kentässä olevat oliot yksitellen. Oletetaan, että käytössämme on elementti, jonka tunnus on "vitsit". JQuery tarjoaa myös mahdollisuuden nopeaan tekstielementtien luontiin komennolla $("<p/>")
. Elementteihin voi asettaa tekstin text
-komennolla, ja elementin voi lisätä tietyllä tunnuksella määriteltyyn elementtiin komennolla appendTo("#tunnus")
.
$.getJSON("http://api.icndb.com/jokes/random/5",
function(data) {
$.each(data.value, function(i, item) {
$("<p/>").text(item.joke).appendTo("#vitsit");
});
}
);
Tiedon lähettäminen palvelimelle
Jos tiedämme, että palvelu palauttaa JSON-dataa, voimme käyttää yllä käsiteltyä lähestymistapaa. Esimerkiksi viestien noutaminen Chat-chat -tehtävän viestipalvelimelta onnistuu seuraavalla komennolla. Tässä tapauksessa lisäämme jokaiseen viestiin liittyvän message
-attribuutin "vitsit"-tunnuksella määriteltyyn elementtiin. Osoitteessa http://bad.herokuapp.com/app/messages on valmiina viestejä tarjoava sovellus.
$.getJSON("http://bad.herokuapp.com/app/messages", function(data) {
$.each(data, function(i, item) {
$("<p/>").text(item.message).appendTo("#vitsit");
});
});
Yllä oleva komento on lyhenne alla määritellystä komennosta.
$.ajax({
url: "http://bad.herokuapp.com/app/messages",
dataType: 'json',
success: parseMessages
});
function parseMessages(messages) {
$.each(messages, function(i, item) {
$("<p/>").text(item.message).appendTo("#vitsit");
});
}
Komennolle $.ajax
voi lisätä myös dataa, mitä lähetetään palvelimelle. Esimerkiksi seuraavalla komennolla lähetetään osoitteeseen http://bad.herokuapp.com/app/in
olio, jonka sisällä on attribuutit name
ja details
. Lähetettävän datan tyyppi asetetaan attribuutilla contentType
, alla ilmoitamme että data on json-muotoista, ja että se käyttää utf-8 -merkistöä.
var dataToSend = JSON.stringify({
name: "bob",
details: "i'm ted"
});
$.ajax({
url: "http://bad.herokuapp.com/app/in",
dataType: 'json',
contentType:'application/json; charset=utf-8',
type: 'post',
data: dataToSend
});
Pyynnössä voi sekä lähettää että vastaanottaa dataa. Attribuutin success
asettaminen ylläolevaan pyyntöön aiheuttaa success-attribuutin arvona olevan funktion kutsun kun pyyntö on onnistunut.
Tehtävään on hahmoteltu tehtävien hallintaan tarkoitetun sovelluksen palvelinpuolen toiminnallisuutta. Lisää sovellukseen selainpuolen toiminnallisuus, joka mahdollistaa tehtävien lisäämisen sivulle Javascriptin avulla. Uusien tehtävien lisäämisen ei siis pidä aiheuttaa sivun uudelleenlatausta, vaan uusi tehtävä tulee lähettää palvelimelle Javascript-pyyntönä.
Kun saat sovelluksen toimimaan, mieti myös sen käytettävyyttä. Sovellukselle ei ole automaattisia testejä.
Tyylitiedostot
Olet ehkäpä huomannut, että tähän mennessä tekemämme web-sovellukset eivät ole kovin kaunista katsottavaa. Kurssilla pääpaino on palvelinpään toiminnallisuuden toteuttamisessa, joten emme jatkossakaan keskity sivustojen ulkoasuun. Sivujen ulkoasun muokkaaminen on kuitenkin melko suoraviivaista. Verkosta löytyy iso kasa oppaita sivun ulkoasun määrittelyyn -- tässä yksi.
Ulkoasun määrittelyssä käytetään usein apuna valmista Twitter Bootstrap -kirjastoa. Ulkoasun määrittely tapahtuu lisäämällä sivun head
-osioon oleelliset kirjastot -- tässä kirjastot haetaan https://www.bootstrapcdn.com/-palvelusta, joka tarjoaa kirjastojen ylläpito- ja latauspalvelun, jonka lisäksi elementteihin voi lisätä luokkamäärittelyjä, jotka kertovat niiden tyyleistä.
Alla on esimerkki HTML-sivusta, jossa Twitter Bootstrap on otettu käyttöön. Sivulla on lisäksi määritelty body
-elementin luokaksi (class) "container", mikä tekee sivusta päätelaitteen leveyteen reagoivan. Elementillä table
oleva luokka "table" lisää elementtiin tyylittelyn. Erilaisiin Twitter Bootstrapin tyyleihin voi tutustua tarkemmin täällä.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Blank</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css"/>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</head>
<body class="container">
<table class="table">
<tr>
<th>An</th>
<th>important</th>
<th>header</th>
</tr>
<tr>
<td>More</td>
<td>important</td>
<td>text</td>
</tr>
<tr>
<td>More</td>
<td>important</td>
<td>text</td>
</tr>
<tr>
<td>More</td>
<td>important</td>
<td>text</td>
</tr>
<tr>
<td>More</td>
<td>important</td>
<td>text</td>
</tr>
</table>
</body>
</html>
Tässä tehtävässä tavoitteena on lähinnä kokeilla sovelluksessa olevaa sivua ilman tyylitiedostoja sekä tyylitiedostojen kanssa. Käynnistä palvelin ja katso miltä juuripolussa toimiva sovellus näyttää.
Sammuta tämän jälkeen palvelin ja muokkaa sovellukseen liittyvää index.html
-tiedostoa siten, että poistat kommenttimerkit head
-elementissä olevien Twitter Bootstrap -kirjaston linkkien ympäriltä. Käynnistä tämän jälkeen palvelin uudestaan ja katso miltä sivu tämän jälkeen näyttää. Oleellista tässä on se, että sivun ulkoasun muuttamiseen tarvittiin käytännössä vain tyylitiedostojen lisääminen.
Tehtävässä ei ole testejä -- voit palauttaa sen kun olet kokeillut ylläolevaa muutosta.