Tämä on 3. periodilla pidetyn, jo päättyneen kurssin sisältö.

Jos haluat suorittaa kurssin nyt, mene osoitteeseen https://fullstackopen-2019.github.io/

Palataan yritykseemme käyttää nyt tehtyä backendiä osassa 2 tehdyllä React-frontendillä. Aiempi yritys lopahti seuraavaan virheilmoitukseen

fullstack content

Frontendin tekemä GET-pyyntö osoitteeseen http://localhost:3001/notes ei jostain syystä toimi. Mistä on kyse? Backend toimii kuitenkin selaimesta ja postmanista käytettäessä ilman ongelmaa.

Same origin policy ja CORS

Kyse on asiasta nimeltään CORS eli Cross-origin resource sharing. Wikipedian sanoin

Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the first resource was served. A web page may freely embed cross-origin images, stylesheets, scripts, iframes, and videos. Certain "cross-domain" requests, notably Ajax requests, are forbidden by default by the same-origin security policy.

Lyhyesti sanottuna meidän kontekstissa kyse on seuraavasta: websovelluksen selaimessa suoritettava Javascript-koodi saa oletusarvoisesti kommunikoida vain samassa originissa olevan palvelimen kanssa. Koska palvelin on localhostin portissa 3001 ja frontend localhostin portissa 3000, niiden origin ei ole sama.

Korostetaan vielä, että same origin policy ja CORS eivät ole mitenkään React- tai Node-spesifisiä asioita, vaan yleismaailmallisia periaatteita Web-sovellusten toiminnasta.

Voimme sallia muista origineista tulevat pyynnöt käyttämällä Noden cors-middlewarea.

Asennetaan cors komennolla

npm install cors --save

Otetaan middleware käyttöön ja sallitaan kaikki origineista tulevat pyynnöt:

const cors = require('cors')

app.use(cors())

Nyt frontend toimii! Tosin muistiinpanojen tärkeäksi muuttavaa toiminnallisuutta backendissa ei vielä ole.

CORS:ista voi lukea tarkemmin esim. Mozillan sivuilta.

Sovellus internettiin

Kun koko "stäkki" on saatu vihdoin kuntoon, siirretään sovellus internettiin. Käytetään seuraavassa vanhaa kunnon Herokua.

Jos et ole koskaan käyttänyt herokua, löydät käyttöohjeita kurssin Tietokantasovellus-materiaalista ja Googlaamalla...

Lisätään projektin juureen tiedosto Procfile, joka kertoo Herokulle, miten sovellus käynnistetään

web: node index.js

Muutetaan tiedoston index.js lopussa olevaa sovelluksen käyttämän portin määrittelyä seuraavasti:

const PORT = process.env.PORT || 3001app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

Nyt käyttöön tulee ympäristömuuttujassa PORT määritelty portti tai 3001 jos ympäristömuuttuja PORT ei ole määritelty. Heroku konfiguroi sovelluksen portin ympäristömuuttujan avulla.

Tehdään projektihakemistosta git-repositorio, lisätään .gitignore jolla seuraava sisältö

node_modules

Luodaan heroku-sovellus komennolla heroku create, tehdään sovelluksen hakemistosta git-repositorio, commitoidaan koodi ja siirretään se Herokuun komennolla git push heroku master.

Jos kaikki meni hyvin, sovellus toimii:

fullstack content

Jos ei, vikaa voi selvittää herokun lokeja lukemalla, eli komennolla heroku logs.

HUOM ainakin alussa on järkevää tarkkailla Herokussa olevan sovelluksen lokeja koko ajan. Parhaiten tämä onnistuu antamalla komento heroku logs -t, jolloin logit tulevat konsoliin sitä mukaan kun palvelimella tapahtuu jotain.

Myös frontend toimii Herokussa olevan backendin avulla. Voit varmistaa asian muuttamalla frontendiin määritellyn backendin osoitteen viittaamaan http://localhost:3001:n sijaan Herokussa olevaan backendiin.

Seuraavaksi herää kysymys miten saamme myös frontendin internettiin? Vaihtoehtoja on useita, mutta käydään seuraavaksi läpi yksi niistä.

Frontendin tuotantoversio

Olemme toistaiseksi suorittaneet React-koodia sovelluskehitysmoodissa, missä sovellus on konfiguroitu antamaan havainnollisia virheilmoituksia, päivittämään koodiin tehdyt muutokset automaattisesti selaimeen ym.

Kun sovellus viedään tuotantoon, täytyy siitä tehdä production build eli tuotantoa varten optimoitu versio.

create-react-app:in avulla tehdyistä sovelluksista saadaan muodostettua tuotantoversio komennolla npm run build.

Suoritetaan nyt komento frontendin projektin juuressa.

