Reactin perusteet

Tämä materiaali on suunnattu ensisijaisesti ammatillisen koulutuksen opiskelumateriaaliksi. Tavoitteena on, että tämän materiaalin läpikäymisen jälkeen hallitsee seuraavat asiat:

  • React-projektin luominen Vite-ympäristössä
  • Kehitysympäristön käynnistäminen
  • Yksinkertaisen komponentin lisääminen
  • Reactin JSX-merkkaus

Osallistuminen

Jos löydät tästä materiaalista virheitä tai vanhentuneita sisältöjä, niin otan siitä tiedon ilolla vastaan. Myös kehitysehdotukset ovat tervetulleita.

Lisenssi

Tämä teos on lisensoitu Creative Commons Nimeä 4.0 Kansainvälinen -lisenssillä.

React-kirjasto

React on kirjoitushetkellä laajimmin käytetty selainpohjaisten sovellusten käyttöliittymien toteuttamiseen soveltuva JavaScript-kirjasto. Kirjaston käytön oppimiselle on hyvin matala kynnys, toteutus tapahtuu hyvin pitkälle JavaScriptistä tutuilla rakenteilla.

React-kirjaston kotisivut

React-kirjaston kotisivut löytyvät osoitteesta https://react.dev/. Sivuilta löytyy mm. lyhyt tutoriaali React-kirjaston toimintaan sekä kirjaston dokumentaatio.

Projektin alustaminen

Kehitysympäristöt

React-projektin toteuttamiselle ei ole olemassa yhtä ja ainoaa oikeaa tapaa. Yksinkertaisimmillaan projektin voi toteuttaa linkittämällä React-kirjaston JavaScript-tiedostot HTML-sivulle ja kutsumalla kirjaston funktioita dokumentaation mukaisesti. Tämä menetelmä ei ole kuitenkaan sovelluskehityksen näkökulmasta tehokkain tapa, sillä muutosten päivittäminen ja sovelluksen "paketointi" toimivaksi kokonaisuudeksi vaatii erilaisia työvaiheita.

React-kirjaston ympärille on kehitetty useita erilaisia kehitystyökaluja, joilla React-projektin aloittaminen, kehitys ja julkaiseminen on helpompaa. Alla on listä kirjoitushetkellä käytetyimmistä ympäristöistä.

Vite-projektin alustaminen

Tässä materiaalissa käytetään Vite-ympäristöä, koska se on rakenteeltaan kevyt ja se ei sisällä perus-Reactin lisäksi paljoa muuta. Vite on kevyt, mutta soppivan tehokas kehitysympäristö, jonka tavoitteena on mm. tehdä kehityksen aikana tehtävät käännökset nopeasti.Vite ei ole sidottu ainoastaan React-kirjastoon, sen avulla pystyy toteuttamaan sovelluksia myös Vue-, Preact-, Lit- ja Svelte-ympäristöissä.

Projekti aloitetaan antamalla komentorivillä komento:

npm create vite@latest

Komento kysyy sinulta muutaman projektin alustamiseen liittyvän kysymyksen.

  • Syötä projektin nimeksi react101.
  • Valitse kehyskirjastoksi react.
  • Valitse variantiksi JavaScript.

Näiden kysymysten jälkeen Vite alustaa react101-kansioon uuden projektin.

Vite-projektin alustus

Seuraavaksi asennetaan ohjeistuksen mukaisesti projektin tarvitsemat paketit seuraavilla komennoilla.

cd react101
npm install

Kun kaikki paketit on asennettu, niin komentoriville tulostuu tästä ilmoitus. Tällä kertaa asennettiin 240 eri pakettia, niissä ei ollut haavoittuvuuksia asennushetkellä.

added 240 packages, and audited 241 packages in 30s

81 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Projektin avaaminen Visual Studio Codessa

Avataan projekti vielä Visual Studio Codessa seuraavalla komennolla ennen kuin käynnistetään kehitysympäristö.

code .

Tämä komento edellyttää, että koneellesi on asennattu Visual Studio Code sen oletusasetuksilla.

Komento käynnistää uuden ikkunan Visual Studio Codeen ja avaa sinne react101-kansion projektikansioksi. Voit valita Yes, I trust the authors -vaihtoehdon, kun sinulta varmistaan luotatko projektissa olevaan ohjelmakoodeihin.

Projekti avattuna Visual Studio Codessa

Kehitysympäristö käynnistäminen

Viimeiseksi käynnistetään kehitysympäristö antamalla komentoriville komento:

npm run dev

Pienen hetken päästä komentoriville tulostuu seuraavan kaltainen teksti:

  VITE v4.3.5  ready in 1866 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