Komennon seurauksena syntyy hakemiston build (joka sisältää jo sovelluksen ainoan html-tiedoston index.html) sisään hakemisto static, minkä alle generoituu sovelluksen Javascript-koodin minifioitu versio. Vaikka sovelluksen koodi on kirjoitettu useaan tiedostoon, generoituu kaikki Javascript yhteen tiedostoon, samaan tiedostoon tulee itseasiassa myös kaikkien sovelluksen koodin tarvitsemien riippuvuuksien koodi.

Minifioitu koodi ei ole miellyttävää luettavaa. Koodin alku näyttää seuraavalta:

!function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c<i.length;c++)f=i[c],o[f]&&s.push(o[f][0]),o[f]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var l=t[i];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={2:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})

Staattisten tiedostojen tarjoaminen backendistä

Eräs mahdollisuus frontendin tuotantoon viemiseen on kopioida tuotantokoodi, eli hakemisto build backendin repositorion juureen ja määritellä backend näyttämään pääsivunaan frontendin pääsivu, eli tiedosto build/index.html.

Aloitetaan kopioimalla frontendin tuotantokoodi backendin alle, projektin juureen. Omalla koneellani kopiointi tapahtuu frontendin hakemistosta käsin komennolla

cp -r build ../../../osa3/notes-backend

Backendin sisältävän hakemiston tulee nyt näyttää seuraavalta:

fullstack content

Jotta saamme expressin näyttämään staattista sisältöä eli sivun index.html ja sen lataaman Javascriptin ym. tarvitsemme expressiin sisäänrakennettua middlewarea static.

Kun lisäämme muiden middlewarejen määrittelyn yhteyteen seuraavan

app.use(express.static('build'))

tarkastaa Express GET-tyyppisten HTTP-pyyntöjen yhteydessä ensin löytyykö pyynnön polkua vastaavan nimistä tiedostoa hakemistosta build. Jos löytyy, palauttaa express tiedoston.

Nyt HTTP GET -pyyntö osoitteeseen www.palvelimenosoite.com/index.html tai www.palvelimenosoite.com näyttää Reactilla tehdyn frontendin. GET-pyynnön esim. osoitteeseen www.palvelimenosoite.com/notes hoitaa backendin koodi.

Koska tässä tapauksessa sekä frontend että backend toimivat samassa osoitteessa, voidaan React-sovelluksessa tapahtuva backendin baseUrl määritellä suhteellisena URL:ina, eli ilman palvelinta yksilöivää osaa:

import axios from 'axios'
const baseUrl = '/notes'
const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

// ...

Muutoksen jälkeen on luotava uusi production build ja kopioitava se backendin repositorion juureen.

Sovellusta voidaan käyttää nyt backendin osoitteesta http://localhost:3001:

fullstack content

Sovelluksemme toiminta vastaa nyt täysin osan 0 luvussa Single page app läpikäydyn esimerkkisovelluksen toimintaa.

Kun mennään selaimella osoitteeseen http://localhost:3001 palauttaa palvelin hakemistossa build olevan tiedoston index.html, jonka sisältö hieman tiivistettynä on seuraava:

<head>
  <meta charset="utf-8"/>
  <title>React App</title>
  <link href="/static/css/main.f9a47af2.chunk.css" rel="stylesheet">
</head>
<body>
  <div id="root"></div>
  <script src="/static/js/1.578f4ea1.chunk.js"></script>
  <script src="/static/js/main.104ca08d.chunk.js"></script>
</body>
</html>

Sivu sisältää ohjeen ladata sovelluksen tyylit määrittelevän CSS-tiedoston, sekä kaksi script-tagia, joiden ansiosta selain lataa sovelluksen Javascript-koodin, eli varsinaisen React-sovelluksen.

React-koodi hakee palvelimelta muistiinpanot osoitteesta http://localhost:3001/notes ja renderöi ne ruudulle. Selaimen ja palvelimen kommunikaatio selviää tuttuun tapaan konsolin välilehdeltä Network:

fullstack content

Kun sovelluksen "internettiin vietävä" versio todetaan toimivan paikallisesti, commitoidaan frontendin tuotantoversio backendin repositorioon ja pushataan koodi uudelleen herokuun.

Sovellus toimii moitteettomasti lukuunottamatta vielä backendiin toteuttamatonta muistiinpanon tärkeyden muuttamista:

fullstack content

Sovelluksemme tallettama tieto ei ole ikuisesti pysyvää, sillä sovellus tallettaa muistiinpanot muuttujaan. Jos sovellus kaatuu tai se uudelleenkäynnistetään, kaikki tiedot katoavat.

Tarvitsemme sovelluksellemme tietokannan. Ennen tietokannan käyttöönottoa katsotaan kuitenkin vielä muutamaa asiaa.

Frontendin deployauksen suoraviivaistus

Jotta uuden frontendin version generointi onnistuisi jatkossa ilman turhia manuaalisia askelia, tehdään frontendin repositorion juureen yksinkertainen shell-scripti, joka suorittaa uuden tuotantoversion buildaamisen eli komennon npm run build ja sen siirron backendin alle. Annetaan skriptille nimeksi deploy.sh. Sisältö on seuraava