Avaa komentorivillä oleva osoite (https://localhost:5173/) selaimessa, niin näet miltä projekti näyttää.

Projekti avattuna selaimessa

Muutoksen tekeminen

Kokeillaan vielä pienen muutoksen tekemistä alkuperäiseen ohjelmakoodiin, niin näemme miten kehitysympäristö toimii. Avaa src-kansion alta App.jsx-niminen tiedosto ja muokkaa rivillä 19 oleva h1-elementtirivi seuraavanlaiseksi.

      <h1>Vite + React yhteen soppii..</h1>

Tallenna tekemäsi muutokset. Muutokset päivittyvät lähes samanaikaisesti selainsivulle ilman, että joudut päivittämään selainsivua.

Muutos päivittynyt selaimeen

Voit tutustua tarkemmin alustuksen yhteydessä luotuun esimerkkipohjaan, siellä on mm. yksinkertainen esimerkki siitä, miten lasketaan kuinka monta kertaa nappia on painettu yhteensä.

Ohjelmakoodin rakenne ja sisältö voi tuntua vielä tässä vaiheessa tosi oudolta, mutta se tulee jatkossa paljon tutummaksi.

Kehitysympäristön käynnistäminen jatkossa

Projektin alustaminen tehdään luonnollisesti ainoastaan kerran. Jos haluat palata myöhemmin takaisin tämän projektin pariin, niin se onnistuu seuraavilla komennoilla.

cd react101
code .
npm run dev

React-komponentit

Aikaisemmin todettiin, että React on käyttöliittymien toteuttamiseen luotu kirjasto, sen avulla voi toteuttaa sovellusten lisäksi myös kotisivuja. Reactissa kaikki rakentuvat komponenttien ympärille. Komponenttia voidaan ajatella yksittäiseksi rakennuspalaksi, joka toteuttaa käyttöliittymästä (tai sivustosta) jonkin pienen kokonaisuuden.

Komponentteja voidaan ajatella tavallaan HTML-kielen laajennukseksi, niillä voidaan esimerkiksi paketoida jokin elementtikokonaisuus (kuten esimerkiksi avatar) yhdeksi loogiseksi kokonaisuudeksi, jota sitten käytetään sopivissa tilanteissa. Samoin kuin HTML-elementtejä voidaan sijoittaa toisten sisälle, niin React-komponenttien voidaan käyttää muita komponentteja.

Toteutetaan ensimmäinen, hyvin alkeellinen React-komponentti Avatar.

  1. Siistitään ensin projektipohjasta ylimääräiset asiat pois. Avaa src-kansiossa oleva App.jsx-tiedosto ja korvaa sen sisältö seuraavalla ohjelmakoodilla.

    import './App.css'
    
    // TODO Avatar-komponentti
    
    function App() {
      return (
        <div>
          TODO Avatar-komponentin kutsu
        </div>
      )
    }
    
    export default App
    

    Tämän muutoksen jälkeen selainikkuna on muuten tyhjä, mutta sen keskellä lukee teksti TODO Avatar-komponentin kutsu.

  2. Hae jokin sopiva profiilikuva ja tallenna se src/assets-kansioon nimellä profiili.png. Jos käyttämäsi kuvan tiedostomuoto on jokin muun kuin PNG, niin muuta tiedostopääte vastaamaan sitä.

    Tässä esimerkissä profiilikuva on haettu Open Peeps -sivustolta, josta saa vapaasti käyttöönsä huikean määrän erilaisia profiilikuvia.

    Vite-projektin alustus

    Tämä profiilikuva löytyy tästä linkistä.

  3. Sijoita seuraavaksi // TODO Avatar-komponentti -tekstin paikalle seuraava ohjelmakoodi:

    import profileimage from './assets/profiili.png';
    
    function Avatar() {
      return (
        <img className="avatar" src={profileimage} alt="" />
      );
    }
    

    import-rivi tuo ensin komponentissa käytettävän kuvan JavaScriptin puolella sellaiseksi asiaksi, että se voidaan sijoittaa suoraan img-elementin kuvaksi. Tätä tapaa käytetään silloin, kun ohjelmakoodissa liitettävä kuva on aika sama. Myöhemmin tulemme käyttämään toista menetelmää silloin, kun käytettävä kuva riippuu tilanteesta.

    Tämän jälkeen esitellään ihan tavallinen Avatar-niminen funktio, joka ei saa parametrina mitään ja joka palauttaa yksinkertaisen img-elementin. Tarkkaan ottaen return-lauseen sisällä oleva img-elementti ei ole JavaScript-koodia, vaan sen JSX-laajennusta. Huomaa, että edellä muokatun tiedoston pääte on myös jsx. JSX mahdollistaa HTML-kielen kaltaisen merkkauksen JavaScript-koodin keskellä, tähän palaamme vielä tarkemmin myöhemmin.

  4. Kutsutaan tätä komponenttia seuraavaksi App-komponentin sisällä. Muuta TODO Avatar-komponentin kutsu -tekstin paikalle seuraava koodirivi. Tallenna viimeistään tässä vaiheessa tekemäsi muutokset.

          <Avatar />
    

    Tämän seurauksena selainikkukaan ilmestyi iso profiilikuva, sillä kuvalle ei ole vielä määritelty mitään CSS-tyylejä.

    Profiilikuva selainikkunassa

  5. Lisätään seuraavaksi komponentille CSS-tyylimääritteet. Lisää src-kansion App.css-tiedoston loppuun seuraavat tyylimääritteet.

    .avatar {
      width: 8em;
      height: 8em;
      background-color: cadetblue;
      border-radius: 50%;
      object-fit: cover;
      margin: 1em;
    }
    

    Tämä määrittelee avatar-luokalle seuraavat tyyliasetukset:

    • Määritellään elementin leveydeksi ja korkeudeksi kahdeksan kirjaimen kokoa.
    • Määritellään kuvan taustaväriksi sinervän vihreä. Taustaväri näkyy silloin kun kuvassa on läpinäkyviä kohtia, kuten esimerkkikuvassa.
    • Pyöristetään reunat niin, että laatikosta tulee ympyrä. Tämä edellyttää, että kuvan leveys ja korkeus ovat samat.
    • Viimeisenä on määritys, joka sijoittaa kuvan niin, että se täyttää koko ympyrän. Tällä asetuksella saadaan kuvan kuvasuhde säilymään.Jos kuva on kuvan korkeutta kapeampi, niin kuvaa suurennetaan niin, että se täyttää koko leveyden. Tällöin kuvan ylä- ja alareunasta leikkautuu osa pois.
    • Kuvan ympärillä on yhden kirjaimen kokoinen marginaali.

    Tyylimäärittelyiden jälkeen selainikkuna näyttää seuraavanlaiselta.

    Tyylitelty profiilikuva selainikkunassa

  6. Lisätään sivulle vielä kaksi samanlaista avataria lisää. Muuta src-kansion App.jsx-tiedoston App-komponentti seuraavanlaiseksi.

    function App() {
      return (
        <div>
          <Avatar />
          <Avatar />
          <Avatar />
        </div>
      )
    }   
    

    Tämän muutoksen jälkeen sivulla on nyt kolma samanlaista avataria vierekkäin.

    Kolme profiilikuvaa

    Tässä vaiheessa emme tutustu vielä siihen, miten avatarin sisälle saadaan eri profiilikuvat. React mahdollistaa myös sen, mutta seuraavaksi tutustutaan JSX-laajennukseen.

Lemon Clicker

Tässä osiossa toteutetaan vaihe vaiheelta clicker-pelin yksinkertainen kopio. Clicker-pelien on hyvin yksinkertainen, pelissä on kuva, jota naputtamalla kerätään pelin sisäistä valuuttaa. Kerätyllä valuutalla voidaan ostaa erilaisia laajennuksia, jotka tehostavat valuutan kasvua. Lähtötilanteessa yhdellä napautuksella tulee yksi yksikkö, laajennuksen ostamalla tämä voi kasvaa esimerkiksi 0.2, 5 tai 25 yksikköä.

Tämän projektin aihepiiri pyörii perinteisen limonadin ympärillä, siitä juontuu nimi Lemon Clicker.

Tässä projektissa tullaan hyödyntämään seuraavia tekniikoita:

  • Vite-kehitysympäristöä,
  • React-kirjastoa ja
  • sopivia npm-paketteja (React Router ja React Icons).

Toteutettava sovellus tulee toimimaan sekä SPA-sovelluksena että PWA-sovelluksena.

  • SPA-sovelluksella (Single Page Application) tarkoitetaan sellaista selainpohjaista sovellusta, joka toimii yhdellä sivulatauksella. Kun käyttäjä avaa sovelluksessa uuden sivun, niin silloin selain tuottaa sivun käyttäjälle eikä sitä ladata palvelimelta.
  • PWA-sovelluksella (Progressive Web Application) tarkoitetaan sellaista sovellusta, joka osaa ladata päätelaitteelle (esim. kännykälle) kaikki tarvitsemansa tiedostot, jotta sovellusta voi tarvittaessa käyttää ilman nettiyhteyttä. Lisäksi PWA-sovellus toimii asennettuna hyvin samankaltaisesti, kuin tavallinen sovelluskaupasta ladattu sovellus.

Projektin luominen

Projektin käynnistäminen ensimmäisellä kerralla

Jokainen projekti aloitetaan sen luomisella. Ennen kuin luomme projektia, niin siirrytään ensin siihen kansioon, johon projekti luodaan. Tämä materiaali oletaan, että projekti luodaan Documents-kansion alle luotuun projektit-kansioon. Siirrytään ensin Documents-kansioon.

cd Documents

Tämä materiaali olettaa, että komentorivi käynnistyy käyttäjän profiilikansioon (kotikansioon), josta löytyy Documents-niminen kansio.

Jos sinulla ei ole Documents-kansiota, niin voit hyvin hypätä cd Documents-komennon yli.

Luodaan projektit-kansio seuraavalla komennolla.

mkdir projektit

Huomaa, että projektit-kansio tarvitsee luoda ainoastaan kerran. Jos se on jo olemassa, niin voit siirtyä seuraavaan vaiheeseen.

Siirrytään projektit-kansioon komennolla:

cd projektit

Seuraavaksi kopioimme GitHubista projektirungon tähän kansioon. Käytämme valmista pohjaa siksi, että tällä tavalla projektissa käytettävien pakettien uudet versiot eivät riko toiminnallisuutta.

Lataa projektin zip-paketti osoitteesta https://github.com/pekkatapio/lemon-clicker-pohja ja pura se projektit-kansioon.

Zip-paketin lataus löytyy Code-napin alla olevasta Download ZIP-vaihtoehdosta.

Muuta kansion nimeksi lemon-clicker komennolla.

mv lemon-clicker-pohja-main lemon-clicker

Normaalisti alustaisimme projektirungon kutsumalla Viten luontiskriptiä. Materiaalissa oleva projektirunko on alunperin luotu komennolla:

npm create vite@latest lemon-clicker -- --template react

Kopioidusta projektirungosta on muutettu seuraavat asiat alkuperäiseen verrattuna:

  • public-kansio on poistettu
  • src/assets-kansio on tyhjennetty
  • src/App.css-tiedosto on tyhjennetty
  • src/App.jsx-tiedostosta on poistettu ylimääräinen sisältö
  • src/index.css-tiedosto on poistettu
  • src/main.jsx-tiedostosta on poistettu linkitys index.css-tiedostoon
  • index.html-tiedoston title-elementti on päivitetty

Viimeisenä vaiheena on vielä npm-pakettien asentaminen, Visual Studio Coden käynnistäminen projektikansiossa ja kehitysympäristön käynnistäminen. Siirrytään ensin projektikansioon.

cd lemon-clicker

Asennetaan projektin käyttämät npm-paketit komennolla:

npm install

Tämä komento asentelee paketteja jonkin aikaa riippuen työaseman tehokkuudesta ja nettiliittymän nopeudesta. Komento tulostaa lopulta seuraavan kaltaisen tulostuksen:

added 240 packages, and audited 241 packages in 1m

81 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Jos sinulle tulee hieman eri luvut, niin siitä ei kannata säikähtää. Erityisesti viimeisellä rivillä ilmoitettujen haavoittuvuuksien määrä voi olla eri. Tämän materiaalin kirjoittamisen jälkeen asennettavista paketeista on todennäköisesti löytynyt haavoittuvuuksia.

Emme tässä vaiheessa lähde korjaamaan näitä mahdollisia haavoittuvuuksia.

Seuraavaksi käynnistämme Visual Studio Coden niin, että projektikansiona on edellä kopioimamme kansio. Tämä tapahtuu antamalla seuraava komento komentorivillä:

code .

Lopuksi voimme käynnistää kehitysympäristön komennolla:

npm run dev

Tämä komento "jumittaa" komentorivin eli kehitysympäristö jää taustalle tarkkailemaan projektikansiossa tapahtuvia muutoksia. Kun johonkin tiedostoon tulee muutos, niin kehitysympäristö tekee automaagisesti tarvittavat käännökset ja päivittää selainsivun. Komentoriville tulee seuraavankaltainen teksti:

  VITE v4.3.7  ready in 1585 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

Avaa selaimella tuo komentorivin tulosteessa mainittu osoite. Oletuksena osoite on http://localhost:5173/, jos portti ei ole varattu. Selainsivun pitäisi näyttää seuraavalta:

Projektipohja selainikkunassa

Projektin käynnistäminen myöhemmin

Seuraavilla kerroilla projektia ei tarvitse enää luoda, riittää että tarvittavat ohjelmat käynnistetään. Tämä onnistuu seuraavilla komennoilla:

cd Documents
cd projektit
code .
npm run dev

Tiedostojen tuominen ja kytkeminen

Projektin aikana tulee monesti eteen tilanteita, jossa joudutaan tuomaan tarvittavia tiedostoja muualta.

CSS-tyylitiedosto

Tämän projektin ensisijaisena tavoitteena on React-kirjaston toiminnan ymmärtäminen. Tästä johtuen emme käsittele tämän projektin aikana syvällisesti muita asioita, kuten esimerkiksi miten sovelluksen ulkoasu on toteutettu CSS-tyylien avulla. Siksi kopioimme projektin aluksi valmiit CSS-tyylimäärittelyt, jota käytämme projektin aikana.

Kopioi osoitteessa https://raw.githubusercontent.com/pekkatapio/lemon-clicker/main/src/App.css löytyvät kaikki CSS-tyylimääritteet ja liitä ne src-kansiossa olevaan App.css-tiedostoon.

Projektin CSS-tyylimääritteet

Tämä CSS-tyylitiedosto sisältää kaikki projektin aikana käytettävät CSS-tyylimääritteet. Voit vilkaista tiedoston sisällön lävitse, siellä on joukossa paljon selittäviä kommentteja. Tiedoston alussa on ensin värimäärittelyt ja yleiset sivustoa koskevat määritykset, niiden jälkeen tulee komponenttikohtaiset määritykset.

Toteuttamamme sovellus on todellisuudessa sen verran pieni, että voimme määritellä tyylimääritteet keskitetysti yhteen tiedostoon. Isommissa projekteissa tyylimääritteet kannattaa pilkkoa komponenttien yhteyteen, tämän tulet näkemään seuraavassa projektissa.

Kuvat

Projektissa tulemme käyttämään myös paljon erilaisia kuvia. Suurin osa projektissa käytettävistä kuvista ovat SVG-muotoisia. Seuraavaksi tuomme projektikansioon kuva, jota pelissä napautetaan eli yksinkertainen versio halkaistusta sitruunasta.

Kuva sijoitetaan assets-kansioon, joka on tarkoitettu kaikille projektiin tuotaville staattisille tiedostoille, kuten esimerkiksi kuville. Periaatteessa kansion nimellä ei ole merkitystä, mutta kannattaa käyttää aina niitä nimiä, jotka ovat vakiintuneet.

  1. Mene osoitteeseen https://www.svgrepo.com/svg/474522/lemon ja lataa sivulla oleva kuva SVG-muodossa.

    Lemon SVG Vector -sivu

  2. Luo src-kansioon uusi kansio ja anna sen nimeksi assets.

  3. Siirrä lataamasi SVG-kuva juuri luomaasi kansioon.

  4. Uudelleennimeä kuva nimellä lemon-big.svg.

    Kuva assets-kansiossa

Versiohallinnan alustaminen

Nyt meillä on tuotuna sekä CSS-tyylimääritteet että yksittäinen kuva. Viedään tämä alkutilanne versiohallintaan. Projektilla ei ole vielä alustettu versiohallintaa, joten teemme sen ensin. Suorita seuraavat komennot projektikansiossa.

git init
git add .
git commit -m "lisää projektirungon"

Jatkossa löydät täältä alaotsakkeesta linkin materiaalissa tehtyyn commit-taltiointiin. Linkin kautta pääset näkemään koosteen siitä, mitä muutoksia viimeisessä taltioinnissa on tehty.

Tämän sivun lisää projektirunko -taltiointi.

Funktiokomponentti

Kuten aiemminkin on jo todettu, niin Reactissa käyttöliittymä toteutetaan komponentteina. Alla on havainnekuva, jossa sovelluksen pääsivulla olevat sisällöt on pilkottu pienempiin komponentteihin.

Lemon SVG Vector -sivu

  • Header-komponentti sisältää sivun otsikon. Alasivuilla tämä näyttää hieman erilaiselta.
  • Balance-komponentti näyttää käytettävissä olevien sitruunoiden määrän. Tämä määrä kasvaa jokaisella napautuksella.
  • Lemon-komponentti sisältää napautettavan kuvan.
  • Booster näyttää yhdellä napautuksella kertyvien sitruunoiden määrän.

Seuraavaksi toteutamme projektin ensimmäisen komponentin ja otamme sen käyttöön. Lähdemme toteuttamaan sovellusta keskeisimmästä asiasta, pelin napauteltavasta sitruunan kuvasta eli Lemon-komponentista.

  1. Luo src-kansion alle uusi kansio ja anna sen nimeksi components. Tähän kansioon lisäämme projektin aikana toteutettavat komponentit.

  2. Luo edellä luotuun components-kansioon uusi Lemon.jsx -niminen tiedosto ja liitä sen sisällöksi seuraava ohjelmakoodi.

    import lemon from '../assets/lemon-big.svg'
    
    function Lemon() {
      return (
        <div className="lemon">
          <img src={lemon} alt="lemon" />
        </div>
      );
    }
    
    export default Lemon;
    

    Tämä komponentti on rakenteeltaan yksinkertaisimmasta päästä. Ensimmäisellä rivillä tuodaan (import) käytettävä kuva. Tämän jälkeen esitellään Lemon-niminen komponentti, joka palauttaa kuvan div-elementtiin käärittynä.

    Kuvat on helpointa liittää tässä esimerkissä näkyvällä tavalla, sillä kuvan sijainti tiedetään käännösvaiheessa ja osoiteviittaus kuvaan tulee siksi oikein.

    Komponentin palauttama "HTML-koodi" on ihan normaalia kolmea poikkeusta lukuunottamatta:

    • class-sana on varattu toiseen tarkoitukseen, tästä johtuen elementtien luokkamääritykset tehdään className-sanan kanssa. Muuten luokkamääritykset toimivat samalla tavalla, div-elementti kytketään lemon-nimiseen luokkaan.
    • Kuvan src-määritteen arvo on annettu aaltosulkeissa. JSX-merkkauksen sisällä JavaScript-sisältöön viitataan nimenomaan aaltosulkeiden avulla. Tässä yhteydessä lemon on koodin alussa tuotu kuva eli se on koodin näkökulmasta JavaScript-olio. Kuvan lähteeksi määritellään tämä kyseinen olio.
    • img-elementin lopussa on ns. lopettava /-merkki. JSX-merkkaus on pohjimmiltaan XML-merkkausta. XML-merkkauksessa pitää kertoa erikseen, että elementille ei ole lopetustagia sijoittamalla aloitustagin loppuun /-merkki. Tämä sama sääntö koskee kaikkia elementtejä, joista merkataan ainoastaan aloitustagi.
  3. Avaa src-kansiossa oleva App.jsx -niminen tiedosto ja tee siihen seuraavat muutokset:

    • Lisää ohjelmakoodin alkuun seuraava koodirivi:

      import Lemon from './components/Lemon';
      

      Tämä tuo edellä luomamme Lemon-komponentin niin, että voimme käyttää sitä jatkossa. Huomaa, että jatkossa käytetään import-sanan perässä olevaa nimeä. Periaatteessa tässä nimeksi voisi antaa minkä tahansa, sillä ei ole merkitystä, kunhan käyttää kutsun yhteydessä samaa määritteltyä nimeä.

    • Korvaa TODO Lemon Clicker -teksti seuraavalla koodilla:

              <Lemon />
      

      Komponenttia kutsutaan ihan samalla tavalla kuin mitä tahansa HTML-elementtiä. Huomaa tagin lopussa oleva /-merkki, tälle komponentille ei tule lopetustagia.

  4. Avaa sovellus selaimessa, sinne on nyt ilmestynyt iso sitruunan puolikas.

    Lemon-komponentti selaimessa

    Jos klikkaat tätä kuvaat, niin se pienenee ja palautuu takaisin alkuperäiseen kokoo aina kun klikkaat kuvaa. Tämä toiminnallisuus on määritelty CSS-tyylimääritteissä animaationa, joka on kytketty kuvaan silloin, kun se on aktiivinen.

    Lemon-komponentin animaatiomääritykset

Edellä on esimerkki komponentista, jonka ulkoasu on aina samanlainen. Useasti sisältö kuitenkin muuttuu jollakin tavalla, toteutetaan seuraavaksi sisällöltään muuttuva komponentti.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää Lemon-komponentin"

lisää Lemon-komponentin -commit

Tietojen välitys komponentille

Seuraavaksi tutustumme siihen, miten tietoja voidaan välittää React-komponentille ja miten välitettyjä tietoja voidaan hyödyntää.

  1. Luo components-kansioon uusi Balance.jsx -niminen tiedosto ja liitä sen sisällöksi seuraava ohjelmakoodi.

    function Balance(props) {
    
      // Poimitaan komponentille välitetty total-arvo
      const total = props.total;
    
      return (
        <div className="balance">
          <div>lemons</div>
          <div className="balance_total">{total}</div>
        </div>
      );
    
    }
    
    export default Balance;
    

    Tämä koodi on rakenteeltaan samankaltainen edellä lisätyn Lemon-komponentin kanssa. Keskeisin ero on se, että funktiolla on props-niminen parametri. Tämä on erityismuuttuja, johon React kerää kaikki komponentin kutsun yhteydessä välitetyt arvot.

    Esimerkiksi jos komponenttia kutsutaan <Balance total="42" />, niin props-muuttujasta löytyy total-niminen avain ja sen arvona teksti 42. Vastaavasti jos komponenttia kutsutaan <Balance kind="neutral">, niin props-muuttujasta löytyy kind-niminen avain arvolla neutral.

    Tätä toiminnallisuutta hyödynnetään funktiokoodissa ennen return-lausetta, jossa haetaan komponentille total-määritteellä välitetty tieto. Tämä arvo tallennetaan vakioon, joka tulostetaan return-lauseen sisällä. Muuttujan arvo tulostetaan JSX-koodissa sijoittamalla muuttujan nimi aaltosulkeiden sisälle, kuten esimerkiksi {total}.

  2. Muokkaa src-kansiossa olevaa App.jsx-tiedostoa. Lisää tiedoston alkuun seuraava ohjelmarivi.

    import Balance from './components/Balance';
    

    import-rivien järjestyksellä ei ole merkitystä, mutta nekin kannattaa järjestää jonkin logiikan perusteella. Tässä projektissa komponentit tuodaan aakkosjärjestyksessä ja niiden jälkeen muut sisällöt.

    Muuta komponentin return-lause seuraavan kaltaiseksi:

      return (
        <>
          <div>
            <Balance total="157" />
            <Lemon />
          </div>  
        </>
      )
    

    Edellä kutsutaan Balance-komponenttia ja sille välitetään kutsun yhteydessä arvo 157.

  3. Avaa sovellus selaimessa. Nyt sitruunan yläpuolelle on ilmestynyt lemons-teksti ja sen alapuolelle luku. Voit testata komponentin toimintaan vaihtamalla total-määritteen arvoa App.jsx -komponentissa.

    Balance- ja Lemon-komponentit

    Jos tutkit tarkemmin Balance-komponentin lähdekoodia, niin huomaat, että uloimmassa div-elementti on määritelty kuuluvan luokkaan balance ja sisemmässä on luokkamääritteenä balance_total. Tässä noudatetaan löyhästi BEM-nimeämiskäytäntöä, jossa nimen ensimmäinen osa määrittelee kokonaisuuden (block), toinen osa elementin kokonaisuuden sisällä (element). Nimelle on mahdollista määritellä myös kolmaskin osa (modifier), mutta tähän palaamme projektin myöhemmässä vaiheessa.

    Tässä projektissa kokonaisuus (block) on käytännössä sama kuin komponentin nimi (mutta pienillä kirjaimilla kirjoitettuna) eli Balance-komponentin luokka on balance. Komponentin sisällä olevia asioita on määritelty lisänimellä, kuten esimerkiksi balance_total. Tällä tavalla luokkatunnisteesta pystyy suoraan päättelemään, mille komponentille se kuuluu.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää Balance-komponentin"

Toteutushaaste 1

Seuraavaksi toteutamme Booster-komponentin aivan samalla periaatteella kuin edellä toteutetun Balance-komponentin.

Ennen kuin siirryt seuraavaan osioon, niin kokeile toteuttaa Booster-niminen komponentti, joka saa kutsun yhteydessä value-määritteellä arvon. Komponentin tyylimääritykset löytyvät booster-luokan alta.

Mallin Booster-luokan tulostamasta tekstistä löydät Funktiokomponentti-sivulta.


lisää Balance-komponentin -commit

Tietojen välitys komponentille 2

Seuraavaksi toteutamme Booster-nimisen komponentin, jonka rakenne on identtinen edellä lisätyn Balance-komponentin kanssa.

  1. Luo components-kansioon uusi Booster.jsx -niminen tiedosto ja liitä sen sisällöksi seuraava ohjelmakoodi.

    function Booster(props) {
    
      // Poimitaan komponentille välitetty value-arvo
      const value = props.value;
    
      return (
        <div className="booster">
          {value} lemon / click
        </div>
      );
    
    }
    
    export default Booster;
    
  2. Muokkaa src-kansiossa olevaa App.jsx-tiedostoa. Lisää tiedoston alkuun seuraava ohjelmarivi.

    import Booster from './components/Booster';
    

    Muuta komponentin return-lause seuraavan kaltaiseksi:

      return (
        <>
          <div>
            <Balance total="157" />
            <Lemon />
            <Booster value="3.2" />
          </div>  
        </>
      )
    
  3. Avaa sovellus selaimessa. Nyt sitruunan alapuolelle on ilmestynyt lemon per click -teksti ja sen edessä on luku. Voit testata komponentin toimintaan vaihtamalla value-määritteen arvoa App.jsx -komponentissa.

    Booster-komponentti lisätty

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää Booster-komponentin"

lisää Booster-komponentin -commit

Sisällön välitys

Joissain tilanteissa on tarve, että komponenttia voisi käyttää samalla tavalla kuin esimerkiksi HTML:n otsikkoelementtiä, kuten esimerkiksi:

<Header>lemon clicker</Header>

Tämä onnistuu Reactissa niille tekniikoilla, joita olemme jo aiemmin käyttäneet.

  1. Luo components-kansioon uusi Header.jsx -niminen tiedosto ja liitä sen sisällöksi seuraava ohjelmakoodi.

    function Header(props) {
    
      return (
        <div className="header">
          <h1>{props.children}</h1>
        </div>
      );
    
    }
    
    export default Header;
    

    React sijoittaa komponenttikutsun yhteydessä annetun sisällön automaattisesti children-nimiselle arvolle. Tämän sisällön voi sijoittaa ihan samalla tavalla kuin muut komponentille välitetyt arvot.

    Tämä esimerkkikomponentti poikkeaa kahdesta aikaisemmasta siinä, että tässä komponentissa välitettyä arvoa ei napattu erikseen omaan muuttujaansa vaan käytettiin suoraan props-muuttujan kautta h1-elementin sisällä. Ei ole isoa merkitystä sillä, kumpaa tapaa käytetään. Välimuuttujan käyttö lisää yhden ylimääräisen muuttujan, joka kuluttaa marginaalisesti resursseja. Tämän kaltaisen sovelluksen näkökulmasta tällä ei ole juurikaan merkitystä.

  2. Muokkaa src-kansiossa olevaa App.jsx-tiedostoa. Lisää tiedoston alkuun seuraava ohjelmarivi.

    import Header from './components/Header';
    

    Muuta komponentin return-lause seuraavan kaltaiseksi:

      return (
        <>
          <div>
            <Header>lemon clicker</Header>
            <Balance total="157" />
            <Lemon />
            <Booster value="3.2" />
          </div>  
        </>
      )
    
    1. Avaa sovellus selaimessa. Nyt aikaisempien sisältöjen yläpuolelle on ilmestynyt lemon clicker -otsikkoteksti.
    Header-komponentti lisätty

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää Header-komponentin"

lisää Header-komponentin -commit

State-tilamuuttujat

Tähän mennessä olemme toteuttaneet staattisen ulkoasun, joka ei reagoi käyttäjän toimintaan oikeastaan muuten kuin pienellä animaatiolla, kun sitruunaa napautetaan.

Jotta käyttäjän napautukset voidaan tallentaa, niin sitä varten Reactissa on oma muuttujarakenteensa, joka säilyttää muuttujan arvon renderöintien välillä. Reactissa tietoja ei pysty tallentamaan "tavalliseen" JavaScript-muuttujaan, sillä komponentteja kutsutaan aina sivun päivityssyklin yhteydessä.

Toteutetaan ensin ohjelmakoodi, joka tallentaa käyttäjän napautusten lukumäärän. Muokkaa src-kansiossa olevaa App.jsx-tiedostoa. Lisää ensin tiedoston alkuun seuraava ohjelmarivi:

import { useState } from 'react';

Tämä import-tuonti poikkeaa aikaisemmista siinä, että react-nimen edessä ei ole ./-merkkejä. Aikaisemmissa tuonti tapahtui projektin tiedostoista (siksi kansioviittaus). Tällä kerralla tuodaan toiminnallisuus npm-pakettihallinnan kautta asennetusta react-paketista, jolloin viitataan paketin nimeen.

Aaltosulkeillakin on tärkeä rooli, niiden sisällä määritellään, minkä niminen kokonaisuus react-paketista tuodaan. React-paketti sisältää lukuisan määrän erilaisia toiminnallisuuksia ja selkeyden vuoksi ohjelmakoodiin tuodaan ainoastaan ne, jotka ovat tarpeellisia. Todellisuudessa kehitysympäristö tuo automaattisesti react-paketista sinne määritellyn ns. oletuskokonaisuuden (default) ja käyttää sitä. Sitä ei tarvitse tuoda erikseen jokaiselle sivulle, sen tuonti hoidetaan automaattisesti.

useState-funktio on yksi eniten käytetyistä react-paketissa olevista toiminnallisuuksista. Se on osa Reactin koukkuja (React Hooks), joiden avulla on sovellusta on helpompi toteuttaa. useState mahdollistaa jonkin tallennetun tiedon säilyttämisen sivurenderöintien välillä. Luodaan sovellukseen uusi tilamuuttuja useStaten avulla, lisää funktion alkuun seuraava ohjelmarivi:

  // Luodaan tilamuuttuja, jossa tallennetaan napautusten määrä.
  const [clicks, setClicks] = useState(0);

Tämä luo uuden tilamuuttujan alkuarvolla 0. useState-funktio palauttaa kaksialkioisen taulukon, jossa ensimmäinen alkio on muuttuja, josta pystyy lukemaan tilamuuttujan sen hetkisen arvon. Toinen alkio on funktio, jolla tilamuuttujan arvon voi päivittää uuteen. Funtion palautusarvoina tuleva tilamuuttuja ja päivitysfunktio voidaan nimetä täysin vapaasti, mutta yleinen tapa on, että päivitysfunktion nimen alussa on set-sana ja loppuosa on tilamuuttujan nimi, kuten esimerkiksi [values, setValues] ja [visible, setVisible].

Välitetään seuraavaksi tämä tilamuuttujan arvo Balance-komponentille. Muuta return-lauseessa Balance-komponentin kutsurivi seuraavanlaiseksi:

        <Balance total={clicks} />

Nyt Balance-komponentille välitetään tilamuuttujan arvo. Voit testata tätä muuttamalla useState-kutsun yhteydessä annettua arvoa eli sulkeiden välissä olevaa lukua. Jos teet muutoksen ja tallennat, niin sitruunoiden lukumäärä muuttuu vastaavasti sovelluksessa.

State-muuttujan arvo välitetty

Lisätään seuraavaksi käsittelijä, joka suoritetaan, kun määriteltyä elementtiä napautetaan. Lisää funktion alkuun, useState-lauseen jälkeen, seuraava ohjelmakoodi:

  const handleClick = () => {
    // Kasvatetaan napautusten määrää yhdellä.
    setClicks(clicks + 1);
  }

Tämä esittelee handleClick-nimisen funktion, joka hakee tilamuuttujan clicks sen hetkisen arvon, lisää siihen yhden ja tallentaa yhteenlaskun tuloksen tilamuuttujan uudeksi arvoksi setClicks-funktiokutsulla.

Demotaan ensin käsittelijän toimintaa, lisää seuraava rivi render-lauseen sisälle, ennen Balance-komponentin kutsua.

        <button onClick={handleClick}>napauta</button>  

Nyt voit testata muuttujan toimintaa napauttamalla selaimen ruudulle ilmestynyttä napauta-nappia.

  • Kun napautat nappia kerran, kasvaa ruudulla oleva arvo yhdellä.
  • Kun päivität selainikkunan, luku nollautuu. Päivitys käynnistää sovelluksen uudelleen, jolloin tilamuuttujan arvo alustetaan.
State-muuttujan arvo päivittyy

Seuraavaksi pitäisi saada napautettava sitruunan puolikas reagoimaan napautuksiin. Tämä tapahtuu oikeastaan ihan samalla tavalla kuin edellä toteutettu testinappi. Erona on oikeastaan se, että Lemon-komponentti ei osaa automaattisesti käsitellä tapahtuvaa klikkausta, vaan se pitää toteuttaa ensin.

Muuta src/components-kansiossa olevan Lemon.jsx-komponentin ohjelmakoodi seuraavanlaiseksi:

import lemon from '../assets/lemon-big.svg'

function Lemon(props) {
  return (
    <div className="lemon">
      <img src={lemon} alt="lemon" onClick={props.onClick} />
    </div>
  );
}
  
export default Lemon;

Funktiomäärittelyyn tuli kaksi pientä muutosta. Funktio saa parametrinaan props-taulukon, jolloin päästään kiinni komponentin kutsun yhteydessä välitettyihin arvoihin. Toiseksi kuvalle lisättiin onClick-käsittelijä, joka kytketään propsien kautta välitettyyn funktioon.

Tämän jälkeen voit muokata App.jsx-tiedoston render-lausetta. Poista edellä lisätty button-testinappi ja muuta Lemon-komponentin kutsu seuraavanlaiseksi:

        <Lemon onClick={handleClick} />

Tämän muutoksen jälkeen sitruunan napauttaminen kasvattaa lukumäärää ihan samalla tavalla kuin testinapin napauttaminen aikaisemmin.

Nyt sovelluksessa on jo hyvin yksinkertainen clicker-pelin toiminnallisuus. Sitruunaa napauttamalla saa kasvatettua lukumäärää yhdellä. Jotta sitruunoiden kerääminen olisi mielekästä, niin hankituilla sitruunoilla pitäisi pystyä lunastamaan päivityksiä. Ennen kuin voimme toteuttaa sitä, on sovellukseen toteutettava navigointi.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää useState-muuttujan napautuksille"

lisää useState-muuttujan napautuksille -commit

Menu-komponentin luominen

Ennen navigoinnin lisäämistä toteutetaan ensin Menu-komponentin pohja, jonka päälle navigointi toteutetaan.

Ladataan ensin navigoinnissa käytettävät kuvakkeet:

  1. Lataa osoitteesta https://www.svgrepo.com/svg/23967/lemon löytyvä kuvake SVG-muodossa ja tallenna se src/assets-kansioon nimellä lemon.svg.
  2. Lataa osoitteesta https://www.svgrepo.com/svg/409249/package löytyvä kuvake SVG-muodossa ja tallenna se src/assets-kansioon nimellä package.svg.
  3. Lataa osoitteesta https://www.svgrepo.com/svg/509221/settings löytyvä kuvake SVG-muodossa ja tallenna se src/assets-kansioon nimellä settings.svg.

Lisää src/components-kansioon uusi Menu.jsx-niminen tiedosto ja liitä sen sisällöksi seuraava ohjelmakoodi:

import iconLemon from '../assets/lemon.svg';
import iconPackage from '../assets/package.svg';
import iconSettings from '../assets/settings.svg';

function Menu(props) {

  return (
    <div className="menu">
      <div>
        <img src={iconLemon} alt="main" />
      </div>
      <div>
        <img src={iconPackage} alt="store" />
      </div>
      <div>
        <img src={iconSettings} alt="settings" />
      </div>
    </div>
  );
  
}
  
export default Menu;

Samalla kun otetaan Menu-komponentti käyttöön, niin kääritään samalla sivun sisältöä sivun asemointia määrittelevien elementtien sisälle. Muokkaa src-kansiossa olevaa App.jsx-tiedostoa. Lisää tiedoston alkuun ensin seuraava tuontilause:

import Menu from './components/Menu';

Muuta sen jälkeen return-lause seuraavanlaiseksi:

  return (
    <div className="root">
      <div className="root_content">
        <div className="container clicker">
          <Header>lemon clicker</Header>
          <Balance total={clicks} />
          <Lemon onClick={handleClick} />
          <Booster value="3.2" />
        </div>
      </div>
      <Menu />
    </div>  
  )

Tämä muutos lisäsi useamman div-elementin, jotka toimivat sisällön kääreinä.

  • root-luokan tehtävänä on määritellä sovelluksen koko. Tyylimääritteissä on asetettu sovellukselle maksimi- ja minimileveydet, joita leveämmäksi ja kapeammaksi sovellus ei saa laajentua tai supistua. Lisäksi luokassa määritellään, että sovellus käyttää pystysuunnassa koko ikkunan korkeuden.
  • Sivun yksilöllinen sisältö kääritään root_content-luokan sisälle. Luokan tyyliasetukset huolehtivat siitä, että sisältö katkaistaan alareunasta, jos sisällön korkeus kasvaa käytettävää tila-aluetta korkeammaksi.
  • container-luokka huolehtii siitä, että sisältö sijoittuu halutulla tavalla. Lähtökohtaisesti sisältö juoksutetaan ylhäältä alaspäin.

Tämän muutoksen jälkeen selaimessa toimiva sovellus muistuttaa hyvin pitkälle alkuperäistä suunnitelmaa.

Menu-komponentti toteutettu

Sisällön ehdollinen renderöinti

Suunnitelmassa paketti-ikonin päällä oli pieni merkki, joka kertoo ostettavissa olevien lisäosien määrän. Toteutetaan tämä toiminnallisuus seuraavaksi.

Avaa src/components-kansion Menu.jsx-tiedosto ja muuta return-lause seuraavanlaiseksi:

  return (
    <div className="menu">
      <div>
        <img src={iconLemon} alt="main" />
      </div>
      <div>
        <img src={iconPackage} alt="store" />
        { props.items ? <span className="menu_badge">{props.items}</span> : null }
      </div>
      <div>
        <img src={iconSettings} alt="settings" />
      </div>
    </div>
  );

Ohjelmakoodi pysyi muuten muuttumattomana, mutta pakettikuvan alle lisättiin uusi rivi. Tuolla rivillä hyödynnetään kolmiosaista ehdollista operaattoria (?: eli ternary operator). Ensimmäisessä osassa on ehto, joka perusteella toteutuu joko toinen tai kolmas osa. Rakenteeltaan tuo rivi vastaa if-else-rakennetta, mutta kolmiosaista operaattoria käytettäessä ehdollisuus saadaan näppärästi upotettua osaksi JSX-koodia.

Kooditasolla tuolla rivillä tarkistetaan ensin, onko props.items-muuttujalla jokin arvo. Tässä on hyvä muistaa, että JavaScriptissä tyhjästä ja arvosta 0 poikkeavat arvot saavat totuusarvon tosi. Toisin sanoen, ensimmäisestä osasta tulee tosi, kun arvo on jokin muu kuin 0. Toisessa osassa oleva span-elementti renderöidään silloin, kun ensimmäisen osan tulos on tosi (eli arvo on nollasta poikkeava). Kolmannen osan null eli tyhjä renderöidään jos ensimmäisessä osan tulos oli epätosi eli arvo on nolla.

Tyylimääritteet puolestaan huolehtivat siitä, että mahdollisesti renderöitävä span-elementti näyttää halutunlaiselta ja se sijoittuu ikonin oikeaan yläkulmaan.

Tarkistetaan vielä, että tekemämme muutos Menu-komponentissa toimii halutulla tavalla. Muokkaa src-kansion App.jsx-tiedostossa Menu-komponentin kutsu seuraavanlaiseksi:

      <Menu items={2} />

Tämän muutoksen jälkeen sovelluksen menuosiossa paketti-ikonin päälle ilmestyi merkki kertomaan ostettavien lisäosien määrää.

merkki Menu-komponentin  ikonissa

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää Menu-komponentin ja kääre-elementit"

lisää Menu-komponentin ja kääre-elementit -commit

Oliomuuttujan tallennus state-tilamuuttujaan

Edellä toteutimme napautusten tallennuksen tilamuuttujaan. Pelissä joudumme tallentamaan napautusten lisäksi useampi muu asia tilamuuttujiin. Tämä onnistuu luomalla jokaiselle tallennettavalle asialle oma tilamuuttujansa, mutta jokainen niistä pitäisi erikseen välittää niitä tietoja tarvitseville alikomponenteille. Toisin sanoen ennen pitkään tilamuuttujien lisäämiselle tulee raja vastaan koodin ylläpidettävyyden näkökulmasta.

Seuraavaksi ratkaisemme tämän asian muuttamalla toteuttamamme tilamuuttujan tallentamaan yksittäisen luvun sijasta oliomuuttujan, joka voi sisältää useamman tiedon. Ensimmäisessä vaiheessa tallennamme oliomuuttujaan napautusten määrän (clicks), käytettävissä olevan saldon (balance), yksittäisellä napautuksessa kasvatettava määrä (increase) ja ostettavien lisäosien määrä (itemstobuy). Näistä arvoista ainoastaan clicks-arvoa päivitetään, muiden arvojen päivitys toteutetaan myöhemmissä vaiheissa.

Avaa src-kansiossa oleva App.jsx-tiedosto ja muuta funktion alussa esitelty useState-muuttuja seuraavanlaiseksi:

  // Luodaan tilamuuttuja, johon tallennetaan pelin laskennalliset tiedot.
  const [stats, setStats] = useState({clicks: 0, balance: 0, increase: 1, itemstobuy: 0});

Tämä rivi on rakenteeltaan samanlainen. useState-funktion palauttamat muuttuja ja asetusfunktio tallennetaan eri nimillä. Lisäksi alkuarvoksi asetetaan oliorakenne.

Oliorakenteisen tilamuuttujan päivittäminen on aavistuksen haasteellisempaa kuin tavallisen muuttujan päivittäminen. Ennen arvon päivittämistä oliosta täytyy tehdä kopio, jonka arvoja muokataan. Muokkaa handleClick-funktio seuraavanlaiseksi:

  const handleClick = () => {
    // Tehdään kopio stats-tilamuuttujasta.
    let newstats = {...stats}
    // Kasvatetaan napautusten lukumäärää yhdellä.
    newstats.clicks = newstats.clicks + 1;
    // Tallennetaan päivitetty stats-muuttuja.
    setStats(newstats); 
  }

Oliorakenteesta voi tehdä kopion useammalla eri tavalla. Tässä käytetty menetelmä soveltuu yksitasoiseen rakenteseen.

{...stats} avaa stats-oliorakenteen auki eli ottaa sen jokaisen alkion omaksi arvokseen. Lopputuloksena syntyy alkuperäisen muuttujan kopio.

Muokkaa vielä funktion return-lausen seuraavanlaiseksi:

  return (
    <div className="root">
      <div className="root_content">
        <div className="container clicker">
          <Header>lemon clicker</Header>
          <Balance total={stats.clicks} />
          <Lemon onClick={handleClick} />
          <Booster value={stats.increase} />
        </div>
      </div>
      <Menu items={stats.itemstobuy} />
    </div>
  )

Sisältö ei rakenteellisesti muuttunut mihinkään, ainoastaan komponenteille välitettävät tiedot haetaan tilamuuttujasta.

Näiden muutosten jälkeen sovelluksen toiminnassa ei tapahtunut muutosta.

Sovelluksen tiedot haetaan tilamuuttujasta

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "muuttaa state-muuttujan tallennusmuodon"

muuttaa state-muuttujan tallennusmuodon -commit

Sisällön pilkkominen omaksi komponentiksi

Tähän mennessä olemme sisällyttäneet sisällön kokonaisuudessaan App-komponentin sisälle. Jotta voimme toteuttaa navigoinnin, on pilkotaan sivun yksilöllinen sisältö omaksi komponentikseen.

Tässä vaiheessa luomme myös uuden kansion, johon sijoitamme erityisesti ne komponentit, jotka on kytketty tiettyyn osoitteeseen.

  1. Luo src-kansion alle uusi kansio ja anna sen nimeksi pages.

  2. Lisää src/pages-kansioon uusi Clicker.jsx-niminen tiedosto ja liitä sen sisällöksi seuraava ohjelmakoodi:

    import Balance from '../components/Balance';
    import Booster from '../components/Booster';
    import Header from '../components/Header';
    import Lemon from '../components/Lemon';
    
    function Clicker(props) {
      return (
        <div className="container clicker">
          <Header>lemon clicker</Header>
          <Balance total={props.stats.clicks} />
          <Lemon onClick={props.handleClick} />
          <Booster value={props.stats.increase} />
        </div>
      );
    }
    
    export default Clicker;
    

    Ohjelmakoodi on todellisuudessa karsittu versio App.jsx-tiedostosta löytyvästä koodista. Isoin ero on se, että Balance-, Lemon- ja Booster-komponenteille välitettävät arvot tulevat props-muuttujan kautta. Lisäksi import-lauseilla tuotavien komponenttien polku on korjattu.

  3. Muokkaa src-kansion App.jsx-tiedostoa seuraavasti:

    • Poista tiedoston alusta Balance-, Booster, Header- ja Lemon-komponenttien import-lauseet.
    • Lisää tiedoston alkuun seuraava import-lause:
      import Clicker from './pages/Clicker';
      
    • Muuta komponentin return-lause seuraavanlaiseksi:
        return (
          <div className="root">
            <div className="root_content">
              <Clicker stats={stats} handleClick={handleClick} />
            </div>
            <Menu items={stats.itemstobuy} />
          </div>
        )
      

Näiden muutosten jälkeen sovellus toimii edelleen samalla tavalla kuin aiemmin.

Sivun sisältö pilkottu omaan komponenttiin

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "siirtää sisällön Clicker-komponentiksi"

siirtää sisällön Clicker-komponentiksi -commit

React Router

Seuraavaksi asennamme projektiin ensimmäisen npm-paketin, jota tulemme hyödyntämään. Tämä paketti on React Router, se osaa käsitellä selaimen tekemät sivupyynnöt ja päivittämään selaimen näkymän vastaamaan selaimessa olevaa osoitetta.

React Routerin viralliset kotisivut löytyvät osoitteesta https://reactrouter.com/, samasta paikasta löytyy myös hyvin kattava dokumentaatio paketin käytöstä. React Router tarjoaa paljon erilaisia ominaisuuksia, joista tämän projektin puitteissa hyödynnämme ainoastaan pientä osaa.

Käytämme React Routerista tämän materiaalin kirjoitushetken versiota, jotta se varmasti toimii käytettävän React-version kanssa.

  1. Keskeytä komentorivillä oleva kehitysympäristö näppäinyhdistelmällä Ctrl + C.

  2. Varmista, että olet projektikansiossa ja anna komentorivillä komento:

    npm install react-router-dom@6.11.2
    

    Pienen odottelun jälkeen npm saa asennettua paketin ja kaikki siihen liittyvät alipaketit. Onnistuneen asennuksen jälkeen ruulla on seuraavat tekstit:

    added 3 packages, and audited 244 packages in 9s
    
    81 packages are looking for funding
    run `npm fund` for details
    
    found 0 vulnerabilities
    

    Viimeisellä rivillä olevien haavoittuvuuksien määrä voi olla eri, mahdollisista haavoittuvuuksista ei kannata välittää.

  3. Käynnistä kehitysympäristö uudelleen komennolla:

    npm run dev
    

Näiden vaiheiden jälkeen projektiin on asennettu React Router -paketti. Seuraavaksi hyödynnetään tätä pakettia ohjelmakoodissa.

Virhesivun lisääminen

React Router ohjaa käyttäjän tarvittaessa virhesivulle, kun esimerkiksi sivun osoitetta ei löydy. Toteutetaan tämä virhesivu ensimmäiseksi.

Luo src/pages-kansioon ErrorPage.jsx-niminen tiedosto ja laita sen silällöksi seuraava ohjelmakoodi:

import { useRouteError } from "react-router-dom";

function ErrorPage() {
  const error = useRouteError();
  return (
    <div className="root">
      <div className="root_content">
        <h1>Hupsis!</h1>
        <p>Valitettavasti tapahtui odottaman virhe.</p>
        <p>{error.statusText || error.message}</p> 
      </div>
    </div>
  );
}

export default ErrorPage;

Funktion alussa noudetaan virhetiedot useRouteError-funktion kautta ja renderöidään käyttäjälle pahoitteleva virhesivu. Virhesivulla käytetään sovelluksen ylimpien tasojen luokkamääritteitä, joilla saadaan teksti asemoitumaan samoille kohdille kuin sovelluksen sisältö.

Tämän komponentin toiminnallisuutta emme pääse testaamaan ennen, kuin olemme määritelleet sovelluksen käyttämään React Routeria reitityksessä.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää ErrorPage-komponentin"

lisää ErrorPage-komponentin -commit

Alasivujen pohjat

Seuraavaksi toteutamme alasivuille pohjat, jotta voimme kutsua niitä React Routerin kautta. Luomme sivupohjat sekä kauppasivulle että asetussivulle.

Store-komponentti

Luo src/pages-kansioon Store.jsx-niminen tiedosto ja liitä sen sisällöksi seuraava ohjelmakoodi:

import Header from '../components/Header';

function Store(props) {
  return (
    <div className="container">
      <Header balance={props.stats.balance}>store</Header>
      <div className="scrollbox items">
        TODO items
      </div>
    </div>
  );
}

export default Store;

Settings-komponentti

Luo src/pages-kansioon Settings.jsx-niminen tiedosto ja liitä sen sisällöksi seuraava ohjelmakoodi:

import Header from '../components/Header';

function Settings(props) {
  return (
    <div className="container">   
      <Header balance={props.stats.balance}>settings</Header>
      <div className="scrollbox">
        <div className="settings">
          <h2>lemon stats</h2>
          <div>
            TODO stats
          </div>
        </div>
        TODO reset
      </div>
    </div>
  );
}

export default Settings;

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää Store- ja Settings-komponentit"

lisää Store- ja Settings-komponentit -commit

React Routerin juurikomponentti

Vielä viimeiseksi, ennen kuin otamme React Routerin käyttöön, muodostamme App-komponentin sisällöstä ns. Root-komponentin, jota React Router käyttää sivurenderöintien pohjana. Root-komponentti on tarkoitettu tilanteisiin, jossa sivulla on jotain yhteistä kaikkien muiden sivujen kanssa. Sovelluksessamme yhteistä on esimerkiksi sovelluksen sijoittuminen ruudulle sekä ruutualueen jakautuminen sisältö- ja menualueeseen.

Luo src/components-kansioon Root.jsx-niminen tiedosto ja sijoita sen sisällöksi seuraava ohjelmakoodi:

import Menu from '../components/Menu'
import { Outlet } from "react-router-dom";

function Root(props) {
  return (
    <div className="root">
      <div className="root_content">
        <Outlet />
      </div>
      <Menu items={props.items}/>
    </div>
  )
}

export default Root;

Tässä ohjelmakoodissa ei ole oikeastaan mitään muuta erityistä kuin react-router-dom-paketista tuotu Outlet-komponentti. Outlet toimii ainoastaan merkkaamaan, mihin kohtaan sivujen sisältökomponentit sijoitetaan. React Router tulee sijoittamaan Clicker, Store ja Settings-komponentit Outlet-komponentin paikalle.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää Root-komponentin"

lisää Root-komponentin -commit

Reititys React Routerilla

Edellä olemme valmistelleet React Routerin käyttöönottoa luomalla lukaisan määrän erilaisia komponentteja. Tämä kaikki saattaa tuntua vielä tässä vaiheessa sekavalta, mutta seuraavissa vaiheissa palat loksahtelevat kohdilleen.

Sijoitetaan reititys omaan komponenttiinsa, tällä tavoin sovelluksen toiminta ja reititys pysyvät paremmin erossa toisistaan.

Luo src/components-kansioon AppRouter.jsx-niminen tiedosto ja sijoita sen sisällöksi seuraavat ohjelmakoodit:

  1. import { createBrowserRouter, RouterProvider } from "react-router-dom";
    

    Aivan ensimmäiseksi tuomme reitityksessä tarvittavan luontifunktion (createBrowserRouter) sekä reitityskomponentin (RouterProvider). Funktion avulla tulemme määrittelemään sovelluksen reitit. Komponentti tulee huolehtimaan reitityksestä sovelluksen suoritusaikana.

  2. import Root from "../components/Root";
    import ErrorPage from '../pages/ErrorPage';
    import Clicker from '../pages/Clicker';
    import Store from '../pages/Store';
    import Settings from '../pages/Settings';
    

    Lisäksi tuodaan myös kaikki sovelluksen reitityksessä tarvittavat komponentit. Nämä komponentit toteutimme aikaisemmissa vaiheissa.

  3. function AppRouter(props) {
    

    Aloitetaan AppRouter-komponentin määrittely tutulla tavalla. Komponentille välitetyt tiedot viedään props-muuttujaan, josta niihin pääsee käsiksi.

  4.   const router = createBrowserRouter([
        {
          path: "/",
          element: <Root items={props.stats.itemstobuy} />,
          errorElement: <ErrorPage />,
          children: [
            { path: "", element: <Clicker stats={props.stats} handleClick={props.handleClick} /> },
            { path: "store", element: <Store stats={props.stats} />},
            { path: "settings", element: <Settings stats={props.stats} />}, 
          ]
        }
      ]);
    

    Tässä taulukkorakenteessa määritellään React Routerille, miten sen tulee toimia.

    • Sovellus toimii sivuston juuressa (path).
    • Sovelluksen juurielementtinä toimii Root-komponentti, jonka sisälle upotetaan alisivujen komponentit (element).
    • Virhesivuna toimii ErrorPage-komponentti (errorElement).
    • children määrittelee kolme alasivua, joista Clicker-komponentti toimii osoitteessa /, Store-komponentti osoitteessa /store ja Settings-komponentti osoitteessa /settings.
  5.   return (
        <RouterProvider router={router} />
      );
    }
    
    export default AppRouter;
    

    Lopuksi vielä palautetaan RouterProvider, jolla on annettu parametreina edellä määritellyt reititystiedot.

Otetaan vielä AppRouter-komponentti käyttöön. Muokkaa src-kansiossa olevaa App.jsx-tiedostoa seuraavasti:

  1. Poista tiedoston alusta Clicker- ja Menu-komponenttien import-lauseet.

  2. Lisää tiedoston alkuun seuraava koodirivi, jolla tuodaan AppRouter-komponentti:

    import AppRouter from './components/AppRouter';
    
  3. Muuta komponentin return-lause seuraavanlaiseksi:

      return (
        <AppRouter stats={stats} handleClick={handleClick} />
      )
    

Näiden muutosten jälkeen selain toimii samalla tavalla kuin aikaisemmin. Sitruuna reagoi napautuksiin, alhaalla olevat menuikonit eivät reagoi toimintaan. Voit kokeilla reitittimen toimintaa kirjoittamalla selaimen osoiteriville kunkin sivun osoitteen:

Päädyt virhesivulle, jos laitat osoitteeksi sellaisen, jota ei ole määritelty. Esimerkiksi osoite http://localhost:5173/profile johtaa virhesivulle.

Seuraavaksi muokataan menualueen ikoneita niin, että ne linkittävät oikeille sivuille.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää AppRouter-komponentin ja reitityksen"

lisää AppRouter-komponentin ja reitityksen -commit

Navigointilinkkien lisääminen

Seuraavaksi lisätään sovelluksen alaosan menualueen ikoneihin linkitys. Avaa src/components-kansiossa oleva Menu.jsx-niminen tiedosto ja muokkaa sitä seuraavasti:

  1. Lisää tiedoston alkuun seuraava NavLink-komponentin tuontirivi:

    import { NavLink } from "react-router-dom";
    
  2. Kääri jokainen render-lauseen sisällä oleva ikoni NavLink-komponentilla seuraavasti:

      return (
        <div className="menu">
          <div>
            <NavLink to="/"><img src={iconLemon} alt="main" /></NavLink>
          </div>
          <div>
            <NavLink to="/store">
              <img src={iconPackage} alt="store" />
              { props.items ? <span className="menu_badge">{props.items}</span> : null }
            </NavLink>
          </div>
          <div>
            <NavLink to="/settings"><img src={iconSettings} alt="settings" /></NavLink>
          </div>
        </div>
      );
    

NavLink-komponentin käyttö hyvin samanlainen kuin esimerkiksi HTML:n a-elementin. Klikattava asia tulee komponentin sisällöksi, linkki määritellään komponentin to-määritteellä. Muuta ei tarvitse tehdä, React Router hoitaa loput.

Nyt sovellus toimii selaimessa jo niin, että sivu vaihtuu aina kun käyttäjä klikkaa sivun alareunassa olevaa ikonia. Lisäksi klikkausten määrä säilyy, vaikka välillä käytäisiin toisella sivulla.

Navigointi toimii menualueen ikoneista

Tarkkaavaisimmat huomasivat, että menualueella korostuu automaattisesti se sivu, joka on aktiivisena. Tämä on toteutettu hyödyntämällä NavLink-komponentin toimintaa. NavLink kytkee aktiivisena olevan linkin automaattisesti active-luokkaan. Tyylimääritteissä korostus toteutetaan niin, että ei aktiiviset elementit ovat osittain läpinäkyviä. Alla on katkelma näistä tyylimääritteistä.

.menu a {
  opacity: 0.5;              /* kaikki menuikonit ovat läpinäkyvyviä */
}

.menu a.active {
  opacity: 1;                /* korostetaan aktiivinen menuelementti */
}

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää linkityksen Menu-komponenttiin"

lisää linkityksen Menu-komponenttiin -commit

Header-komponentin vaihtoehtoinen sisältö

Sovelluksen navigointi toimii tällä hetkellä juuri niin, kuin sen pitäisikin. Aikaisemmin, kun toteutimme Store- ja Settings-komponentteja, niin välitimme Header-komponentille kerättyjen sitruunoiden määrän (balance).

Store-komponentissa Header-komponenttia kutsutaan seuraavasti:

      <Header balance={props.stats.balance}>store</Header>

Ja hyvin identtisesti myös Settings-komponentissa:

      <Header balance={props.stats.balance}>settings</Header>

Toteutetaan seuraavaksi Header-komponenttiin ehtorakenne, joka renderöi otsikon hieman eri tavalla, jos komponentille on välitetty balance-arvo. Avaa src/components-kansion Header.jsx-tiedosto ja tee siihen seuraavat muutokset:

  1. Tuodaan sitruunaikoni tiedoston alussa seuraavalla import-lauseella:

    import iconLemon from '../assets/lemon.svg';
    
  2. Muutetaan funktion koodi seuraavanlaiseksi:

      if (props.hasOwnProperty("balance")) {
        return (
          <div className="header header_sub">
            <h1>{props.children}</h1>
            <div>{props.balance} <img src={iconLemon}/></div>
          </div>
        );
      } else {
        return (
          <div className="header">
            <h1>{props.children}</h1>
          </div>
        );
      }
    

    Tämä ehdolllistaa renderöinnin sen perusteella, onko balance-arvo määritelty. Tarkistus tapahtuu oliomuuttujan hasOwnProperty-funktion avulla. Se palauttaa toden, jos props-muuttujassa on balance-avaimella arvo. Jos balance-arvo on määritelty, niin renderöidään otsikon lisäksi div-elementti, jossa on kassassa olevien sitruunoiden määrä. Elementin toiiseksi luokaksi määritellään header_sub, joka asemoi sisällön sekä vasempaan että oikeaan reunaan. Koodin else-osa on sama kuin aikaisemmin, jossa otsikko tulostetaan keskitettynä.

Näiden muutosten jälkeen sekä Store- että Settings-sivujen otsikko on muuttunut niin, että otsikkoteksti on pienemmällä ja oikeassa reunassa on kassassa olevien sitruunoiden määrä.

Alisivujen muokattu otsikko

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "muuttaa Header-komponentin toiminnan"

muuttaa Header-komponentin toiminnan -commit

Balance-arvon laskeminen

Tarkkaavaisimmat huomasivat, että pääsivun sitruunoiden määrä ei täsmää alisivujen sitruunoiden määrän kanssa. Tämä johtuu siitä, että etusivulla tulostetaan todellisuudessa napautusten lukumäärä, joka myöhemmissä vaiheissa ei ole sama kuin kassassa olevien sitruunoiden lukumäärä. Alisivuilla sitruunoiden määrä ei tulostu, koska balance-arvoa ei vielä lasketa. Lisätään seuraavaksi sen laskukaava ja käytetään laskettua arvoa pääsivulla.

  1. Avaa src-kansion App.jsx-tiedosto ja lisää handleClick-funktion toisiksi viimeisiksi koodiriveiksi seuraavat rivit:

        // Kasvataan sitruunoiden määrää kasvatusarvolla.
        newstats.balance = newstats.balance + newstats.increase;
    

    Huomaa, että setStats-funktion kutsu pitää jäädä viimeiseksi, sillä sen jälkeen tehdyt muutokset eivät tallennu tilamuuttujaan.

  2. Korjataan vielä pääsivulla näkymään kassasssa olevien sitruunoiden määrä napausten sijasta. Avaa src/pages-kansion Clicker.jsx-tiedosto ja muuta Balance-komponentin kutsu seuraavanlaiseksi:

          <Balance total={props.stats.balance} />
    

Näiden muutosten jälkeen balance-arvo lasketaan ja se näkyy sekä pääsivulla että alisivuilla.

Sitruunoiden määrä näkyy pääsivulla

Sitruunoiden määrä näkyy alisivulla

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää balance-arvon laskennan"

lisää balance-arvon laskennan -commit

Laskukaavat

Sovellus on nyt siinä vaiheessa, että aloitamme toteuttamaan Store-sivulle tuotelistaa. Ennen kuin toteutamme tuotalistaa sovellukseen, niin tutustutaan hieman pelissä käytettyihin laskukaavoihin. Alla olevassa taulukossa on listattu pelin seitsemän ensimmäistä tuotetta.

nronimikerroinpohjahinta
1Lemon tree0,210
2Blender270
3Carbonator10490
4Bottler503400
5Truck23524000
6Spring1150169000
7Sugar cane field56001200000
....... ...................

Tällä sivulla esitellyt tuotteet, luvut ja kaavat löytyvät kokonaisuudessaan tässä linkissä olevasta Excel-taulukosta. Osa laskentakentistä on piilotettuna, jotta taulukosta ei tule liian sekava.

Lemons per click (increase)

Joka napautuksella käytettävissä olevien sitruunoiden määrä kasvaa aina tietyn vakiomäärän verran. Pelin alussa yhdellä napautuksella saa yhden sitruunan. Jos ostaa päivityksiä, niin silloin yhdellä napautuksella saatavien sitruunoiden määrä kasvaa.

Jokainen ostettu päivitys kasvattaa kertoimen mukaisella määrällä yhdellä napautuksella saattavien sitruunoiden määrää (increase). Alkutilanteessa increase-arvo on 1.

Jos käyttäjä on ostanut yhden kappaleen ensimmäistä tuotetta (Lemon tree), niin silloin yhdellä napautuksella saatavien sitruunoiden määrä saadaan laskukaavalla:

$$ increase = 1 + 1 \times 0,2 = 1,2 $$

Vastaavasti, jos käyttäjä on ostanut kaksi kappaletta Lemon tree -tuotetta ja yhden kappalleen Blender-tuotetta, niin silloin napautuksella saatavien sitruunoiden määrä saadaan laskukaavalla:

$$ increase = 1 + 2 \times 0,2 + 1 \times 2 = 3,4 $$

Samalla logiikalla, jos käyttäjä on ostanut viisi Lemon tree -tuotetta, viisi Blender-tuotetta ja kolme Carbonator-tuotetta, niin laskukaava olisi seuraavanlainen:

$$ increase = 1 + 5 \times 0,2 + 5 \times 2 + 3 \times 10 = 42 $$

Jokaisen tuoteoston jälkeen lasketaan uusi increase-arvo edellä kuvatulla periaatteella ja tallennetaan stats-tilamuuttujan increase-avaimen arvoksi.

Taustatietoa

Tuotteiden kerroin-arvot noudattavat kahta ensimmäistä lukuunottamatta tiettyä logiikkaa. Toisesta tuotteesta eteenpäin seuraavan tuotteen kerroin on suunnilleen 5-kertainen edelliseen kertoimeen verrattuna. Tätä kerrointa pienennetään asteittain, jotta peliin tulisi lisähaastetta myöhemmissä vaiheissa. Laskukaavalla saatua tulosta on pyöristetty, jotta luvusta on saatu sopivampi.

tuotelaskukaavakerroin
Carbonator\(2 \times 4,9 = 9,8\)10
Bottler\(2 \times 5 \times 4,8 = 48\)50
Truck\(2 \times 5 \times 5 \times 4,7 = 235\)235
Spring\(2 \times 5 \times 5 \times 5 \times 4,6 = 1150\)1150
Sugar cane field\(2 \times 5 \times 5 \times 5 \times 5 \times 4,5 = 5625\)5600

Tuotteen hinta

Tuotteen hinta nousee aina 15 % jokaisen ostoksen jälkeen. Tämä hinnan nosto huolehtii siitä, että jossain vaiheessa pohjahinnaltaan kalliimpien tuotteiden hankinta on kannattavampaa.

Alla on esimerkki Lemon tree -tuotteen hintakehityksestä, tuotteen pohjahinta on 10. Laskukaavan tulos pyöristetään aina alaspäin seuraavaan kokonaislukuun.

#laskukaavahinta
1.\(10\)10
2.\(10 \times 1,15 = 11,5\)11
3.\(10 \times 1,15^2 \approx 13,23\)13
4.\(10 \times 1,15^3 \approx 15,21\)15
5.\(10 \times 1,15^4 \approx 17,49\)17
6.\(10 \times 1,15^5 \approx 20,11\)20
7.\(10 \times 1,15^6 \approx 23,13\)23
8.\(10 \times 1,15^7 \approx 26,60\)26
9.\(10 \times 1,15^8 \approx 30,59\)30
10.\(10 \times 1,15^9 \approx 35,18\)35
11.\(10 \times 1,15^{10} \approx 40,46\)40
12.\(10 \times 1,15^{11} \approx 46,52\)46
13.\(10 \times 1,15^{12} \approx 53,50\)53
14.\(10 \times 1,15^{13} \approx 61,53\)61
15.\(10 \times 1,15^{14} \approx 70,76\)70

Taustatietoa

Tuotteen 15. hinta määrittelee seuraavan tuotteen pohjahinnan. Esimerkiksi Blenderin pohjahinta on 70. Vastaavasti Blenderin 15. hinta (\(70 \times 1,15^{14} \approx 495\)) määrittelee Carbonator-tuotteen pohjahinnaksi 490. Pohjahinta pyöristetään tapauksesta riippuen joko kahden tai kolmen numeron tarkkuuteen.

Näillä edellä kuvatuilla periaatteilla pelin kannalta keskeiset arvot on laskettu. Seuraavaksi määrittelemme tuotelistan, jossa on muiden tietojen lisäksi määritelty ko. tuotteen pohjahinta ja kerroin. Näitä arvoja käytetään pelissä increase-arvon ja tuotehinnan laskentaan edellä kuvatulla tavalla.

Tuotelistan tuonti

Tuotelista on todellisuudessa taulukko, joka sisältää samanlaisen oliorakenteen listan jokaiselle tuotteelle. Esimerkiksi alla on Lemon tree -tuotteen tiedot:

  { 
    id: "lemontree",
    name: "Lemon tree",
    qty: 0,
    baseprice: 10,
    price: 10,
    multiplier: 0.2,
    image: "lemontree.svg"
  }
  • id määrittelee tuotteen yksilöllisen tunnuksen, käytetään yksilöimään mille tuotteelle toiminta kohdistuu. Tämän arvon pitää olla yksilöllinen.
  • name sisältää tuotteen nimen, joka näkyy käyttäjälle.
  • qty kertoo, kuinka monta kappaletta käyttäjä on tuotetta ostanut. Pelin alussa käyttäjä ei ole ostanut yhtään tuotetta eli lähtöarvo on 0. Tämä arvo kasvaa aina, kun käyttäjä ostaa tuotteen.
  • baseprice määrittelee tuotteen pohjahinnan, jonka perusteella tuotteen nouseva hinta lasketaan Laskukaavat-sivun periaatteiden mukaisesti.
  • price sisältää tuotteen nykyisen hinnan. Tämä arvo päivitetään aina, kun käyttäjä ostaa tuotteen. Uusi hinta lasketaan Laskukaavat-sivun periaatteiden mukaisesti. Lähtötilanteessa tuotteen nykyinen hinta on sama kuin lähtöhinta.
  • multiplier määrittelee kertoimen eli kuinka paljon yksi tuote kasvattaa yhdellä napautuksella saatavien sitruunoiden määrää.
  • image kertoo tuotekuvan nimen. Kuvan tiedostonimen alkuosa on sama kuin tuotteen id.

Tuodaan seuraavaksi koko tuotelista JavaScript-taulukkona. Ylläpidettävyyden vuoksi tallennamme taulukon omaan tiedostoonsa. Sijoitamme tämän tiedoston myös omaan kansioonsa, jonne kootaan kaikki sovelluksen asetuksiin liittyvät tiedostot.

  1. Luo src-kansion alle uusi kansio ja anna sen nimeksi config. Kuten nimikin jo viittaa, kansio on erilaisia asetustiedostoja varten.

  2. Luo src/config-kansioon uusi tiedosto, anna sen nimeksi items.js ja kopioi osoitteesta https://raw.githubusercontent.com/pekkatapio/lemon-clicker/main/src/config/items.js löytyvä ohjelmakoodi tiedoston sisällöksi.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää tuotelistataulukon"

lisää tuotelistataulukon -commit

Tuotekuvien tuonti

Seuraavaksi noudamme tuotelistan tuotteille kuvat. Tässä projektissa käytämme netissä vapaasti käytettäviä ikoneita. Jos vaihdat omaan projektiisi jonkin toisen kuvan, niin muista varmistaa, että sinulla on lupa käyttää ko. kuvaa sovelluksessasi.

  1. Viedään tuotekuvat omaan kansioonsa,joten luo src/assets-kansioon uusi kansio ja anna sen nimeksi items.

  2. Käy lataamassa alla olevan taulukon osoitteista löytyvät kuvat SVG-muodossa ja tallenna ne src/assets/items-kansioon määritellyllä nimellä.

    Esimerkiksi lataa ensimmäisessä linkissä oleva kuva SVG-muodossa, muuta tiedoston nimeksi lemontree.svg ja siirrä kuva src/assets/items-kansioon. Tee vastaavat toimenpiteet myös taulukon muille kuville.

    nimilinkkilisenssi
    lemontree.svghttps://www.svgrepo.com/svg/475465/treeCC0
    blender.svghttps://www.svgrepo.com/svg/275630/blenderCC0
    carbonator.svghttps://www.svgrepo.com/svg/203198/gas-pipe-natural-gasCC0
    bottler.svghttps://www.svgrepo.com/svg/259377/bottleCC0
    truck.svghttps://www.svgrepo.com/svg/298750/truckCC0
    spring.svghttps://www.svgrepo.com/svg/219675/bottleCC0
    sugarcane.svghttps://www.svgrepo.com/svg/211891/sugarCC0
    warehouse.svghttps://www.svgrepo.com/svg/298685/warehouseCC0
    lab.svghttps://www.svgrepo.com/svg/269831/labCC0
    secret.svghttps://www.svgrepo.com/svg/474975/safeCC0
    power.svghttps://www.svgrepo.com/svg/274799/power-plant-nuclearCC0
    park.svghttps://www.svgrepo.com/svg/231859/amusement-park-funnyCC0
    lemonai.svghttps://www.svgrepo.com/svg/246646/artificial-intelligence-brainCC0
    accelerator.svghttps://www.svgrepo.com/svg/286746/atomCC0

    Jos koneellasi toimii wget-komento, niin voit ladata kuvat suorittamalla seuraavat komennot src/assets/items-kansiossa. PowerShellissä on alias wget-komennolle, useista Linux- ja MacOS-koneista löytyy myös wget-komento asennettuna.

    wget -O lemontree.svg https://www.svgrepo.com/download/475465/tree.svg
    wget -O blender.svg https://www.svgrepo.com/download/275630/blender.svg
    wget -O carbonator.svg https://www.svgrepo.com/download/203198/gas-pipe-natural-gas.svg
    wget -O bottler.svg https://www.svgrepo.com/download/259377/bottle.svg
    wget -O truck.svg https://www.svgrepo.com/download/298750/truck.svg
    wget -O spring.svg https://www.svgrepo.com/download/219675/bottle.svg
    wget -O sugarcane.svg https://www.svgrepo.com/download/211891/sugar.svg
    wget -O warehouse.svg https://www.svgrepo.com/download/298685/warehouse.svg
    wget -O lab.svg https://www.svgrepo.com/download/269831/lab.svg
    wget -O secret.svg https://www.svgrepo.com/download/474975/safe.svg
    wget -O power.svg https://www.svgrepo.com/download/274799/power-plant-nuclear.svg
    wget -O park.svg https://www.svgrepo.com/download/231859/amusement-park-funny.svg
    wget -O lemonai.svg https://www.svgrepo.com/download/246646/artificial-intelligence-brain.svg
    wget -O accelerator.svg https://www.svgrepo.com/download/286746/atom.svg
    

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää tuotekuvat"

lisää tuotekuvat -commit

Tuotekomponentin toteuttaminen

Seuraavaksi toteutamme komponentin, joka huolehtii yksittäisen tuotteen renderöinnistä. Tämä komponentti näyttää toteutettuna seuraavanlaiselta:

Item-komponentti

Tuotelistan kuvat on sijoitettu erikseen omaan kansioonsa. Valitettavasti emme pysty viittaamaan niihin samalla tavalla kuin aikaisemmin eli tuomalla kuvan import-lauseella ja sitten sijoittamaan kuvan img-elementin src-määritteen arvoksi. Emme myöskään voi käyttää suhteellista polkua, koska polun sijainti poikkeaa kehitysversion ja käännetyn version sisällä.

Kuvapolun selvittämisessä käytämme hyödyksi Vite-dokumentaatiossa olevaa funktiota.

Item-komponentti

Luo src/components-kansioon uusi tiedosto, anna sen nimeksi Item.jsx ja liitä sen sisällöksi seuraava ohjelmakoodi:

import iconLemon from '../assets/lemon.svg';

// Funktio, joka selvittää kuvan polun suoritusaikana.
function getImageUrl(name) {  
  return new URL(`../assets/items/${name}`, import.meta.url).href;
}

function Item(props) {

  // Selvitetään kuvan url.
  const url = getImageUrl(props.item.image);

  return (
    <div className="item">
      <div className="item_icon"><img src={url} alt=""/></div>
      <div className="item_desc">
        {props.item.name}<br/>
        {props.item.price} <img src={iconLemon} alt="lemons" />
      </div>
      <div className="item_qty">{props.item.qty}</div>
    </div>
  );

}

export default Item;

Tuotelistan välitys Store-komponentille

Seuraavaksi tuomme tuotelistan sovellukseen ja välitämme sen komponenttipuuta pitkin Store-komponentille. Muokkaamme seuraavaksi tiedostoja siinä järjestyksessä, missä tuotelista kuljetetaan pääkomponentilta alemmille komponenteille.

  1. Muokkaa src-kansiossa olevaa App.jsx-tiedostoa seuraavasti:

    • Lisää tiedoston alkuun seuraava rivi, jolla tuomme tuotelistan sovelluksen käyttöön.

      import items from './config/items.js';
      
    • Lisää App-funktion alkuun seuraava ohjelmarivi:

        // Luodaan tilamuuttuja, johon tallennetaan tuotelista.
        const [storeitems,setStoreitems] = useState(items);
      

      Tuotelista tallennetaan myös tilamuuttujaan. Tässä vaiheessa emme vielä hyödynnä tilamuuttujaa, mutta tarvitsemme sitä heti, kun käsittelemme käyttäjän tekemän ostoksen.

    • Välitä tuotelista AppRouter-komponentille muuttamalla render-lause seuraavanlaiseksi:

        return (
          <AppRouter stats={stats} 
                     storeitems={storeitems} 
                     handleClick={handleClick} />
        )
      

      Komponenttikutsuun lisättiin storeitems-määrite, muuten se pysyi samanlaisena.

  2. Muokkaa src/components-kansiossa olevaa AppRouter.jsx-tiedostoa seuraavasti:

    • Muokkaa reitittimen määrityksistä Store-komponentin kutsu seuraavanlaiseksi:
              { path: "store", element: <Store stats={props.stats} storeitems={props.storeitems} />},
      

Tuotelistan renderöinti

Nyt tuotelista on kuljetettu App-komponentilta Store-komponentille. Seuraavaksi voimme toteuttaa ohjelmakoodin, joka tulostaan tuotelistan sovellusikkunaan.

Muokkaa src/pages-kansiossa olevaa Store.jsx-tiedostoa seuraavasti:

  • Lisää tiedoston alkuun seuraava import-lause:

    import Item from '../components/Item';
    
  • Lisää funktion sisälle, ennen return-lausetta seuraava koodirivi:

      // Muodostetaan renderöitävä tuotelista.
      const items = props.storeitems.map(item => <Item key={item.id} item={item} />);
    

    Tämä koodirivi hyödyntää JavaScriptin taulukoihin sisäänrakennettua map-funktiota, joka käy taulukon alkiot yksi kerrallaan lävitse, suorittaa sille toimenpiteen ja koostaa tuloksen uudeksi taulukoksi. Tässä tilanteessa tuotelistan listan tuotteista muodostetaan yksittäisiä Item-komponentin renderöintejä ja koostetaan ne taulukkoon.

  • Muuta return-lausessa TODO items -rivin paikalle seuraava rivi:

            {items}
    

    Tämä sijoittaa edellä map-funktiossa koostetun taulukon div-elementin sisälle.

Näiden muutosten jälkeen Store-sivulle ilmestyi tuotelista.

Tuotelista Store-sivulla

Tuotelistan tuotteita ei pysty vielä ostamaan, sen toiminnallisuuden toteutamme myöhemmin. Ennen ostotoiminnallisuuden toteuttamista muutamme sovelluksen tulostamia lukuja luettavampaan muotoon.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää tuotelistan"

lisää tuotelistan -commit

Lukujen sieventäminen

Tuotelistan loppupäässä hinnat ovat isoja lukuja, joiden kokoluokkaa on aika vaikea hahmottaa. Lukujen lukemisen helpottamiseksi toteutamme seuraavaksi funktion, joka sieventää lukuja luettavampaan muotoon. Esimerkiksi luku 20800000000 tulostetaan muodossa 20.80 Billion.

  1. Luo src-kansioon uusi kansio ja anna sen nimeksi utils. Tähän kansioon kootaan jatkossa erilaisia apufunktioita.

  2. Luo src/utils-kansioon uusi tiedosto, anna sen nimeksi shortenNumber.js ja laita sen sisällöksi seuraavat ohjelmarivit:

    const numberMillion = Math.pow(10,6);
    const numberBillion = Math.pow(10,9);
    const numberTrillion = Math.pow(10,12);
    const numberQuadrillion = Math.pow(10,15);
    
    function shortenNumber(number) {
      if (number > numberQuadrillion) {
        return (number / numberQuadrillion).toFixed(2) + " Lemollion";
      } else if (number > numberTrillion) {
        return (number / numberTrillion).toFixed(2) + " Trillion";
      } else if (number > numberBillion) {
        return (number / numberBillion).toFixed(2) + " Billion";
      } else if (number > numberMillion) {
        return (number / numberMillion).toFixed(2) + " Million";
      } else {
        return number;
      }
    }
    
    export default shortenNumber;
    

    Funktion toiminta on suoraviivainen, se saa parametrinaan sievennättävän luvun. Ehtolauseella tutkitaan mitä kokoluokkaa luku on ja sen perusteella luku muutetaan miljooniksi, miljardeiksi ja niin edelleen. Samassa yhteydessä luku pyöristetään kahden desimaalin tarkkuuteen.

  3. Avaa src/components-kansiossa oleva Item.jsx-tiedosto ja tee siihen seuraavat muutokset:

    • Lisää tiedoston alkuun seuraava import-rivi:

      import shortenNumber from '../utils/shortenNumber';
      
    • Muuta return-lauseessa {props.item.price}-tekstin sisältävä rivi muotoon:

              {shortenNumber(props.item.price)} <img src={iconLemon} alt="lemons" />
      

Näiden lisäysten jälkeen Store-sivun hinnat ovat luettavammassa muodossa.

Store-sivun hinnat luettavammassa muodossa

Olemme tulostaneet sievennettäviä lukuja jo aiemmin, mutta niiden arvot ovat olleet toistaiseksi niin pieniä, että luettavuuden ongelma ei ole tullut vielä vastaan. Lisätään seuraavaksi sievennys myös balance-arvon tulostukseen Balance-, Booster- ja Header-komponentteihin.y

Harjoitus

Ennen kuin jatkat eteenpäin, niin käy lisäämässä luvun sievennys Balance-, Booster- ja Header-komponentteihin.

Jokaiseen kolmeen komponenttiin tehtävät muutokset ovat periaatteeltaan samat kuin edellä Item-komponenttiin tekemämme muutokset (kohta 3).

Kun olet tehnyt lisäyksesi, niin vertaa niitä alla olevaan vaiheisiin.

  1. Avaa src/components-kansiossa oleva Balance.jsx-tiedosto ja muokkaa sitä seuraavasti:

    • Lisää tiedoston alkuun seuraava import-tuonti:
      import shortenNumber from '../utils/shortenNumber';
      
    • Muuta ennen return-lausetta oleva total-arvon poiminta seuraavaksi:
        const total = shortenNumber(props.total);
      

      Vaihtoehtoisesti shortenNumber-funktion kutsun voi sijoittaa myös return-lauseen sisällä total-arvon tulostuksen yhteyteen. Tällöin {total} muuttuisi muotoon {shortenNumber(total)}.

  2. Avaa src/components-kansiossa oleva Booster.jsx-tiedosto ja muokkaa sitä seuraavasti:

    • Lisää tiedoston alkuun seuraava import-tuonti:
      import shortenNumber from '../utils/shortenNumber';
      
    • Muuta ennen return-lausetta oleva value-arvon poiminta seuraavaksi:
        const value = shortenNumber(props.value);
      

      Vaihtoehtoisesti shortenNumber-funktion kutsun voi sijoittaa myös return-lauseen sisällä value-arvon tulostuksen yhteyteen. Tällöin {value} muuttuisi muotoon {shortenNumber(value)}.

  3. Avaa src/components-kansiossa oleva Header.jsx-tiedosto ja muokkaa sitä seuraavasti:

    • Lisää tiedoston alkuun seuraava import-tuonti:
      import shortenNumber from '../utils/shortenNumber';
      
    • Muuta ehtolauseen ensimmäisestä osasta balance-arvon tulostus muuttamalla {props.balance} muotoon {shortenNumber(props.balance)}.

Näiden muutosten jälkeen sovelluksen tähän mennessä tulostamat luvut sievennetään, vaikka muutosten vaikutus ei vielä tässä vaiheessa tule esille.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää lukujen sievennyksen"

lisää lukujen sievennyksen -commit

Ostotapahtuman käsittely

Sovellus listaa nyt kaikki tuotteet. Seuraavaksi toteutamme toiminnallisuuden, joka käsittelee tuotteen oston. Ensin toteutetaan otsotapahtuman käsittelevä koodi, sen jälkeen funktio välitetään komponenttipuuta pitkin Item-komponentille.

  1. Muokkaa src-kansion App.jsx-tiedostoa seuraavasti:

    • Lisää ennen return-lausetta seuraava handlePurchase-funktion esittelevä ohjelmakoodi:

        const handlePurchase = (id) => {
          // Etsitään tunnistetta vastaavan tuotteen indeksi taulukosta.
          const index = storeitems.findIndex(storeitem => storeitem.id == id);
          // Varmistetaan, että käyttäjällä on varaa ostaa tuote.
          if (stats.balance >= storeitems[index].price) {
            // Tehdään kopiot tilamuuttujista.
            let newstoreitems = [...storeitems];
            let newstats = {...stats};
            // Kasvatetaan tuotteiden määrää yhdellä.
            newstoreitems[index].qty++;
            // Vähännetään varoista tuotteen hinta.
            newstats.balance = newstats.balance - newstoreitems[index].price;
            // TODO Uusi tuotehinta
            // Tallennetaan uudet tilamuuttujien arviot.
            setStoreitems(newstoreitems);
            setStats(newstats);
          }
        }
      

      Funktion toiminta on aika suoraviivainen. Funktio saa kutsun yhteydessä tuotteen id-tunnisteen, jonka perusteella etsitään oikea alkio taulukosta. Ennen ostotapahtuman suorittamista tarkistetaan, että käyttäjällä on riittävästi saldoa tuotteen ostoon. Jos saldoa on riittävästi, niin sitten suoritetaan itse ostotapahtuma. Ensin kummastakin tilafunktiosta tehdään kopio, sillä kummankin sisältöön tehdään muutoksia. Seuraavaksi kasvatetaan käyttäjän omistamien tuotteiden lukumäärää yhdellä ja vähennetään käyttäjän saldosta tuotteen hinta. Lopuksi tallennetaan päivitetyt tilamuuttujien arvot talteen.

    • Välitä handlePurchase-funktio AppRouter-komponentille muuttamalla AppRouter-kutsu seuraavanlaiseksi:

          <AppRouter stats={stats} 
                     storeitems={storeitems} 
                     handleClick={handleClick} 
                     handlePurchase={handlePurchase} />
      
      
  2. Muokkaa src/components-kansion AppRouter.jsx-tiedostoa seuraavasti:

    • Muuta reititystaulukon store-polun määritys seuraavanlaiseksi:
      { path: "store", element: <Store stats={props.stats}
                                       storeitems={props.storeitems}
                                       handlePurchase={props.handlePurchase} />},
      
      Tämä muutos välittää App-komponentilta tulleen funktion eteenpäin Store-komponentille.
  3. Muokkaa src/pages-kansion Store.jsx-tiedostoa muuttamalla ennen return-lausetta sijaitseva map-funktiokutsu seuraavanlaiseksi:

      // Muodostetaan renderöitävä tuotelista.
      const items = props.storeitems.map(item => (
        <Item key={item.id}
              item={item}
              handlePurchase={props.handlePurchase} />
      ));
    

    Tämä välittää handlePurchase-funktion eteenpäin jokaiselle Item-komponentikutsulle.

  4. Muokkaa src/components-kansion Item.jsx-tiedoston return-lauseen ensimmäinen div-aloitustagi seuraavanlaiseksi:

        <div className="item"
             onClick={()=>{props.handlePurchase(props.item.id)}}>
    

    Tämä lisää koko tuotteen sisältävään laatikkoelementtiin käsittelijän, joka käynnistyy laatikkoa klikattaessa. Klikattaessa kutsutaan pääkomponentilta välitettyä handlePurchase-funktiota, jolla annetaan klikatun tuotteen id kutsun yhteydessä parametrina.

    Huomaa, että onClick-määritteen arvo tulee olla funktio, joka suoritetaan klikattaessa. Koska kutsun yhteydessä annetaan myös tuotteen id parametrina, on funktiokutsu käärittävä nimettömäksi funktioksi, jotta handlePurchase-funktiota ei kutsuta renderöinnin yhteydessä.

Näiden lisäysten jälkeen voit ostaa tuotteen, jos sinulla on siihen varallisuutta.

Tuotteiden ostaminen toteutettu

Tuotteen hinta ei vielä kasva, kun tuotetta ostetaan. Myöskään yhdellä napautuksella saatavien sitruunoiden määrä ei muutu, vaikka ostaisit kuinka monta tuotetta. Nämä toiminnallisuudet toteutetaan seuraavaksi.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää tuotteen ostokäsittelijän"

lisää tuotteen ostokäsittelijän -commit

Uuden tuotehinnan laskeminen

Seuraavaksi toteutamme toiminnallisuuden, joka laskee tuotteelle uuden hinnan, kun käyttäjä ostaa tuotteen.

Tuotteen uusi hinta saadaan Laskukaavat-sivun periaatteiden mukaisesti kaavalla:

$$ hinta = floor(pohjahinta \times 1,15^{määrä}),$$

missä määrä on ostettujen tuotteiden lukumäärä.

Muokkaa src-kansion App.jsx-tiedostossa olevaa handlePurchase-funktiota. Korvaa // TODO Uusi tuotehinta-rivi seuraavilla riveillä:

      // Lasketaan tuotteen uusi hinta.
      newstoreitems[index].price =
        Math.floor(newstoreitems[index].baseprice * Math.pow(1.15,newstoreitems[index].qty));
      // TODO lasketaan uusi kasvatusarvo

Tämän muutoksen jälkeen tuotesivulla lasketaan tuotteelle uusi hinta aina, kun käyttäjä ostaa tuotteen.

Tuotteelle lasketaan uusi hinta oston yhteydessä

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää uuden tuotehinnan laskennan"

lisää uuden tuotehinnan laskennan -commit

Kasvatusarvon laskeminen

Tuoteoston yhteydessä yhdellä napautuksella saatavien sitruunoiden määrä muuttuu. Seuraavaksi toteutamme koodin, joka laskee uuden kasvatusarvon. Uusi kasvatusarvo lasketaan Laskukaavat-sivun periaatteiden mukaisesti. Samalla laskemme ostettujen tuotteiden yhteislukumäärän.

Muokkaa src-kansion App.jsx-tiedostossa olevaa handlePurchase-funktiota. Korvaa // TODO lasketaan uusi kasvatusarvo-rivi seuraavilla riveillä:

      // Koostemuuttujien esittely.
      let increase = 1;
      let upgrades = 0;
      // Käydään tuotteet yksitellen lävitse.
      for (let i=0; i<storeitems.length; i++) {
        // Lisätään tuotteiden määrä kokonaismäärään.
        upgrades = upgrades + storeitems[i].qty;
        // Lisätään tuotteen vaikutus kasvatusarvoon.
        increase = increase + storeitems[i].multiplier*storeitems[i].qty;
      }
      // Tallennetaan lasketut koostearvot.
      newstats.increase = increase;
      newstats.upgrades = upgrades;

Tämän lisäyksen jälkeen sovellus laskee uuden kasvastusarvon tuotteen oston yhteydessä.

Yhdellä napautuksella saatavien sitruunoiden määrä lasketaan ostosten perusteella

Joissain tilanteissa sitruunoiden lukumäärä saattaa pitkänä desimaaliarvona. Tämä johtuu siitä, että kasvatusarvo on tietyissä tilanteissa liukuluku, joka on JavaScriptin näkökulmasta likiarvo. Jotta sovellus ei näytä pitkää desimaalimuotoa, on luvut pyöristettävä.

Lukujen pyöristäminen

JavaScript ei sisällä valmista funktiota, jolla luvun voisi pyöristää tiettyyn desimaalimäärään. Toteutamme ensin yksinkertaisen funktion, joka suorittaa tämän pyöristämisen.

Luo src/utils-kansioon uusi tiedosto, anna sen nimeksi round.js ja laita sen sisällöksi seuraava ohjelmakoodi:

// Pyöristää luvun määriteltyyn tarkkuuteen.
function round(value, precision) {
  const multiplier = Math.pow(10, precision || 0); 
  return Math.round(value * multiplier) / multiplier;
}

export default round;

Muokkaa src-kansion App.jsx-tiedostoa seuraavasti:

  • Lisää tiedoston alkuun seuraava import-tuonti:

    import round from './utils/round';
    
  • Muokkaa handleClick-funktion sisällä oleva balance-arvon laskenta seuraavanlaiseksi:

        newstats.balance = round(newstats.balance + newstats.increase,1);
    
  • Muokkaa handlePurchase-funktion sisällä oleva balance-arvon laskenta seuraavanlaiseksi:

          newstats.balance = round(newstats.balance - newstoreitems[index].price,1);
    

Nämä lisäykset pyöristävät lasketut balance-arvot yhden desimaalin tarkkuuteen.

Lukujen pyöristys toteutettu

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää kasvatusarvon laskennan ja  pyöristyksen"

lisää kasvatusarvon laskennan ja pyöristyksen -commit

Tuotteen ostettavuuden ilmaiseminen

Tuotelistassa ei tällä hetkellä näy se, että onko käyttäjällä varaa ostaa tuotetta vai ei. Ainoa tapa tietää se, on verrata tuotteen hintaa ja saldoa keskenään. Seuraavaksi lisäämme toiminnallisuuden, joka häivyttää ne tuotteet, joita käyttäjällä ei ole varaa ostaa.

  1. Tämä toiminnallisuus toteutetaan ensin Item-komponentille. Muokkaa src/components-kansion Item.jsx-tiedoston return-lauseen ensimmäinen div-aloitustagi seuraavanlaiseksi:

        <div className={ props.disabled ? "item item-disabled" : "item" }
             onClick={()=>{props.handlePurchase(props.item.id)}}>
    

    Tämä lisää div-elementin luokkamääritteeseen ehdollisena item-disabled-luokan, jos komponentin kutsun yhteydessä on määritelty disabled -määritteelle arvo (eli se tulkitaan todeksi).

  2. Muokkaa src/pages-kansion Store.jsx-tiedostoa muuttamalla ennen return-lausetta sijaitseva map-funktiokutsu seuraavanlaiseksi:

      // Muodostetaan renderöitävä tuotelista.
      const items = props.storeitems.map(item => (
        <Item key={item.id}
              item={item}
              handlePurchase={props.handlePurchase} 
              disabled={props.stats.balance < item.price} />
      ));
    

    Tämä määrittelee Item-komponentin disabled-arvon todeksi, jos saldo on pienempi kuin tuotteen hinta.

Nyt tuotelistalla on häivytettynä ne tuotteet, joita käyttäjällä ei ole varaa ostaa.

Tuotelistalla häivytetty ne tuotteet, joita ei ole varaa ostaa

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää tuotteen ostettavuuden ilmaisemisun"

lisää tuotteen ostettavuuden ilmaisun -commit

Hankittavissa olevat tuotteet

Tuotelista sisältää kaikki tuotteet, vaikka loppupään tuotteita ei ole mahdollista hankkia vielä pitkään aikaan. Tuotelista olisi toimivampi, jos listalla näkyisi aina seuraava uusi hankittava tuote, kun käyttäjä on ostanut edellisestä vähintään yhden kappaleen. Toteutetaan seuraavaksi tämä toiminnallisuus.

Luo src/utils-kansioon uusi tiedosto, anna sen nimeksi getPurchasableItems.js ja sijoita sen sisällöksi seuraava ohjelmakoodi:

const getPurchasableItems = (items) => {
  // Taulukko, johon ostettavissa olevat tuotteet kootaan.
  let purchasableitems = [];
  // Lähtökohtaisesti kaikkia ostettvia tuotteita ei ole listattu.
  let allItemsListed = false;
  // Käydään tuotteet yksitellen lävitse.
  items.forEach(item => {
    // Tarkista, onko ostettavat tuotteet listattu.
    if (!allItemsListed) {
      // Ei ole, lisätään tuote listaan.  
      purchasableitems.push(item);
      // Jos tuotteen määrä on nolla, niin ei listata enempää tuotteita. 
      if (item.qty == 0) {
        allItemsListed = true;
      }
    }
  });
  // Palautetaan koostettu taulukko.
  return purchasableitems;
}

export default getPurchasableItems;

Tämä funktio käy tuotelistan lävitse ja poimii palautettavaan listaan mukaan ne tuotteet, jotka käyttäjä on ostanut ja niiden lisäksi seuraava, jota ei ole vielä hankittu.

Huomaa, että voimme olettaa, että listan välissä ei ole ostamattomia tuotteita, koska käyttäjälle tarjotaan seuraavaa tuotetta vasta, kun hän on ostanut edellisen.

Hyödynnetään seuraavaksi tätä funktiota. Muokkaa src/pages-kansiossa olevaa Store.jsx-tiedostoa seuraavasti:

  • Lisää tiedoston alkuun seuraava import-tuonti:

    import getPurchasableItems from '../utils/getPurchasableItems';
    
  • Muuta ennen return-lausetta sijaitseva map-funktiokutsu seuraavanlaiseksi:

      // Muodostetaan renderöitävä tuotelista.
      const items = getPurchasableItems(props.storeitems).map(item => (
        <Item key={item.id}
              item={item}
              handlePurchase={props.handlePurchase} 
              disabled={props.stats.balance < item.price} />
      ));
    

    Koodirivi muuttui niin, että kutsun yhteydessä tullut storeitems-lista ajetaan getPurchasableItems-funktion kautta, jolloin tuotelistalle renderöidään ainoastaan ne tuotteet, jotka on hankittavissa.

Tuotelistalla näkyy hankittavissa olevat tuotteet

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää hankittavien tuotteiden suodatuksen"

lisää hankittavien tuotteiden suodatuksen -commit

Ostettavissa olevien tuotteiden määrä

Kaupan toiminnallisuudesta puuttuu enää tieto ostettavissa olevien tuotteiden lukumäärästä. Käyttäjä näkee Store-sivulla ostettavien tuotteiden määrän menupalkin ikonin oikeasta yläkulmasta olevasta lukemasta. Näin käyttäjän ei tarvitse turhaan liikkua pääsivun ja Store-sivun väliä tarkistamassa, että joko hänellä riittää saldo tuotteen ostamiseen.

Muokkaa src-kansiossa olevaa App.jsx-tiedostoa seuraavasti:

  • Lisää tiedoston alkuun seuraava import-lause:

    import getPurchasableItems from './utils/getPurchasableItems';
    
  • Lisää App-funktion sisälle seuraava funktiomäärittely:

      // Laskee niiden tuotteiden lukumäärän, joiden ostamiseen on varaa.
      const countBuyableItems = (items, balance) => {
        let total = 0;
        getPurchasableItems(items).forEach(item => {
          if (item.price <= balance) total++;
        });
        return total;
      }
    

    Tämä funktio käy tuotelistan tuotteen lävitse ja tutkii, onko tuotteen hinta pienempi kuin saldo. Jos on, niin tuote on ostettavissa.

Olemme jo toteuttaneet aikaisemmin ostettavien tuotteiden lukumäärän tulostuksen. Nyt riittää, että tallennamme lukumäärän stats-tilamuuttujaan itemstobuy-avaimen arvoksi. Tämä päivitys täytyy tehdä sekä handleClick- että handlePurchase-funktioissa.

  1. Lisää seuraavat ohjelmarivit handleClick-funktion loppupuolelle balance-arvon päivityksen ja setStats-funktiokutsun väliin.

      // Lasketaan ostettavissa olevien tuotteiden lukumäärä.
      newstats.itemstobuy = countBuyableItems(storeitems,newstats.balance);
    
  2. Lisää seuraavat ohjelmarivit handlePurchase-funktion loppuosaan juuri ennen setStoreItems- ja setStats-funktiokutsuja.

          // Lasketaan ostettavissa olevien tuotteiden lukumäärä.
          newstats.itemstobuy = countBuyableItems(newstoreitems,newstats.balance);
    

    Huomaa, että tuotelistaa on muokattu aiemmin, joten tässä yhteydessä välitämme muokatun tuotelistan countBuyableItems-funktiolle.

Nyt menualueen Store-kuvakkeen päällä näkyy ostettavissa olevien tuotteiden lukumäärä. Tämä lukumäärä päivittyy jokaisella napautuksella ja tuoteostoksella.

Menualueen Store-kuvakkeen päällä näkyy ostettavissa olevien tuotteiden lukumäärä

Lisätietoa

Toteuttamamme tapa käy tuotetaulukon lävitse joka kerta, kun käyttäjä napauttaa sitruunan kuvaa tai ostaa tuotteen. Todellisuudessa tätä läpikäyntiä pystyy vähentämään merkittävästi sillä tavalla, että muuttujaan tallennetaan se arvo, jonka jälkeen lukumäärä on tarve laskea uudelleen.

Esimerkiksi kuvan esimerkkitilanteessa seuraava raja-arvo on 70 (seuraavaksi ostettavissa olevan tuotteen hinta). Niin kauan kun saldo on alle tuon raja-arvon, ostettavissa olevien tuotteiden lukumäärää ei tarvitse laskea uudelleen.

Jos käyttäjä ostaa tuotteen, niin silloin sekä raja-arvo että lukumäärä selvitetään.

Nyt ostotoiminnallisuus on kokonaisuudessaan toteutettu. Seuraavaksi siirrymme toteuttamaan Settings-sivun toiminnallisuuksia.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää ostettavien tuotteiden laskennan"

lisää ostettavien tuotteiden laskennan -commit

Tilastotietojen tulostaminen

Seuraavana vaiheena on toteuttaa Settings-sivulle tilastotietojen tulostus. Ensin tulostetaan tilastotiedot niistä tiedoista, jotka on jo laskettu.

  1. Luo src/components-kansioon uusi tiedosto, anna sen nimeksi Stat.jsx ja liitä sen sisällöksi seuraavat rivit:

    import shortenNumber from "../utils/shortenNumber";
    
    function Stat(props) {
      return (
        <div className="stat">
          <h3>{props.title}</h3>
          <p>{shortenNumber(props.value)}</p>
        </div>
      )
    }
    
    export default Stat;
    
  2. Muokkaa src/pages-kansiossa olevaa Settings.jsx-tiedostoa seuraavasti:

    • Lisää tiedoston alkuun seuraava import-rivi:

      import Stat from '../components/Stat';
      
    • Korvaa return-lauseen sisältä TODO stats -teksti seuraavilla riveillä:

                  <Stat title="in bank" value={props.stats.balance} />
                  <Stat title="per click" value={props.stats.increase} />
                  <Stat title="clicks" value={props.stats.clicks} />
                  <Stat title="upgrades" value={props.stats.upgrades} />
      

Nyt Settings-sivulla näkyy neljä tilastolaatikkoa.

Settings-sivun tilastolaatikot

Viimeisessä ei ole lukua ennen kuin käyttäjä on ostanut ensimmäisen tuotteen. Tämä johtuu siitä, että upgrades-kentälle ei määritellä arvoa tilamuuttujan alustuksessa. Korjataan tämä ja lisätään samalla yhden tilastoarvon laskenta.

  1. Muokkaa src-kansion App.jsx-tiedostoa seuraavasti:

    • Muuta App-funktion alussa oleva stats-tilamuuttujan esittely seuraavanlaiseksi:

        // Esitellään pelin laskennalliset alkuarvot.
        const initialstats = {
          clicks: 0,
          balance: 0,
          increase: 1,
          itemstobuy: 0,
          upgrades: 0,
          collected: 0
        }
      
        // Luodaan tilamuuttuja, johon tallennetaan pelin laskennalliset tiedot.
        const [stats, setStats] = useState(initialstats);
      

      Toteutuksessa tulee helposti eteen tilanne, että muuttujan alkuarvo kannattaa esitellä omana vakionaan, jotta alkuarvo olisi helpompi käyttää. Tässä yhteydessä erotettiin alkuarvo omaksi vakiomuuttujaksi, jota käytämme tilamuuttujan alustuksen myös myöhemmin.

    • Lisää seuraava koodirivi handleClick-funktion sisälle ennen setStats-funktion kutsua:

          // Kasvatetaan sitruunoiden keräysmäärää.
          newstats.collected = round(newstats.collected + newstats.increase,1);
      

      Tämä tekee saman kasvatuksen, mitä tehdään balance-arvolle. Tarvitsemme tilastoinnin kannalta kaksi eri arvoa, sillä toinen kertoo käytettävissä olevien sitruunoiden määrän (balance) ja toinen kertoo koko pelin aikana poimittujen sitruunoiden määrän (collected).

  2. Muokkaa src/pages-kansion Settings.jsx-tiedoston return-lausetta niin, että lisäät seuraavat rivit increase- ja clicks-tulostusten väliin.

                <Stat title="collected" value={props.stats.collected} />
    

Näiden lisäysten jälkeen upgrades-arvo tulostuu alkutilanteessa ja sen lisäksi tulostetaan collected-arvo, jonka laskenta lisättiin.

Settings-sivun päivitetyt tilastolaatikot

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää tilastoarvojen tulostuksen"

lisää tilastoarvojen tulostuksen -commit

Pelin suoritustietojen poistaminen

Pelistä on toteutettu kaikki ne toiminnallisuudet, jotka tarvitaan pelin pelaamiseen. Joskus käyttäjä haluaa aloittaa pelaamisen alusta, joten toteutamme sitä varten toiminnallisuuden.

Huomaa, että tässä vaiheessa suoritustietojen poistaminen tuntuu epämielekkäältä, koska suoritustiedot katoavat kuitenkin sivupäivityksen myötä. Lisäämme tämän jälkeen toiminnallisuuden, jolla sovellus muistaa käyttäjän pelitilanteen sivupäivityksestä huolimatta.

Reset-komponentin luonti

Luo src/components-kansioon uusi tiedosto, anna sen nimeksi Reset.jsx ja liitä sen sisällöksi seuraavat ohjelmarivit:

function Reset(props) {
  return (
    <div className="reset">
      <button>Poista suoritustiedot</button>
    </div>
  );
}

export default Reset;

Edellä oleva komponentti ei sisällä vielä toiminnallisuutta, se renderöi ainoastaan napin. Kutsutaan tätä komponenttia seuraavaksi Settings-sivulla. Muokkaa src/pages-kansion Settings.jsx-tiedostoa seuraavasti:

  • Lisää tiedoston alkuu seuraava import-rivi:

    import Reset from '../components/Reset';
    
  • Korvaa return-lauseen sisältä TODO reset-rivi seuraavalla rivillä:

            <Reset />
    

Settings-sivulle tuli tilastotietojen alle punainen nappi, jossa on tekstinä Poista suoritustiedot.

Poista suoritustiedot -nappi

Komponentin tulostusnäkymän ehdollistaminen

Hyvään käytettävyyteen kuuluu, että käyttäjä ei pääse vahingossa poistamaan omia suoritustietojaan. Siksi napin painaminen ei saa poistaa tietoja kysymättä. Sen sijaan nappi avaakin lomakkeen, joka vaatii käyttäjältä aktiivista toimintaa tietojen poistamiseksi.

Reset-komponentilla tulee olemaan kaksi eri näkymää:

  • oletusnäkymä, jossa on vain Poista suoritustiedot -nappi ja
  • lomakenäkymä, jossa on tietojen poiston varmistava lomake.

Kun käyttäjä painaa oletusnäkymän nappia, niin ruudulle tulostuu lomakenäkymä. Tämä toteutetaan hyödyntämällä useState-tilamuuttujaa komponentin sisällä.

Muokkaa src/components-kansion Reset.jsx-komponenttia seuraavasti:

  • Lisää tiedoston alkuun seuraava useState-funktion tuonti.

    import { useState } from 'react';
    
  • Lisää tilamuuttujan esittely Reset-funktion alkuun.

      const [showForm, setShowForm] = useState(false);
    

    Tämä esittelee totuusarvoisen tilamuuttujan, jonka alkuarvo on epätosi (false).

  • Korvaa return-lause kokonaisuudessaan seuraavilla ohjelmariveillä:

      if (showForm) {
        return (
          <div>TODO lomakenäkymä</div>
        );
      } else { 
        return (
          <div className="reset">
            <button onClick={()=>{setShowForm(true)}}>Poista suoritustiedot</button>
          </div>
        );
      }
    

Edellä koodissa muuttui kaksi asiaa. Ensiksi alkuperäinen return-lause sijoitettiin if-else-rakenteen else-osan sisällöksi. If-else-rakenne tulostaa jomman kumman näkymän riippuen showForm-tilamuuttujan arvosta. Alkutilanteessa tulostetaan else-osan sisältö. Toiseksi nappiin lisättiin onClick-käsittelijä, joka muuttaa showForm-muuttujan arvoksi tosi (true), kun nappia painetaan.

Käyttäjälle tämä toiminnallisuus näkyy niin, että ensimmäisen kerran Settings-sivulle tultaessa tilastotietojen alla näkyy nappi, kun käyttäjä painaa tuota nappia, niin napin tilalle tulee TODO lomakenäkymä -teksti.

Napin painalluksen kautta tullut TODO lomakenäkymä -teksti

Lomakenäkymän toteuttaminen

Seuraavaksi toteutamme napin alta paljastuvan lomakenäkymän. Korvaa if-else-lauserakenteen ensimmäinen return-lause (eli se, jonka sisältönä on <div>TODO lomakenäkymä</div>) seuraavalla:

    return (
      <div className="reset reset_box">
        <h2>Suoritustietojen poistaminen</h2>
        <p>Varoitus! Olet poistamassa kaikki,
           mitä olet tähän mennessä kerännyt.
           Jatkamalla tiedot nollautuvat ja peli alkaa
           alusta.</p>
        <p>Kirjoita teksti <span>{props.resetvalue}</span> alla olevaan kenttään.</p>
        <div><input type="text" /></div>
        <button>Poista suoritustiedot</button>
      </div>
    );

Nyt napin kautta avatuu jo lomakenäkymä, jossa ei ole vielä toiminnallisuutta.

Suoritustietojen poistaminen -lomakenäkymä

Näkymän ohjerivi on hassu, sillä Reset-komponentille ei vielä välitetä resetValue-arvoa, joka return-lauseen sisällä tulostetaan.

Muokkaa src/pages-kansion Settings.jsx-tiedoston return-lauseen sisällä oleva Reset-komponentin kutsu seuraavanlaiseksi:

        <Reset resetvalue={props.stats.clicks} />

Tämän muutoksen myötä käyttäjän tekemien napautusten määrä tulostuu osaksi ohjetekstiä.

Käyttäjän napautusten määrä tulostuu osaksi ohjetekstiä

Lomakekentän hallinta

Lomakkeen kenttään pystyy tällä hetkellä syöttämään tietoa, mutta syötetyllä tiedolla ei ole vielä vaikutusta. Tallennetaan ensin käyttäjän syöttämä tieto tilamuuttujaan ja hyödynnetään tätä arvoa sen jälkeen.

Muokkaa src/components-kansiossa olevaa Reset-jsx-tiedostoa seuraavasti:

  • Lisää funktion alkuun, ennen return-lausetta seuraava useState-muuttujan esittely:

      const [value, setValue] = useState("");
    

    Tähän tilamuuttujaan tallennetaan käyttäjän lomakekenttään syöttämä teksti.

  • Muokkaa ensimmäisen return-lauseen sisällä olevaa lomakekentän määrittelevä rivi seuraavanlaiseksi:

            <div><input type="text" value={value} /></div>
    

    Muutoksessa input-elementin value-arvoksi annetaan value-tilamuuttujan arvo.

Nyt lomakekenttään ei pysty kirjoittamaan mitään tekstiä. Tämä johtuu siitä, että React kontrolloi lomakekentän sisältöä ja huolehtii siitä, että kentän sisältö on aina sama kuin value-tilamuuttujan sisältö.

Jos haluamme mahdollistaa tekstin syöttämisen lomakekenttään, on sille määriteltävä käsittelijä, jota kutsutaan aina, kun lomakkeen sisältöä muutetaan.

Muokkaa ensimmäisen return-lauseen sisällä olevan lomakekentän määrittelyrivi seuraavanlaiseksi:

        <div>
          <input type="text"
                 value={value}
                 onChange={(e) => {setValue(e.target.value)}} />
        </div>

Sen lisäksi, että sisältöä rivitettiin uudelleen, lisättiin input-elementille onChange-käsittelijä, joka kutsuu setValue-funktiota aina kun lomakkeen kentän sisältöä muokataan. e viittaa siihen elementtiin, jossa muutos tapahtui.

Lomakekentän arvon muuttuminen tapahtuu taustalla seuraavasti:

  1. Kun käyttäjä syöttää lomakekenttään jonkin merkin, niin kentän onChange-käsittelijä käynnistyy ja kutsuu sille määritellyn funktion.

  2. Funktio käy poimimassa lomakekentän uuden arvon (e.target.value) ja tallentaa sen setValue-funktion kautta value-tilamuuttujan arvoksi.

  3. React tunnistaa, että value-tilamuuttujan arvo on päivittynyt, jolloin se renderöi uudelleen lomakekentän, jossa value-tilamuuttujan arvoa käytetään.

  4. Lomakekentän renderöinnin myötä lomakekentän arvoksi tulee value-tilamuuttujan arvo eli käyttäjän muokkaama arvo.

  5. Jos käyttäjä syöttää kenttään lisää sisältöä, niin tämä prosessi käynnistyy alusta.

Tämä prosessi selittää myös sen, miksi lomakekentän sisältöä ei pystynyt aiemmin muokkaamaan, kun kenttään ei ollut vielä lisätty onChange-käsittelijää.

Napin aktivointi lomakekentän arvon perusteella

Lomakkeen käytettävyys edellyttää vielä palautetta siitä, milloin käyttäjän lomakekenttään syöttämä teksti on toivotunlainen. Tämä tapahtuu niin, että lomakkeen alla oleva nappi on lähtökohtaisesti disabloitu ja aktivoituu, kun nappia voidaan painaa.

Muokkaa ensimmäisen return-lauseen sisällä olevan button-napin määrittely seuraavanlaiseksi:

        <button disabled={props.resetvalue==value?false:true}>Poista suoritustiedot</button>

Tämä määrittelee napille disabled-arvon riippuen siitä, vastaavatko käyttäjän syöttämä teksti ja komponentille välitetty resetvalue toisiaan. Jos arvot eivät vastaa toisiaan, tulee disabled-arvoksi tosi (true), jolloin nappi ei ole aktiivinen. Napin tyylimääritteissä se on määritelty harmaaksi. Kun arvot vastaavat toisiaan, tulee disabled-arvoksi epätosi (false), jolloin nappi on aktiivinen ja sen värinä on punainen.

Nappi on disabloitu, kun lomakekentän sisältöl ei vastaa pyydettyä

Suoritustietojen poiston käsittelijä

Lomake toimii tällä hetkellä muuten, mutta aktiivisen napin painaminen ei vaikuta mitään. Toteutetaan ensin pääkomponentille funktio, joka suorittaa tietojen poiston, välitetään funktio komponenttipuuta pitkin Reset-komponentille ja määritellään funktio napin onClick-käsittelijäksi.

  1. Muokkaa src-kansion App.jsx-tiedostoa seuraavasti:

    • Lisää seuraava funktiomäärittely ennen return-lausetta:
        const handleReset = () => {
          // Päivitetään tilamuuttujat alkuarvoihin.
          setStats(initialstats);
          setStoreitems(items);
        }
      
    • Välitetään tämä funktio komponenttipuiuta pitkin AppRouter-komponentille. Muuta return-lauseen AppRouter-komponenttikutsu seuraavanlaiseksi:
          <AppRouter stats={stats}
                     storeitems={storeitems}
                     handleClick={handleClick}
                     handlePurchase={handlePurchase}
                     handleReset={handleReset} />
      
  2. AppRouter-komponentilla välitetään funktio eteenpäin Settings-komponentille. Muokkaa src/components-kansion AppRouter.jsx-tiedoston reititysmäärityksissä settings-sivun määritys seuraavanlaiseksi:

            { path: "settings", element: <Settings stats={props.stats}
                                                   handleReset={props.handleReset} />},
    
  3. Settings-komponentilla välitetään funktio eteenpäin Reset-komponentille. Muokkaa src/pages-kansion Settings.jsx-tiedostossa Reset-komponenttikutsu seuraavanlaiseksi:

            <Reset resetvalue={props.stats.clicks}
                   handleReset={props.handleReset} />
    
  4. Nyt handleReset-funktio on kuljettu komponenttipuuta pitkin App-komponentilta Reset-komponentille. Seuraavaksi kytkemme funktion napin onClick-käsittelijäksi. Muokkaa src/components-kansion Reset.jsx-tiedostoa seuraavasti:

    • Lisää funktion alkuun ennen ehtolauserakennetta seuraava funktiomäärite:

        const handleReset = () => {
          // Nollataan pelin tiedot ja tyhjennetään tekstikenttä.
          props.handleReset();
          setValue("");
        }
      
    • Muokkaa ensimmäisessä return-lausessa oleva button-määrittely seuraavanlaiseksi:

              <button disabled={props.resetvalue==value?false:true}
                      onClick={handleReset}>Poista suoritustiedot</button>
      

Näiden muutosten jälkeen sovellus nollaa käyttäjän pelitiedot nappia painettaessa.

Käyttäjän tiedot nollattu

Tuotelistan kopioinnin korjaaminen

Tarkkaavaisimmat huomasivat, että pelitietojen nollaamisen yhteydessä ostettujen tuotteiden määrät eivät nollaantuneet.

Tuotelistan tuotteet eivät ole nollaantuneet

Tämä johtuu tavasta, jolla items-taulukosta tehdään kopio. Kopiossa tehdään ns. matalatason kopio, jossa kopion tekeminen ei ulota syvemmille tasoille. Toisin sanoen kopioidussa taulukossa tuotteet ovat alkuperäisen taulukon tuotteita. Eri tapoja taulukon kopiointiin käsitellään artikkelissa How to clone an array in JavaScript.

Muuta src-kansion App.jsx-tiedoston handlePurchase-funktion sisällä storeitems-muuttujan kopiointi seuraavanlaiseksi:

      // Tehdään kopiot tilamuuttujista.
      let newstoreitems = JSON.parse(JSON.stringify(storeitems));

Tämän muutoksen jälkeen myös tuotelista nollaantuu.

Tuotelistan tuotteet ovat alkuarvoissa

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää suoritustietojen poistamisen"

lisää suoritustietojen poistamisen -commit

Tietojen säilyttäminen

Sovellus on siinä vaiheessa, että toiminnallisuudesta puuttuu enää pelitietojen säilyttäminen niin, että ne eivät katoa sivupäivityksen yhteydessä. Selainsovelluksille on tarjolla monta erilaista tapaa tietojen säilyttämiseen. Näistä yksinkertaisimmasta päästä on HTML5-standardissa määritelty localStorage.

localStorage mahdollistaa pienten tietomäärien tallentamisen päätelaitteeseen niin, että niihin pääsee käsiksi aina tarvittaessa. Sillä on seuraavat ominaisuudet:

  • Tieto tallennetaan paikallisesti käyttäjän selaimen sisällä.
  • Tietoja ei koskaan siirretä palvelimelle toisin kuin evästeet (cookies).
  • Varattu tallennustila on vähintään 5 megatavua, mikä riittää pitkälle pelkän tekstitiedon tallentamiseen.
  • Tallennus tapahtuu sivustokohtaisesti, samasta domainosoitteesta (ja portista) ladatut sivut pääsevät käsiksi tallentamaansa tietoon.

localStoragen käyttö on hyvin suoraviivaista. Sillä on yksi funktio, jolla tiedon voi tallentaa.

localStorage.setItem('luku','42');

Tallennus tapahtuu nimi- ja arvopareina. Esimerkiksi edellä oleva koodirivi tallentaa luku-nimellä merkkijonon 42. Arvo tallennetaan aina merkkijonona.

Tällä tavalla tallennettu arvo säilyy selaimen muistissa vaikka sivun sisältö päivitettäisiin tai selain suljettaisiin välissä.

localStorageen tallennettu arvo luetaan puolestaan omalla funktiolla.

let arvo = localStorage.getItem('luku');

Tämän jälkeen arvoa voidaan käyttää sovelluksessa aivan samalla tavalla kuin mitä tahansa muuta merkkijonoa. Jos tallennettu sisältö on jokin muu kuin merkkijono, silloin arvo täytyy muuttaa ko. muotoon. Tähän tulemme tutustumaan tarkemmin sovelluksen toteutuksessa.

Näiden lisäksi tallennetun arvon voi myös poistaa muistista.

localStorage.removeItem('luku');

Tätä toiminnallisuutta tulemme seuraavaksi hyödyntämään pelin tietojen tallennuksessa. Jotta tallennus olisi tapahtuisi vielä mutkattomammin, niin yhdistämme aikaisemmin käyttämämme useState-muuttujan ja localStorage-toiminnallisuuden yhdeksi kokonaisuudeksi ja toteutamme siitä ns. React Hooks -funktion.

React Hooks -funktiot

Olemme aikaisemmin käyttäneet tämän projektin aikana useamman kerran useState-funktiota, joka on React-kirjaston eniten käytetty Hooks-funktio.

Hooks-funktioiden tavoitteena on paketoida jokin usein tarvittava toiminnallisuus yhdeksi kokonaisuudeksi, jota voi sitten hyödyntää aina kun on tarve.

Hooksit ovat oikeastaan ihan tavallisia JavaScript-funktioita, joilla on kaksi lisäsääntöä:

  • Hooks-funktioita saa kutsua ainoastaan koodin päätasolla eli niitä ei saa kutsua silmukoiden tai ehtorakenteiden sisältä.
  • Hooks-funktioita saa kutsua ainoastaan Reactin (funktio)komponenteista tai itse tehdyistä Hooks-funktioista.

Näiden lisäksi Hooks-funktiot tulee nimetä niin, että ne alkavat sanalla use.

useLocalStorage-funktion esittely

Seuraavaksi toteutamme oman Hooks-funktion, joka hoitaa taustalle tietojen säilömisen. Lisää src/utils-kansioon uusi tiedosto, anna sen nimeksi useLocalStorage.js ja liitä sen sisällöksi seuraava ohjelmakoodi, joka pohjautuu Using localStorage with React Hooks-sivulla olevaan koodiin.

import { useEffect, useState } from "react";

// Muuttaa muuttujan JSON-merkkijonoksi.
const decode = (value) => {  
  return JSON.stringify(value);
}

// Purkaa JSON-merkkijonon muuttujaksi.
const encode = (value) => {
  return JSON.parse(value);
}

const useLocalStorage = (key, defaultState) => {

  // Tilamuuttujan määrittely, arvoksi haetaan joko
  // localStorage-muuttujan arvo tai alkuarvo.
  const [value, setValue] = useState(
    encode(localStorage.getItem(key) || null) || defaultState
  );

  // Tallennetaan tilamuuttuja localStorageen aina,
  // kun arvo muuttuu.
  useEffect(() => {
    localStorage.setItem(key, decode(value));
  },  [value]);

  // Alkuarvojen palautusfunktio.
  const resetValue = () => {
    setValue(defaultState);
  }

  return [value, setValue, resetValue];
}

export default useLocalStorage;

Aivan ensimmäiseksi tuodaan React-kirjastosta useState- ja useEffect-funktiot, joita koodissa tullaan hyödyntämään.

Esitellään decode- ja encode-apufunktiot, joita käytetään muuttamaan ja purkamaan localStorage-taltioon tallennettavaa tietoa. Koska localStorage-taltioon voi talletaan ainoastaan merkkijonoja, on esimerkiksi stats-oliomuuttuja ja storeitems-taulukkomuuttuja muunnettava merkkijonomuotoon. Tämä onnistuu funktioissa käytettävillä JSON.stringify- ja JSON.parse-funktioilla. Näistä ensimmäinen muuttaa olio- tai taulukkomuuttujan merkkijonomuotoiseksi, kun jälkimmäinen puolestaan purkaa merkkijonomuotoisen merkinnän olio- tai taulukkomuuttujaksi.

Seuraavaksi esitellään useLocalStorage-funktio, joka saa kutsuttaessa kaksi parametria: key ja defaultState.

Funktion ensimmäisenä vaiheena on tilamuuttujan alustaminen useState-funktiolla. Tilamuuttujan alkuarvoksi määritellään joko localStorage-taltioon tallennettu arvo tai defaultState-parametrin kautta tullut arvo riippuen siitä kumpi löytyy ensin. Funktiokutsujen sisällä olevat or-operaatiot (||) valitsevat aina käytettäväksi arvoksi ensimmäisen, joka voidaan ajatella muuksi kuin tyhjäksi.

Funktion toisena vaiheena luodaan efektitoiminnallisuus useEffect-funktion avulla. Sen avulla voi määritellä toiminnan, joka suoritetaan aina kun jonkin muuttujan arvo päivittyy. Funktiolle annetaan kutsussa kaksi parametria: funktio ja taulukko. Suoritetta toiminta määritellään funktion sisällö ja seurattava(t) muuttuja(t) määritellään taulukossa.

Tässä ohjelmakoodissa useEffect-toiminnan seurattavaksi muuttujaksi määritellään value-muuttuja. Aina, kun seurattavan muuttujan arvo muuttuu, suoritetaan funktion sisällä oleva ohjelmakoodi, joka tallentaa value-arvon localStorage-taltioon.

Funktion kolmantena vaiheena esitellään resetValue-funktio, jonka avulla tallennetun arvon voi palauttaa alkutilanteeseen.

Aivan lopuksi funktio palauttaa taulukossa kolme asiaa:

  • tilamuuttujan,
  • funktion, jolla tilamuuttujan arvo päivitetään ja
  • funktion, jolla tilamuuttujan arvo palautetaan alkuarvoon.

Tiivistetysti voidaan todeta, että edellä esitelty funktio, toimii pääpiirteissään samalla tavalla kuin useState-funktio. Keskeinen ero on se, että tekemämme funktio osaa säilyttää tiedon sivulatausten välillä ja sen arvo voidaan tarvittaessa palauttaa alkuarvoon.

useLocalStorage-funktion hyödyntäminen

Otetaan seuraavaksi edellä toteuttamamme useLocalStorage-funktio käyttöön. Muokkaa src-kansiossa olevaa App.jsx-tiedostoa seuraavasti.

  • Lisää tiedoston alkuun seuraava import-rivi:

    import useLocalStorage from './utils/useLocalStorage';
    
  • Muuta App-funktion alussa olevat olevat useState-alustukset seuraavanlaisiksi:

      // Luodaan taltio, johon tallennetaan pelin laskennalliset tiedot.
      const [stats, setStats, resetStats] = useLocalStorage('lemon-stats',initialstats);
    
      // Luodaan taltio, johon tallennetaan tuotelista.
      const [storeitems,setStoreitems, resetStoreitems] = useLocalStorage('lemon-items',items);
    

    useLocalStorage-funktion toiminta on käytännössä hyvin samanlainen, kuin useState-funktion toiminta. Alustuksessa määritellään key-arvo, jolla muuttujan tiedot tallennetaan localStorage-taltioon.

    Testaus

    Kokeile pelin toimintaa pelaamalla ja välillä uudelleenlataamalla sivun selaimessa.

    Sovellus muistaa nyt pelin tilanteen sivulatausten välillä. Myös pelin tietojen nollaus toimii mainiosti, mutta käytetään siihen useLocalStorage-funktion palauttamia reset-funktiota.

  • Muuta handleReset-funktio seuraavanlaiseksi:

      const handleReset = () => {
        // Palautetaan taltiot alkuarvoihin.
        resetStats();
        resetStoreitems();
      }
    

Nyt sovellus toimii kokonaisuudessaan suunnitelman mukaisesti. Seuraavaksi paketoimme sovelluksen julkaisukuntoon ja julkaisemme sen.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää tallennuksen localStorage-taltioon"

lisää tallennuksen localStorage-taltioon -commit

PWA-sovellus

React-sovelluksen voi määritellä ns. PWA-sovellukseksi (Progressive Web Application), joka käyttäytyy mobiililaitteilla lisättäessä samalla tavoin kuin sovelluskaupasta asennetut sovellukset. Erona sovelluskaupasta asennettuun sovellukseen on se, että sovellusta lisätään menemällä sovelluksen kotisivuille ja hyväksymällä sovelluksen lisääminen aloitusnäyttöön.

Esimerkiksi Chromessa PWA-sovelluksen asennusta tarjotaan kun:

  • käyttäjä on napauttanut sivulla vähintään kerran,
  • käyttäjä on ollut sivulla vähintään 30 sekuntia,
  • sivusto jaetaan HTTPS-protokollan kautta ja
  • sivustolle on määritelty manifest-tiedot.

Sovelluksen julkaisussa voidaan vaikuttaa tästä listasta kahteen viimeiseen kohtaan eli julkaistaan sivusto HTTPS-protokollaa tukevalla sivustolla ja määritellään sovellukselle vaatimukset täyttävät manifest-tiedot.

Seuraavat vaiheet mukailevat seuraavia artikkeleita:

PWA-lisäosan asennus Vite-projektiin

Aloitetaan PWA-sovelluksen määrittely sillä, että asennetaan Vite-projektiin PWA-toiminnallisuuden lisäävä lisäosa. Suorita seuraava komento projektikansiossa:

npm install -D workbox-window vite-plugin-pwa

Edellä olevassa komennossa -D asentaa paketit kehitysaikaisiksi riippuvuuksiksi (devDependencies). React-projektien kanssa tarvitaan myös workbox-window-paketti. Monissa muissa kehitysympäristöissä riittää pelkän vite-plugin-pwa-paketin asennus.

manifest.json

manifest.json ja nimensä mukaisesti JSON-muotoinen tiedosto, jossa kerrotaan web-sovelluksen tiedot, kuten esimerkiksi sovelluksen nimi ja ikonit. Virallinen määritelmä määrittelee pitkän listan asioita, jotka sovelluksesta voidaan kertoa. PWA-sovelluksessa näistä ei tarvitse määritellä kaikkia, PWA-sovelluksen kannalta pakolliset ainoastaan pakolliset tarvitsee määritellä.

Luo projektin pääkansioon (eli samaan kansioon missä on index.html-tiedosto) uusi tiedosto, anna sen nimeksi manifest.json ja liitä sen sisällöksi seuraavat rivit:

{
  "name": "Lemon Clicker",
  "short_name": "Lemonade",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "orientation": "portrait",
  "theme_color": "#EBEBEB",
  "background_color": "#EBEBEB"
}
  • name määrittelee, millä nimellä sovellus näkyy esim. sovellusvalikossa.
  • short_name määrittelee, millä nimellä sovellus näkyy esim. sovellusvalikoissa, jos name-määreessä oleva nimi on liian pitkä esitettäväksi.
  • start_url määrittelee mistä osoitteesta web-sovellus käynnistyy.
  • scope määrittelee missä osoiteavaruudessa sovelluksen sallitaan navigoivan. Jos käyttäjä avaa sivun, joka on scope-arvon ulkopuolella, avataan sivu normaaliin web-selainnäkymään. Esimerkkikoodin arvolla sallitaan liikkuminen domain-osoitteen sisällä.
  • display määrittelee näyttömoodin eli sen, kuinka paljon selaimen käyttöliittymää näytetään. standalone-arvo määrittelee sovelluksen käyttäytymään omana sovelluksena.
  • orientation määrittelee miten sovellusta ensisijaisesti käytetään (vaakatasossa vai pystytasossa).
  • theme_color määrittelee sovelluksen oletusteemavärin, tässä tapauksessa määritelty samaksi kuin sovelluksen taustaväri. Tämä vaikuttaa mm. siihen, miten järjestelmä näyttää sovelluksen.
  • background_color määrittelee taustavärin, jota käytetään ennen kuin sovelluksen tyylitiedosto on ehditty ladata. Tämä arvo kannattaa olla sama kuin sovelluksen tyylimääritteissä määritelty taustaväri.

Edellä lisätystä manifest.json-määrittelystä puuttuu kokonaan ikonimäärittelyt, jotka ovat PWA-sovelluksen käytön kannalta pakolliset. Niiden määrittely jätetään seuraavassa vaiheessa käytettävälle sovellukselle.

Sovelluksen ikonit

Sovelluksen ikonit ovat harvoin olemassa, vaan ne joutuu erikseen tekemässä. Haasteelliseksi ikonien teon tekee se, että niitä pitäisi olla usemapaan eri kokoa, jotta ne toimivat optimaalisesti eri laitteilla. Ikonit kannattaa tehdä joko palvelun tai sovelluksen avulla.

Tehdään ikonit käyttämällä pwa-asset-generator -sovellusta. Suorita seuraava komento projektin pääkansiossa:

npx pwa-asset-generator ./src/assets/lemon-big.svg ./public/icons -f -b "#EBEBEB" -m ./manifest.json -i 
./index.html -v /icons

Edellä oleva komento on pitkä ja se voi kopioinnin yhteydessä jakautua useammalle riville. Varmista, että annat komennon yhdellä rivillä.

Komennolle annetaan pitkä lista määritteitä, niiden merkitys on seuraava:

  • ./src/assets/lemon-big.svg määrittelee kuvan, josta ikonit muodostetaan.
  • ./public/icons määrittelee kansion, jonne ikonit luodaan.
  • -f määrittelee, että sovellukselle luodaan ja kytketään favicon-ikoni.
  • -b "#EBEBEB" määrittelee kuvan taustalla käytettävän taustavärin, sama kuin sovelluksen taustaväri.
  • -m ./manifest.json määrittelee muokattavan manifest.json-tiedoston sijainnin.
  • -i ./index.html määrittelee muokattavan index.html-tiedoston sijainnin.
  • -v /icons määrittelee ikonin linkityksissä käytettävän polun, yliajaa oletuspolun.

Suorituksen jälkeen public/icons-kansioon ilmestyi monta ikonia, manifest.json-tiedostoon lisättiin ikonimääritykset ja index.html-tiedostoon lisättiin monta ikonimääritystä.

Projektin PWA-määritykset

Seuraavaksi kytketään projektiin PWA-lisäosa sekä edellä määritellyt manifest-määritykset.

Muokkaa projektin pääkansiossa olevaa vite.config.js-tiedostoa seuraavasti:

  • Lisää tiedoston alkuun seuraavat import-tuonnit:

    import { VitePWA } from "vite-plugin-pwa";
    import manifest from './manifest.json';
    
  • Muuta defineConfig-määrittely seuraavanlaiseksi:

    export default defineConfig({
      plugins: [react(),
                VitePWA({ manifest: manifest })],
    })
    

    Tämä lisää Viten PWA-lisäosan osaksi projektia ja antaa määrityksiksi aikaisemmin määrittelemämme manifest.json-määritykset.

Projektin rakentaminen

Nyt kaikki asetukset on tehty, jotta voimme rakentaa julkaistava versio (build) sovelluksesta. Tämä tapahtuu seuraavalla komennolla:

npm run build

Komennon rakentaa julkaistavan version sovelluksesta ja lopullinen versio löytyy suorituksen jälkeen projektikansion dist-kansion alta.

dist/assets-kansio sisältää kaikki src/assets-kansiosta linkitetyt tiedostot sekä sovelluksen JavaScript-koodin ja CSS-tyylimääritteet kummatkin yhteen tiedostoon koostettuna.

dist/icons-kansio sisältää kaikki public/icons-kansion alla olevat sovelluksen ikonit.

dist-kansio sisältää seuraavat tiedostot:

  • index.html on hyvin pitkälle sama kuin projektikansion index.html-tiedosto, build-vaiheessa siihen on lisätty linkitykset rakentamisen yhteydessä muodostettuihin tiedostoihin.

  • manifest.webmanifest on hyvin pitkälle sama mitä edellä defineConfig-määrittelyn kautta määriteltiin.

  • registerSW.js, sw.js ja workbox-xxxxxxxx.js tiedostot ovat rakentamisen yhteydessä lisättyjä tiedostoja. Ne muodostavat sovelluksen ns. Service Worker -kokonaisuuden. Service Worker on sovelluksen taustalla oleva sovelluksen osa, joka huolehtii taustalla muun muassa sovelluksen uuden version lataamisesta.

    Tämä on kätevää esimerkiksi PWA-sovelluksissa, jotka on asennettu mobiililaitteelle sovelluksena. Tällöin itse sovellus käynnistyy nopeasti käyttäen sitä versiota, joka laitteen muistiin on tallennettu. Kun sovellusta käytetään, käy Service Worker taustalla lataamassa uudemman version laitteelle. Oletusmäärityksillä uutta, ladattua versiota käytetään sovelluksen seuraavalla käynnistyskerralla.

Rakennetun projektin esikatselu

Ennen kuin sovellusta lähdetään julkaisemaan, kannattaa rakennettu versio esikatsella ensin omalla laitteella. Tämä tapahtuu komennolla:

npm run preview

Tämä komento tulostaa seuraavan kaltaisen tulostuksen:

> lemon-clicker@0.0.0 preview
> vite preview

  ➜  Local:   http://localhost:4173/
  ➜  Network: use --host to expose

Komento käynnistää paikallisesti toimivan web-palvelimen, joka näkyy tulostuksen ilmoittamassa osoitteessa. Kun menet selaimella tuohon osoitteeseen, niin näet sovelluksesta sen rakennetun version.

Sovelluksen rakennettu versio

Huomaa, että preview-toiminnolla toimiva sivusto on tarkoitettu ainoastaan rakennetun sovelluksen esikatseluun, sitä ei ole tarkoitettu julkaisumenetelmäksi.

Muutosten vienti versiohallintaan

Viedään viimeisimmät muutokset versiohallintaan.

git add .
git commit -m "lisää PWA-lisäosan ja sen määritykset"

lisää PWA-lisäosan ja sen määritykset -commit

Azure for Students

Seuraavissa vaiheissa tulemme julkaisemaan sovelluksen Azure-pilvipalveluun. Azure on Microsoftin ylläpitämä ja hallinnoita pilvipalveluympäristö, josta löytyy jokaisen tarpeisiin soveltuva palvelu tai palvelukokonaisuus. Suurin osa Azuren palveluista on maksullisia, mutta se sisältää myös paljon palveluita, jotka on ilmaisia käyttää.

Normaalin rekisteröitymisen kautta käyttäjälle avautuu käytettäväksi sellaisia palveluita, jotka ovat ilmaisia ensimmäiset 12 kuukautta ja sellaisia jotka ovat aina ilmaisia. Nämä ilmaiset palvelut on listattu Explore free Azure services -sivulla.

Valitettavasti näiden ilmaisten palveluiden käyttäminen edellyttää rekisteröitymisen yhteydessä luottokorttitietojen syöttämisen maksuvälineeksi.

Onneksi oppilaitoksen tunnuksilla pääset rekisteröitymään opiskelijoille tarjottavan Azure for Students -ohjelman kautta. Sitä kautta rekisteröitäessä sinulta ei vaadita luottokorttitietojen syöttämistä. Lisäksi ohjelman kautta saat 100 dollaria krediittejä, joilla voit kokeilla Azuren maksullisia palveluita. Saat vuosittain uudet 100 dollaria käytettäväksi niin kauan, kuin olet opiskelijana.

Huomaa, että Azure for Students -ohjelman alla käyttämäsi palvelut ovat käytössäsi niin kauan kuin olet opiskelija. Opiskeluiden jälkeen sinun tulee rekisteröityä Azure-palveluun tavallisena käyttäjä (luottokorttitiedoilla), jos haluat jatkaa käyttämiesi palveluiden käyttöä.

Tässä projektissa käyttämämme palvelu on aina ilmainen -kategoriassa, joten sen osalta sovelluksen julkaisun siirtäminen omalle henkilökohtaiselle tunnukselle ei ole haitallista.

Rekisteröityminen

Azure for Students -ohjelmaan rekisteröityminen tapahtuu seuraavasti:

  1. Mene selaimessa Azure for Students -ohjelman sivuille.

    Azure for Students -ohjelman sivuston

  2. Klikkaa vihreää Start free-nappia. Tämän jälkeen sinua pyydetään valitsemaan Microsoft-tili. Valitse oppilaitoksen tili.

  3. Tämän jälkeen palvelu pyytää puhelinnumeroasi varmistamista varten. Syötä oma numerosi ja valitse joko Text me tai Call me.

    Käyttäjän varmistus puhelimella

    Syötä valitsemallasi tavalla saamasi varmistuskoodi ja paina Verify code.

  4. Varmistuksen jälkeen palvelu kysyy sinulta nimi ja yhteystiedot. Täytä ne, lukaise ja hyväksy käyttöehdot ja viimeistele rekisteröinti painamalla Sign up.

    Käyttäjän nimen ja yhteystietojen syöttäminen

  5. Odota pieni hetki, että palvelu saa luotua tilisi. Ennen Azure-palveluun siirtymistä sinulta kysytään vielä käyttäjätunnus, jolla haluat kirjautua. Valitse oppilaitostunnuksesi.

  6. Kun olet kirjautunut Azure-palveluun, ruudulle aukeaa Education-osion yhteenvetonäkymä. Siitä ilmenee, että sinulla on 100 dollaria käytettävissä seuraavan vuoden ajan. Tai 93 euroa, jos palvelu näyttää arvot euroina.

    Education-osion yhteenvetonäkymä

Kirjautuminen

Kun haluat jatkossa kirjautua Azure-palveluun, niin avaa ensin selaimessa https://portal.azure.com -sivu ja valitse kirjautumistunnukseksi oppilaitoksen tunnus. Kirjautumisen jälkeen sinut ohjataan Azure-palvelun etusivulle.

Azure-palvelun etusivu

Jos haluat nähdä käytettävissä olevan saldon, niin hae yläreunan hakukentällä Education-sivu. Vaihtoehtoisesti näet mitä käyttämäsi palvelut kuluttavat Subscriptions-sivulla.

Projektin siirtäminen GitHubiin

Jotta projektin voidaan julkaista Azure Static Web App -palvelun kautta, on projekti ensin siirrettävä GitHub-palveluun.

  1. Kirjaudu GitHub-palveluun.

  2. Aloita uuden repositoryn luominen klikkaamalla New-nappia. Määrittele repositoryyn seuraavat asetukset:

    • Anna luotavan repon nimeksi lemon-clicker.
    • Valitse Private-vaihtoehto, jolloin projekti ei näy muille.

    Älä valitse muihin kohtiin mitään valintoja, jotta luotava repo on täysin tyhjä.

    Huomaa myös, että oheisessa esimerkkikuvassa on lomakkeen alussa sisältöä, joka ei luultavasti näy omassa näkymässäsi. Siitä ei tarvitse välittää.

    Viimeistele repositoryn luominen klikkaamalla Create repository -nappia.

    Uuden repositoryn luominen

  3. Kytke projekti repoon suorittamalla …or push an existing repository from the command line -kohdan komennot komentorivillä projektikansiossa.

    git remote add origin https://github.com/tunnus/lemon-clicker.git
    git branch -M main
    git push -u origin main
    

    Voit myös valita yhteystavaksi SSH, jos olet määritellyt kirjautumisen käyttämään SSH-avaimia.

    Päivitä reponäkymä Githubissa näiden komentojen jälkeen, siellä näkyy nyt projektin tiedostot.

    Projekti siirretty GitHubiin

Julkaisu Azure Static Web Apps -sovelluksena

Sovelluksen julkaiseminen Azure Static Web Apps -sovelluksena onnistuu helpoiten Visual Studio Coden lisäosan avulla.

Lisäosan asennus

  1. Mene Visual Studio Codessa lisäosanäymään (Extensions) pikanäppäinyhdistelmällä Ctrl + Shift + X.

  2. Kirjoita hakukenttään Azure Static Web Apps, valitse listasta lisäosa ja paina Install-nappia.

    Visual Studio Coden lisäosanäkymä

    Kun Install-napin paikalle tulee Disable- ja Uninstall-napit, on lisäosa asennettu ja valmiina käytettäväksi.

Sovelluksen julkaiseminen

Seuraavaksi julkaisemme sovelluksen lisäosan avulla.

  1. Klikkaa vasemman reunan palkista Azure-ikonia.

    Visual Studio Coden Azure-lisäosa

  2. Valitse Sign in to Azure... -vaihtoehto ja valitse kirjautumissa oppilaitostunnus. Kun selaimeen avautuu You are signed in now and can close this page. -teksti, niin voit palata takaisin Visual Studio Codeen, jossa näkyy sinun Azure for Students -tilauksesi.

    Azure for Students -tilaus näkyy lisäosan alla

  3. Paina RESOURCES-tekstin vieressä olevaa +-nappia ja valitse Create Static Web App... -vaihtoehto.

    Uuden resurssin luominen

  4. Tämän jälkeen sinulta kysytään muutama asia luotavaan sovellukseen liittyen.

    • Hyväksy luotavan Static Web Appin oletuksena tarjottu nimi (lemon-clicker).
    • Valitse alueeksi West Europe. Tämä määrittelee, mihin resurssit sijoittuvat.
    • Valitse esiasetukseksi React.
    • Hyväksy sovelluksen sijainniksi /.
    • Määrittele kansioksi, jonne sovellus rakennetaan dist.

    Näiden kysymysten jälkeen lisäosa suorittaa taustalla uuden palvelun aktivoinnin ja sovelluksen julkaisemisen. Onnistuneen julkaisun jälkeen alaosan ikkunaan tulee teksti Create Static Web App "lemon-clicker" Succeeded.

    Julkaistu sovellus löytyy vasemman reunan näkymästä kohdasta RESOURCES > Azure for Students > Static Web Apps > lemon-clicker.

    Sovellus on julkaistu Static Web Appina

  5. Jos haluat testata julkaistun version toimintaa, niin osoitteen löydät painamalla hiiren kakkospainikkeella (oikealle) lemon-clicker -tekstin päällä ja valitsemalla ponnahdusvalikosta Browse site.

    Sovelluksen avaaminen selaimessa

    Esimerkiksi tämän materiaalin sovellus julkaistiin osoitteessa https://thankful-glacier-0b2aec903.3.azurestaticapps.net/.

Julkaisun toimintaperiaate

Julkaisun yhteydessä lisäosa lisäsi taustalla repoon GitHubille Actions-määritteet ja siirsi nämä muutokset myös GitHubin palvelimelle.

Actions-määritteet huolehtivat siitä, että joka kerta kun GitHubin repoon viedään uusi commit-talletus, niin GitHub rakentaa (buildaa) sovelluksen ja kopioi julkaisun tuloksena syntyneen dist-kansion sisällön Static Web Appin alle.

TESTAA

Tee sovellukseen jokin muutos, vie se versiohallintaan ja siirrä muutokset GitHubiin. Odota muutama minuutti ja käy tarkistamassa päivitys.

Huomaa kuitenkin, että joudut todennäköisesti avaamaan sovelluksen kahteen kertaan. Ensimmäisellä kerralla sovellus avataan välimuistista ja uusi versio ladataan taustalla. Toisella kerralla käynnistetään päivitetty sovellus.

TUTKI

Tutustu mitä projektin GitHub-repossa on Actions-osion alla. Samoin tutustu mitä löytyy repon .github/workflows-kansiosta. Huomaa, että repon tiedostoissa ei ole näkyvissä salaisia avaimia, joilla GitHub ottaa yhteyden Static Web App -palveluun.

Service Worker

Julkaistu sovellus toimii tällä hetkellä niin, että se ladataan ensimmäisen käyntikerran jälkeen laitteen muistista. Sovelluksen taustalla toimii Service Worker, joka huolehtii sovelluksen lataamisesta taustalla laitteen välimuistiin. Sama toiminto lataa taustalla sovelluksen uusimman version, jos sellainen on tullut. Uusi versio otetaan käyttöön viimeistään siinä vaiheessa, kun sovellus seuraavan kerran käynnistetään.

Oletuksena Service Worker tallentaa selaimen muistiin sovelluksen HTML-, JavaScript- ja CSS-tiedostot sekä manifest-määrittelyssä olevat ikonit. Sovelluksessa käytetyt SVG-kuvia ei tallenneta. Tämä ilmenee, kun sovelluksen avaa ilman verkkoyhteyttä, kuten esimerkiksi lentokonetilassa. Tällöin sovellus pystyy käyttämään ainoastaan laitteelle tallennettuja resursseja, loput resursseista näkyvät rikkinäisinä kuvakkeina.

Sovellus käynnistetty lentokonetilassa

Jotta sovellus tallentaa myös sovelluksessa käytetyt kuvat, on Service Workerin määrityksiin lisättävä asetus, joka ohjeistaa kuvien tallentamisen.

Muokkaa projektin pääkansiossa olevaa vite.config.js-tiedostossa defineConfig-määrittely seuraavanlaiseksi:

export default defineConfig({
  plugins: [react(),
            VitePWA({ manifest: manifest,
                      workbox: {
                        globPatterns: ['**/*.{js,css,html,png,svg}']
                      } })],
})

Tämä lisää VitePWA-lisäosan määrityksiin workbox-määrittelyn, jossa asetetaan ladattavat tiedostot. Service Worker lataa tämän määrittelyn jälkeen kaikki julkaisukansiossa olevat js-, css-, html-, png- ja svg-päätteiset tiedostot.

Oletusmäärityksillä Service Worker tallentaa js-, css- ja html-päätteiset tiedostot. Nämä pitää määritellä, jotta ne ladattaisiin myös määrittelyn jälkeen.

Tämän jälkeen muutokset viedään Git-repoon ja sieltä GitHub-etärepoon seuraavasti:

git add .
git commit -m "korjaa kuvien latautumisen taustalla"
git push origin main

Kun GitHub on paketoinut uuden version ja julkaissut sen Static Web Apps -alustalla, voidaan sovellus avata uudelleen päätelaitteella. Päivityksen jälkeisen käynnistyksen yhteydessä Service Worker lataa taustalla päivitetyt tiedostot. Pienen hetken jälkeen, kun sovellus uudelleenkäynnistetään ilman verkkoyhteyttä, sovelluksen kuvat toimivat.

Päivitetty sovellus käynnistetty lentokonetilassa

Taustalla latautuva sisältö

Jos haluat tutkia, mitä tietoja Service Worker tallentaa, niin sitä voi tutkia selaimen kehittäjänäkymän Application-välilehdeltä. Välilehden vasemmassa reunassa on sivuvalikko, josta pääsee tutkimaan sovelluksen tietoja sekä tekemään joitain toimintoja, kuten tyhjentämään sivustosta tallennetut tiedot.

Manifest-sivulla näkyy manifest.json -tiedostossa määritellyt tiedot, kuten sivuston nimi, kuvaus, taustavärit ja ikonit.

Application-välilehden Manifest-sivu

Service Workers -sivulta ilmenee Service Workerin tila. Sivun yläreunassa on mm. Offline -valinta, jolla selainikkunan saa väliaikaisesti toimimaan ilman verkkoyhteyttä. Lisäksi tältä sivulta saa lähetettyä sovellukselle testiviestin, jos sovellus hyödyntää Push Notification -toiminnallisuutta.

Application-välilehden Service Workers -sivu

Storage-sivulla näkyy, kuinka paljon sovelluksesta tallennetut tiedot kokonaisuudessaan vievät tilaa. Sivulla olavalla Clear site data -napilla pystyy poistamaan kaiken sivustosta tallennetun tiedon, jolloin seuraava sivulataus aloittaa ns. "tyhjältä pöydältä".

Application-välilehden Storage-sivu

Local storage -otsikon alla olevasta sivusta näkee reaaliaikaisesti mitä tietoja sovellus on tallentanut local storage -taltioon.

Application-välilehden Local storage -osio

Cache storage -otsikon alla on workbox-precache-alkuinen sivu, josta näkee mitä tiedostoja Service worker on ladannut laitteen välimuistiin. Tässä listassa näkyvät tiedostot ovat käytettävissä silloin, kun selaimella ei ole verkkoyhteyttä käytettävissä.

Application-välilehden Cache storage -osio

TESTAA

Voit testata Service workerin toimintaa seuraavasti:

  1. Tee selaimessa pakotettu sivupäivitys näppäinyhdistelmällä Ctrl + F5.
  2. Aseta selain offline-tilaan Service workers -sivulla.
  3. Päivitä sivu (tavallisella sivupäivityksellä) ja tarkkaile toimiiko sovellus niin kuin pitäisi. Sovelluksen kaikki kuvat pitäisi toimia.
  4. Tyhjennä sovelluksen tallennetut tiedot klikkaamalla Storage-sivulla Clear site data -nappia.
  5. Päivitä sivu (tavallisella sivupäivityksellä). Ruudulle pitäisi tulostua tieto, että verkkoyhteyttä ei ole.
  6. Aseta selain online-tilaan ottamalla täppä pois Serveice workers -sivun offline-kohdasta.
  7. Päivitä sivu (tavallisella sivupäivityksellä). Sovellus latautuu nyt normaalisti, Cache storage -sivun alta näkyy, että sovelluksen kaikki tiedostot on tallennettu välimuistiin. Tämä lataus vastaa tilannetta, jolloin käyttäjä vierailee ensimmäisen kerran sivustolla.
  8. Aseta selain jälleen offline-tilaan Service workers -sivulla.
  9. Päivitä sivu (tavallisella sivupäivityksellä) ja tarkkaile toimiiko sovellus niin kuin pitäisi. Sovelluksen pitäisi toimia normaalisti. Tämä vastaa tilannetta, jolloin käyttäjä vierailee sivustolla ilman verkkoyhteyttä.
  10. Aseta selain lopuksi online-tilaan ottamalla täppä pois Serveice workers -sivun offline-kohdasta.

korjaa kuvien latautumisen taustalla -commit

Tekijä

Tällä sivustolla oleva materiaali on toteuttu Koodaaja-koulutuksen koulutusmateriaaliksi. Materiaalin avulla on tarkoitus oppia React-projektin luominen Vite-ympäristössä. Materiaali on toteutettu niin, että sitä voi käyttää sekä itsenäiseen opiskeluun että opetusmateriaalina.

Tämän materiaalin on tuottanut Pekka Tapio Aalto ja se on jaettu Creative Commons Nimeä 4.0 Kansainvälinen -lisenssillä.