#!/bin/sh
npm run build
rm -rf ../../osa3/notebackend/build
cp -r build ../../osa3/notebackend/

Skriptille pitää antaa vielä suoritusoikeudet:

chmod u+x deploy.sh

Skripti voidaan suorittaa frontendin juuresta komennolla ./deploy.sh

Backendin urlit

Backendin tarjoama muistiinpanojen käsittelyn rajapinta on nyt suoraan sovelluksen URL:in https://gentle-ravine-74840.herokuapp.com/ alla. Eli https://gentle-ravine-74840.herokuapp.com//notes on kaikkien mustiinpanojen lista ym. Koska backendin roolina on tarjota frontendille koneluettava rajapinta, eli API, olisi ehkä parempi erottaa API:n tarjoama osoitteisto selkeämmin, esim. aloittamalla kaikki sanalla api.

Tehdään muutos ensin muuttamalla käsin kaikki backendin routet:

//...
app.get('/api/notes', (request, response) => {
  response.json(notes)
});
//...

Frontendin koodiin riittää seuraava muutos

import axios from 'axios'
const baseUrl = '/api/notes'
const getAll = () => {
  const request = axios.get(baseUrl)
  return request.then(response => response.data)
}

// ...

Muutosten jälkeen esim. kaikki muistiinpanot tarjoavan API-endpointin osoite on https://gentle-ravine-74840.herokuapp.com/api/notes

fullstack content

Frontend on edelleen sovelluksen juuressa eli osoitteessa https://fullstack-notes.herokuapp.com/.

Sivuhuomautus: API:en versiointi

Joskus API:n urleissa ilmaistaan myös API:n versio. Eri versioita saatetaan tarvita, jos aikojen kuluessa API:in tehdään laajennuksia, jotka ilman versiointia hajoittaisivat olemassaolevia osia ohjelmista. Versioinnin avulla voidaan tuoda vanhojen rinnalle uusia, hieman eri tavalla toimivia versioita API:sta.

API:n version ilmaiseminen URL:issa ei kuitenkaan ole välttämättä, ainakaan kaikkien mielestä järkevää vaikka tapaa paljon käytetäänkin. Oikeasta tavasta API:n versiointiin kiistellään ympäri internettiä.

Proxy

Frontendiin tehtyjen muutosten seurauksena on nyt se, että kun suoritamme frontendiä sovelluskehitysmoodissa, eli käynnistämällä sen komennolla npm start, yhteys backendiin ei toimi:

fullstack content

Syynä tälle on se, että backendin osoite muutettiin suhteellisesti määritellyksi:

const baseUrl = '/api/notes'

Koska frontend toimii osoitteessa localhost:3000, menevät backendiin tehtävät pyynnöt väärään osoitteeseen localhost:3000/api/notes. Backend toimii kuitenkin osoitteessa localhost:3001

create-react-app:illa luoduissa projekteissa ongelma on helppo ratkaista. Riittää, että frontendin repositorion tiedostoon package.json lisätään seuraava määritelmä:

{
  "dependencies": {
    // ...
  },
  "scripts": {
    // ...
  },
  "proxy": "http://localhost:3001"}

Uudelleenkäynnistyksen jälkeen Reactin sovelluskehitysympäristö toimii proxynä ja jos React-koodi tekee HTTP-pyynnön palvelimen http://localhost:3000 johonkin osoitteeseen joka ei ole React-sovelluksen vastuulla (eli kyse ei ole esim. sovelluksen Javascript-koodin tai CSS:n lataamisesta), lähetetään pyyntö edelleen osoitteessa http://localhost:3001 olevalle palvelimelle.

Nyt myös frontend on kunnossa, se toimii sekä sovelluskehitysmoodissa että tuotannossa yhdessä palvelimen kanssa.

Eräs negatiivinen puoli käyttämässämme lähestymistavassa on se, että sovelluksen uuden version tuotantoon vieminen edellyttää frontendin koodin tuotantoversion generoinnista ja sen backendin repositorion kopioimisesta huolehtivan skriptin delpoy.sh suorittamisen. Tämä taas hankaloittaa automatisoidun deployment pipelinen toteuttamista. Deployment pipelinellä tarkoitetaan automatisoitua ja hallittua tapaa viedä koodi sovelluskehittäjän koneelta erilaisten testien ja laadunhallinnallisten vaiheiden kautta tuotantoympäristöön.

Tähänkin on useita erilaisia ratkaisuja (esim. sekä frontendin että backendin sijoittaminen samaan repositorioon), emme kuitenkaan nyt mene niihin.

Myös frontendin koodin deployaaminen omana sovelluksenaan voi joissain tilanteissa olla järkevää. create-react-app:in avulla luotujen sovellusten osalta se on suoraviivaista.

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part3-3.