Osan 7 oppimistavoitteet

  • Webpack
    • Babel: transpilaus, polyfillit
    • Minifiointi
    • Suoritusympäristöt (development/production)
  • Tyylien lisääminen sovellukseen
    • CSS-moduulit
    • Styled components
  • Testaus
    • Headless browser testing
    • Puppeteer
  • React
    • Isompien sovellusten komponenttien organisointi
    • Palvelimella tapahtuvien muutosten välittäminen frontendiin
    • Sovelluksen rakenne jos frontti ja backend kaikki samassa repossa
    • Virtual DOM
  • React/node-sovellusten tietoturva
  • Tyypitys
    • PropTypes revisited
    • Flow
    • typescript
  • Tulevaisuuden trendejä
    • Server side rendering
    • Progessive web apps
    • Microservice-arkkitehtuuri
    • Serverless
    • GraphQL

Tehtävät

Melkein kaikki osan 7 tehtävistä ovat koko kurssin sisältöä kertaavia, voit aloittaa tehtävien tekemisen vaikka heti, vain muutama tehtävistä edellyttää tämän osan teorian läpikäyntiä.

Webpack

React on ollut jossain määrin kuuluisa siitä, että sovelluskehityksen edellyttämien työkalujen konfigurointi on ollut hyvin hankalaa. Kiitos create-react-app:in, sovelluskehitys Reactilla on kuitenkin nykyään tuskatonta, parempaa työskentelyflowta on tuskin ollut koskaan Javascriptillä tehtävässä selainpuolen sovelluskehityksessä.

Emme voi kuitenkaan turvautua ikuisesti create-react-app:in magiaan ja nyt onkin aika selvittää mitä kaikkea taustalla on. Avainasemassa React-sovelluksen toimintakuntoon saattamisessa on webpack-niminen työkalu.

bundlaus

Olemme toteuttaneet sovelluksia jakamalla koodin moduuleihin, joita on importattu niitä tarvitseviin paikkoihin. Vaikka ES6-moduulit ovatkin Javascript-standardissa määriteltyjä, ei mikään selain vielä osaa käsitellä moduuleihin jaettua koodia.

Selainta varten moduuleissa oleva koodi bundlataan, eli siitä muodostetaan yksittäinen, kaiken koodin sisältävä tiedosto. Kun veimme Reactilla toteutetun frontendin tuotantoon osan 3 luvussa Frontendin tuotantoversio, suoritimme bundlauksen komennolla npm run build. Konepellin alla kyseinen npm-skripti suorittaa bundlauksen webpackia hyväksi käyttäen. Tuloksena on joukko hakemistoon build sijoitettavia tiedostoja:

├── asset-manifest.json
├── favicon.ico
├── index.html
├── manifest.json
├── service-worker.js
└── static
    ├── css
    │   ├── main.1b1453df.css
    │   └── main.1b1453df.css.map
    └── js
        ├── main.54f11b10.js
        └── main.54f11b10.js.map

Hakemiston juuressa oleva sovelluksen “päätiedosto” index.html lataa script-tagin avulla bundlatun Javascript-tiedoston:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>React App</title>
  <link href="/static/css/main.1b1453df.css"rel="stylesheet">
</head>
<body>
  <div id="root"></div>
  <script type="text/javascript" src="/static/js/main.54f11b10.js"></script>
  </body>
</html>

Kuten esimerkistä näemme, create-react-app:illa tehdyssä sovelluksessa bundlataan Javascriptin lisäksi sovelluksen CSS-määrittelyt tiedostoon static/css/main.1b1453df.css

Käytännössä bundlaus tapahtuu siten, että sovelluksen Javascriptille määritellään alkupiste, usein tiedosto index.js, ja bundlauksen yhteydessä webpack ottaa mukaan kaiken koodin mitä alkupiste importtaa, sekä importattujen koodien importtaamat koodit, jne.

Koska osa importeista on kirjastoja, kuten React, Redux ja Axios, bundlattuun javascript-tiedostoon tulee myös kaikkien näiden sisältö.

Vanha tapa jakaa sovelluksen koodi moneen tiedostoon perustui siihen, että index.html latasi kaikki sovelluksen tarvitsemat erilliset Javascript-tiedostot script-tagien avulla. Tämä on kuitenkin tehotonta, sillä jokaisen tiedoston lataaminen aiheuttaa pienen overheadin ja nykyään pääosin suositaankin koodin bundlaamista yksittäiseksi tiedostoksi.

Tehdään nyt React-projektille sopiva webpack-konfiguraatio kokonaan käsin.

Luodaan projektia varten hakemisto ja sen sisälle seuraavat hakemistot (build ja src) sekä tiedostot:

├── dist
├── package.json
├── src
│   └── index.js
└── webpack.config.js

Tiedoston package.json sisältö voi olla esim. seuraava:

{
  "name": "webpack-osa7",
  "version": "0.0.1",
  "description": "practising webpack",
  "scripts": {
  },
  "license": "MIT"
}

Asennetaan webpack komennolla

npm install --save-dev webpack webpack-cli

Webpackin toiminta konfiguroidaan tiedostoon webpack.config.js, laitetaan sen alustavaksi sisällöksi seuraava

const path = require('path')

const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  }
}
module.exports = config

Määritellään sitten npm-skripti build jonka avulla bundlaus suoritetaan

// ...
"scripts": {
  "build": "webpack --mode=development"
},
// ...

Lisätään hieman koodia tiedostoon src/index.js:

const hello = (name) => {
  console.log(`hello ${name}`)
}

Kun nyt suoritamme komennon npm run build webpack bundlaa koodin. Tuloksena on hakemistoon dist sijoitettava tiedosto main.js:

Tiedostossa on paljon erikoisen näköistä tavaraa. Lopussa on mukana myös kirjoittamamme koodi.

Lisätään hakemistoon src tiedosto App.js ja sille sisältö

const App = () => {
  return null
}

export default App

Importataan ja käytetään modulia App tiedostossa index.js

import App from './App'

const hello = (name) => {
  console.log(`hello ${name}`)
}

App()

Kun nyt suoritamme bundlauksen komennolla npm run build huomaamme webpackin havainneen molemmat tiedostot:

Kirjoittamamme koodi löytyy melko kryptisesti muotoiltuna bundlen lopussa:

/***/ "./src/App.js":
/*!********************!*\
  !*** ./src/App.js ***!
  \********************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\nconst App = () => {\n  return null;\n};\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (App);\n\n//# sourceURL=webpack:///./src/App.js?");

/***/ }),

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _App__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App */ \"./src/App.js\");\n\n\nconst hello = name => {\n  console.log(`hello ${name}`);\n};\n\nObject(_App__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n\n//# sourceURL=webpack:///./src/index.js?");

Konfiguraatiotiedosto

Katsotaan nyt tarkemmin konfiguraation webpack.config.js tämänhetkistä sisältöä:

const path = require('path')

const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js'
  }
}
module.exports = config

Konfiguraatio on Javascriptia ja tapahtuu eksporttaamalla määrittelyt sisältävä olio Noden moduulisyntaksilla.

Tämän hetkinen minimaalinen määrittely on aika ilmeinen, kenttä entry kertoo sen tiedoston, mistä bundlaus aloitetaan.

Kenttä output taas kertoo minne muodostettu bundle sijoitetaan. Kohdehakemisto täytyy määritellä absoluuttisena polkuna, se taas onnistuu helposti path.resolve-metodilla. __dirname on Noden globaali muuttuja, joka viittaa nykyiseen hakemistoon.

Webpack 4

Helmikuun viimeisten päivien aikana julkaistu Webpackin versio 4 on vähentänyt välttämättömän konfiguroinnin määrää määrittelemällä Webpackille joukon oletusarvoisia konfiguraatioita.

Konfiguraatiossamme entryllä ja outputilla on niiden oletusarvo, eli voisimme myös jättää ne määrittelemättä, ja tiedoston webpack.config.js sisällöksi kävisi:

const config = {
}
module.exports = config

Jätämme kuitenkin entryn ja outputin määrittelyt tiedostoon.

Reactin bundlaaminen

Muutetaan sitten sovellus minimalistiseksi React-sovellukseksi. Asennetaan tarvittavat kirjastot

npm install --save react react-dom

Liitetään tavanomaiset loitsut tiedostoon index.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
  <App />,
  document.getElementById('root')
)

ja muutetaan App.js muotoon

import React from 'react'

const App = () => (
  <div>hello webpack</div>
)

export default App

Tarvitsemme sovellukselle myös “pääsivuna” toimivan tiedoston dist/index.html joka lataa script-tagin avulla bundlatun Javascriptin:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>React App</title>
</head>
<body>
  <div id="root"></div>
  <script type="text/javascript" src="./main.js"></script>
</body>
</html>

Kun bundlaamme sovelluksen, törmäämme kuitenkin ongelmaan

Loaderit

Webpack mainitsee että saatamme tarvita loaderin tiedoston App.js käsittelyyn. Webpack ymmärtää itse vain Javascriptia ja vaikka se saattaa meiltä matkan varrella olla unohtunutkin, käytämme Reactia ohjelmoidessamme JSX:ää näkymien renderöintiin, eli esim. seuraava

const App = () => (
  <div>hello webpack</div>
)

ei ole “normaalia” Javascriptia, vaan JSX:n tarjoama syntaktinen oikotie määritellä div-tagiä vastaava React-elementti.

Loaderien avulla on mahdollista kertoa webpackille miten tiedostot tulee käsitellä ennen niiden bundlausta.

Määritellään projektiimme Reactin käyttämän JSX:n normaaliksi Javascriptiksi muuntava loaderi:

const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        query: {
          presets: ['react']
        }
      }
    ]
  }
}

Loaderit määritellään kentän module alle sijoitettavaan taulukkoon rules.

Yksittäisen loaderin määrittely on kolmiosainen:

{
  test: /\.js$/,
  loader: 'babel-loader',
  query: {
    presets: ['react']
  }
}

Kenttä test määrittelee että käsitellään .js-päätteisiä tiedostoja, loader kertoo että käsittely tapahtuu babel-loader:illa. Kenttä query taas antaa loaderille sen toimintaa ohjaavia parametreja.

Asennetaan loader ja sen tarvitsemat kirjastot kehitysaikaiseksi riippuvuudeksi:

npm install --save-dev babel-core babel-loader babel-preset-react

Nyt bundlaus onnistuu.

Jos katsomme bundlattua koodia, huomaamme, että komponentti App on muuttunut muotoon

const App = () => 
  react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(
    'div',
    null,
    'hello webpack'
  )

Eli JSX-syntaksin sijaan komponentit luodaan pelkällä Javascriptilla käyttäen Reactin funktiota createElement.

Sovellusta voi nyt kokeilla avaamalla tiedoston dist/index.html_ selaimen open file -toiminnolla:

Tässä on jo melkein kaikki mitä tarvitsemme React-sovelluskehitykseen.

Transpilaus

Prosessista, joka muuttaa Javascriptia muodosta toiseen käytetään englanninkielistä termiä transpiling, joka taas on termi, joka viittaa koodin kääntämiseen (compile) sitä muuntamalla (transform). Suomenkielisen termin puuttuessa käytämme prosessista tällä kurssilla nimitystä transpilaus.

Edellisen luvun konfiguraation avulla siis transpiloimme JSX:ää sisältävän Javascriptin normaaliksi Javascriptiksi tämän hetken johtavan työkalun babelin avulla.

Kuten osassa 1 jo mainittiin, läheskään kaikki selaimet eivät vielä osaa Javascriptin uusimpien versioiden ES6:n ja ES7:n ominaisuuksia ja tämän takia koodi yleensä transpiloidaan käyttämään vanhempaa Javascript-syntaksia ES5:ttä.

Babelin suorittama transpilointiprosessi määritellään pluginien avulla. Käytännössä useimmiten käytetään valmiita presetejä, eli useamman sopivan pluginin joukkoa.

Tällä hetkellä sovelluksemme transpiloinnissa käytetään presetiä react:

{
  test: /\.js$/,
  loader: 'babel-loader',
  query: {
    presets: ['react']
  }
}

Otetaan käyttöön preset env, joka sisältää kaiken hyödyllisen, minkä avulla uusimman standardin mukainen koodi saadaan transpiloitua ES5-standardin mukaiseksi koodiksi:

{
  test: /\.js$/,
  loader: 'babel-loader',
  query: {
    presets: ['env', 'react']
  }
}

Preset asennetaan komennolla

npm install babel-preset-env --save-dev

Kun nyt transpiloimme koodin, muuttuu se vanhan koulukunnan Javascriptiksi. Komponentin App määrittely näyttää seuraavalta:

var App = function App() {
  return _react2.default.createElement(
    'div',
    null,
    'hello webpack'
  );
};

Muuttujan määrittely tapahtuu avainsanan var avulla, sillä ES5 ei tunne avainsanaa const. Myöskään nuolifunktiot eivät ole käytössä, joten funktiomäärittely käyttää avainsanaa function.

CSS

Lisätään sovellukseemme hieman CSS:ää. Tehdään tiedosto src/index.css

.container {
  margin: 10;
  background-color: #dee8e4
}

Määritellään tyyli käytettäväksi komponentissa App

const App = () => (
  <div className="container">
    hello webpack
  </div>
)

ja importataan se tiedostossa index.js

import './index.css'

Transpilointi hajoaa, ja CSS:ää varten onkin otettava käyttöön css- ja style-loaderit:

{
  loaders: [
    {
      test: /\.js$/,
      loader: 'babel-loader',
      query: {
        presets: ['env', 'react']
      }
    },
    {
      test: /\.css$/,
      loaders: ['style-loader', 'css-loader']
    }
  ]
}

css-loaderin tehtävänä on ladata CSS-tiedostot, ja style-loader generoi koodiin CSS:t sisältävän style-elementin.

Näin konfiguroituna CSS-määrittelyt sisällytetään sovelluksen Javascriptin sisältävään tiedostoon main.js. Sovelluksen päätiedostossa index.html ei siis ole tarvetta erikseen ladata CSS:ää.

CSS voidaan tarpeen vaatiessa myös generoida omaan tiedostoonsa esim. extract-text-pluginin avulla.

Kun loaderit asennetaan

npm install style-loader css-loader --save-dev

bundlaus toimii taas ja sovellus saa uudet tyylit.

Webpack-dev-server

Sovelluskehitys onnistuu jo, mutta development workflow on suorastaan hirveä (alkaa jo muistuttaa Javalla tapahtuvaa sovelluskehitystä…), muutosten jälkeen koodin on bundlattava ja selain uudelleenladattava jos haluamme testata koodia.

Ratkaisun tarjoaa webpack-dev-server. Asennetaan se komennolla

npm install --save-dev webpack-dev-server

Määritellään dev-serverin käynnistävä npm-skripti (äsken lisätty skripti watch on poistettu koska sille ei ole käyttöä):

{
  // ...
  "scripts": {
    "build": "webpack --mode=development",
    "start": "webpack-dev-server --mode=development"
  },
  // ...
}

Lisätään tiedostoon webpack.config.js kenttä devServer

const config = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  devServer: {
    contentBase: path.resolve(__dirname, "dist"),
    compress: true,
    port: 3000
  },
  // ...
}

Komento npm start käynnistää nyt dev-serverin porttiin, eli sovelluskehitys tapahtuu avaamalla tuttuun tapaan selain osoitteeseen http://localhost:3000. Kun teemme koodiin muutoksia, reloadaa selain automaattisesti itsensä.

Päivitysprosessi on nopea, dev-serveriä käytettäessä webpack ei bundlaa koodia normaaliin tapaan tiedostoksi main.js, bundlauksen tuotos on olemassa ainoastaan keskusmuistissa.

Laajennetaan koodia muuttamalla komponentin App määrittelyä seuraavasti:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 0
    }
  }

  render() {
    return (
      <div className="container">
        <p>hello webpack {this.state.counter} clicks</p>
        <button onClick={()=>this.setState({counter: this.state.counter+1})}>click</button>
      </div>
    )
  }
}

Kannattaa huomata, että virheviestit eivät renderöidy selaimeen kuten create-react-app:illa tehdyissä sovelluksissa, eli on seurattava tarkasti konsolia:

Sovellus toimii hyvin ja kehitys on melko sujuvaa.

Sourcemappaus

Erotetaan napin klikkauksenkäsittelijä omaksi funktioksi:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 0
    }
  }

  onClick() {
    this.setState({ counter: this.state.counter + 1 })
  }

  render() {
    return (
      <div className="container">
        <p>hello webpack {this.state.counter} clicks</p>
        <button onClick={this.onClick}>click</button>
      </div>
    )
  }
}

Sovellus ei enää toimi, ja konsoli kertoo virheestä

Tiedämme tietenkin nyt että virhe on metodissa onClick, mutta jos olisi kyse suuremmasta sovelluksesta, on virheilmoitus sikäli hyvin ikävä, että sen ilmoittama paikka:

App.js:38 Uncaught TypeError: Cannot read property 'setState' of undefined
    at onClick (App.js:38)

ei vastaa alkuperäisen koodin virheen sijaintia. Jos klikkaamme virheilmoitusta, huomaamme, että näytettävä koodi on jotain ihan muuta kuin kirjoittamamme koodi:

Haluamme tietenkin, että virheilmoitusten yhteydessä näytetään kirjoittamamme koodi.

Korjaus on onneksi hyvin helppo, pyydetään webpackia generoimaan bundlelle ns. source map, jonka avulla bundlea suoritettaessa tapahtuva virhe on mahdollista mäpätä alkuperäisen koodin vastaavaan kohtaan.

Source map saadaan generoitua lisäämällä konfiguraatioon kenttä devtool ja sen arvoksi ‘source-map’:

const config = {
  entry: './src/index.js',
  output: {
    // ...
  },
  devServer: {
    // ...
  },
  devtool: 'source-map',
  // ..
}

Konfiguraatioiden muuttuessa webpack tulee käynnistää uudelleen, on tosin mahdollista konfiguroida webpack tarkkailemaan konfiguraatioiden muutoksia, mutta emme tee sitä.

Nyt virheilmoitus on hyvä

Source mapin käyttö mahdollistaa myös chromen debuggerin luontevan käytön

Kyseinen virhe on siis jo osasta 1 tuttu this:in kadottaminen. Korjataan ongelma määrittelemällä metodi uudelleen meille jo kovin tutulla syntaksilla:

onClick = () => {
  this.setState({ counter: this.state.counter + 1 })
}

Tästä aiheutuu kuitenkin virheilmoitus

Virhe johtuu siitä, että käyttämämme syntaksi ei ole vielä mukana Javascriptin uusimmassa standardissa ES7. Saamme syntaksin käyttöön asentamalla transform-class-properties-pluginin komennolla

npm install babel-plugin-transform-class-properties --save-dev

ja kehottamalla babel-loader:ia käyttämään pluginia:

{
  test: /\.js$/,
  loader: 'babel-loader',
  query: {
    presets: ['env', 'react'],
    plugins: [require('babel-plugin-transform-class-properties')]
  }
}

Koodin minifiointi

Kun sovellus viedään tuotantoon, on siis käytössä tiedostoon main.js webpackin generoima koodi. Vaikka sovelluksemme sisältää omaa koodia vain muutaman rivin, on tiedoston main.js koko 557450 tavua, sillä se sisältää myös kaiken React-kirjaston koodin. Tiedoston koollahan on sikäli väliä, että selain joutuu lataamaan tiedoston kun sovellusta aletaan käyttämään. Nopeilla internetyhteyksillä 557450 tavua ei sinänsä ole ongelma, mutta jos mukaan sisällytetään enemmän kirjastoja, alkaa sovelluksen lataaminen pikkuhiljaa hidastua etenkin mobiilikäytössä.

Jos tiedoston sisältöä tarkastelee, huomaa että sitä voisi optimoida huomattavasti koon suhteen esim. poistamalla kommentit. Tiedostoa ei kuitenkaan kannata lähteä optimoimaan käsin, sillä tarkoitusta varten on olemassa monia työkaluja.

Javascript-tiedostojen optimointiprosessista käytetään nimitystä minifiointi. Alan johtava työkalu tällä hetkellä lienee UglifyJS.

Webpackin versiosta 4 alkaen pluginia ei ole tarvinnut konfiguroida erikseen, riittää että muutetaan tiedoston package.json määrittelyä siten, että koodin bundlaus tapahtumaan production-moodissa:

{
  "name": "webpack-osa7",
  "version": "0.0.1",
  "description": "practising webpack",
  "scripts": {
    "build": "webpack --mode=production",
    "start": "webpack-dev-server --mode=development"
  },
  "license": "MIT",
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
  }
}

Kun sovellus bundlataan uudelleen, pienenee tuloksena oleva main.js mukavasti

-rw-r--r--  1 mluukkai  984178727  101944 Mar  3 21:29 main.js

Minifioinnin lopputulos on kuin vanhan liiton c-koodia, kommentit ja jopa turhat välilyönnit ja rivinvaihtot on poistettu ja muuttujanimet ovat yksikirjaimisia:

function h(){if(!d){var e=u(p);d=!0;for(var t=c.length;t;){for(s=c,c=[];++f<t;)s&&s[f].run();f=-1,t=c.length}s=null,d=!1,function(e){if(o===clearTimeout)return clearTimeout(e);if((o===l||!o)&&clearTimeout)return o=clearTimeout,clearTimeout(e);try{o(e)}catch(t){try{return o.call(null,e)}catch(t){return o.call(this,e)}}}(e)}}a.nextTick=function(e){var t=new Array(arguments.length-1);if(arguments.length>1)

Sovelluskehitys- ja tuotantokonfiguraatio

Lisätään sovellukselle backend. Käytetään jo tutuksi käynyttä muistiinpanoja tarjoavaa palvelua.

Talletetaan seuraava sisältö tiedostoon db.json

{
  "notes":[
    {
      "important": true,
      "content": "HTML on helppoa",
      "id": "5a3b8481bb01f9cb00ccb4a9"
    },
    {
      "important": false,
      "content": "Mongo osaa tallettaa oliot",
      "id": "5a3b920a61e8c8d3f484bdd0"
    }
  ]
}

Tarkoituksena on konfiguroida sovellus webpackin avulla siten, että paikallisesti sovellusta kehitettäessä käytetään backendina portissa 3001 toimivaa json-serveriä.

Bundlattu tiedosto laitetaan sitten käyttämään todellista, osoitteessa https://radiant-plateau-25399.herokuapp.com/api/notes olevaa backendia.

Asennetaan axios, käynnistetään json-server ja muokataan komponenttia App seuraavasti:

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      counter: 0,
      noteCount: 0
    }
  }

  componentDidMount() {
    axios.get('http://localhost:3001/notes').then(result => {
      this.setState({ noteCount: result.data.length })
    })
  }

  onClick = () => {
    this.setState({ counter: this.state.counter + 1 })
  }

  render() {
    return (
      <div className="container">
        <p>hello webpack {this.state.counter} clicks</p>
        <button onClick={this.onClick}>click</button>
        <p>{this.state.noteCount} notes in server</p>
      </div>
    )
  }
}

Koodissa on nyt kovakoodattuna sovelluskehityksessä käytettävän palvelimen osoite. Miten saamme osoitteen hallitusti muutettua osoittamaan internetissä olevaan backendiin bundlatessamme koodin?

Muutetaan webpack.config.js oliosta funktioksi:

const path = require('path')

const config = (env, argv) => {

  return {
    entry: './src/index.js',
    output: {
      // ...
    },
    devServer: {
      // ...
    },
    devtool: 'source-map',
    module: {
      // ...
    },
    plugins: [
      // ...
    ]
  }
}

module.exports = config

Määrittely on muuten täysin sama, mutta aiemmin eksportattu olio on nyt määritellyn funktion paluuarvo. Funktio saa parametrit env ja argv, joista jälkimmäisen avulla saamme selville npm-skriptissä määritellyn moden.

Webpackin DefinePlugin:in avulla voimme määritellä globaaleja vakioarvoja, joita on mahdollista käyttää bundlattavassa koodissa. Määritellään nyt vakio BACKEND_URL, joka saa eri arvon riippuen siitä ollaanko kehitysympäristössä vai tehdäänkö tuotantoon sopivaa bundlea:

const path = require('path')
const webpack = require('webpack')

const config = (env, argv) => {
  console.log('argv', argv.mode)

  const backend_url = argv.mode === 'production'
    ? 'https://radiant-plateau-25399.herokuapp.com/api/notes'
    : 'http://localhost:3001/notes'

  return {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: 'main.js'
    },
    devServer: {
      contentBase: path.resolve(__dirname, "dist"),
      compress: true,
      port: 3000
    },
    devtool: 'source-map',
    module: {
      // ...
    },  
    plugins: [
      new webpack.DefinePlugin({
        BACKEND_URL: JSON.stringify(backend_url)
      })
    ]    
  }

}

module.exports = config

Määriteltyä vakiota käytetään koodissa seuraavasti:

componentDidMount() {
  axios.get(BACKEND_URL)
    .then(result => {
      this.setState({noteCount: result.data.length})
    })
}

Jos kehitys- ja tuotantokonfiguraatio eriytyvät paljon, saattaa olla hyvä idea eriyttää konfiguraatiot omiin tiedostoihinsa.

Tuotantoversiota eli bundlattua sovellusta on mahdollista kokeilla lokaalisti suorittamalla komento

npx static-server

hakemistossa dist jolloin sovellus käynnistyy oletusarvoisesti osoitteeseen http://localhost:9080.

Polyfill

Sovelluksemme on valmis ja toimii muiden selaimien kohtuullisen uusilla versiolla, mutta Internet Explorerilla sovellus ei toimi. Syynä tähän on se, että axiosin ansiosta koodissa käytetään Promiseja, mikään IE:n versio ei kuitenkaan niitä tue:

On paljon muutakin standardissa määriteltyjä asioita, joita IE ei tue, esim. niinkin harmiton komento kuin taulukoiden find ylittää IE:n kyvyt:

Tälläisessä tilanteessa normaali koodin transpilointi ei auta, sillä transpiloinnissa koodia käännetään uudemmasta Javascript-syntaksista vanhempaan, selaimien paremmin tukemaan syntaksiin. Promiset ovat syntaktisesti täysin IE:n ymmärrettävissä, IE:ltä vain puuttuu toteutus promisesta, samoin on tilanne taulukoiden suhteen, IE:llä taulukoiden find on arvoltaan undefined.

Jos haluamme sovelluksen IE-yhteensopivaksi, tarvitsemme polyfilliä, eli koodia, joka lisää puuttuvan toiminnallisuuden vanhempiin selaimiin.

Polyfillaus on mahdollista hoitaa Webpackin ja Babelin avulla tai asentamalla yksi monista tarjolla olevista polyfill-kirjastoista.

Esim. kirjaston promise-polyfill tarjoaman polyfillin käyttö on todella helppoa, koodiin lisätään seuraava:

import PromisePolyfill from 'promise-polyfill'

if (!window.Promise) {
  window.Promise = PromisePolyfill
}

Jos globaalia Promise-olioa ei ole olemassa, eli selain ei tue promiseja, sijoitetaan polyfillattu promise globaaliin muuttujaan. Jos polyfillattu promise on hyvin toteutettu, muun koodin pitäisi toimia ilman ongelmia.

Kattavahko lista olemassaolevista polyfilleistä löytyy täältä.

Selaimien yhteensopivuus käytettävien API:en suhteen kannattaakin tarkistaa esim. https://caniuse.com-sivustolta tai Mozillan sivuilta.

Eject

Create-react-app käyttää taustalla webpackia. Jos peruskonfiguraatio ei riitä, on projektit mahdollista ejektoida, jolloin kaikki konepellin alla oleva magia häviää, ja konfiguraatiot tallettuvat hakemistoon config ja muokattuun package.json-tiedostoon.

Jos create-react-app:illa tehdyn sovelluksen ejektoi, paluuta ei ole, sen jälkeen kaikesta konfiguroinnista on huolehdittava itse. Konfiguraatiot eivät ole triviaaleimmasta päästä ja create-react-appin ja ejektoinnin sijaan parempi vaihtoehto saattaa joskus olla tehdä itse koko webpack-konfiguraatio.

Ejektoidun sovelluksen konfiguraatioiden lukeminen on suositeltavaa ja sangen opettavaista!

Lisää tyyleistä

Osissa 2 ja 6 on jo katsottu muutamaa tapaa tyylien lisäämiseen eli vanhan koulukunnan yksittäistä CSS-tiedostoa, inline-tyylejä ja UI-frameworkien kuten Bootstrapin käyttöä.

Tapoja on monia muitakin, katsotaan vielä lyhyestä kahta tapaa.

CSS-moduulit

Yksi CSS:n keskeisistä ongelmista on se, että CSS-määrittelyt ovat globaaleja. Suurissa tai jo keskikokoisissakin sovelluksissa tämä aiheuttaa ongelmia, sillä tiettyihin komponentteihin vaikuttavat monissa paikoissa määritellyt tyylit ja lopputulos voi olla vaikeasti ennakoitavissa.

Laitoksen kurssilistasivun alaosassa on itseasiassa eräs ilmentymä tälläisestä ikävästä bugista

Sivulla on monessa paikassa määriteltyjä tyylejä, osa määrittelyistä tulee Drupal-sisällönhallintajärjestelmän oletuskonfiguraatiosta, osa on Drupaliin laitoksella tehtyjä lisäyksiä, osa taas tulee sivun yläosan olemassaolevaa opetustarjontaa näyttävistä syksyllä lisätystä komponenteista. Vika on niin hankala korjata, ettei kukaan ole viitsinyt sitä tehdä.

Demonstroidaan vastaavankaltaista ongelmatilannetta esimerkkisovelluksessamme.

Muutetaan esimerkkitietostoamme siten, että komponentista App irrotetaan osa toiminnallisuudesta komponentteihin Hello ja NoteCount:

import './Hello.css'

const Hello = ({ counter }) => (
  <p className="content">
    hello webpack {counter} clicks!
  </p>
)

export default Hello
import './NoteCount.css'

const NoteCount = ({ noteCount }) => (
  <p className="content">
    {noteCount} notes in server
  </p>
)

export default NoteCount

Molemmat komponentit määrittelevät oman tyylitiedostonsa:

Hello.css

.content {
  background-color: yellow;
}

NoteCount.css:

.content {
  background-color: blue;
}

Koska molemmat komponentit käyttävät samaa CSS-luokan nimeä content, käykin niin että myöhemmin määritelty ylikirjoittaa aiemmin määritellyn, ja molempien tyyli on sama:

Perinteinen tapa kiertää ongelma on ollut käyttää monimutkaisempia CSS-luokan nimiä, esim. Hello_container ja NoteCount_container, tämä muuttuu kuitenkin jossain vaiheessa varsin hankalaksi.

CSS-moduulit tarjoaa tähän erään ratkaisun.

Lyhyesti ilmaisten periaatteena on tehdä CSS-määrittelyistä lähtökohtaisesti lokaaleja, vain yhden komponentin kontekstissa voimassa olevia, joka taas mahdollistaa luontevien CSS-luokkanimien käytön. Käytännössä tämä lokaalius toteutetaan generoimalla konepellin alla CSS-luokille uniikit luokkanimet.

CSS-moduulit voidaan toteuttaa suoraan Webpackin css-loaderin avulla seuraten sivun ohjetta.

Muutetaan tyylejä käyttäviä komponentteja hiukan:

import styles from './Hello.css'

const Hello = ({ counter }) => (
  <p className={styles.content}>
    hello webpack {counter} clicks!
  </p>
)

export default Hello

Erona siis edelliseen on se, että tyyliit “sijoitetaan muuttujaan” styles

import styles from './Hello.css'

Nyt tyylitiedoston määrittelelyihin voi viitata muuttujan styles kautta, ja CSS-luokan liittäminen tapahtuu seuraavasti

<p className={styles.content}>

Vastaava muutos tehdään komponentille NoteCount.

Muutetaan sitten Webpackin konfiguraatiossa olevaa css-loaderin määrittelyä siten että se enabloi CSS-modulit:

{
  test: /\.css$/,
  loaders: [
    'style-loader',
    'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]&sourceMap&-minimize'
  ]
}

Nyt molemmat komponentit saavat omat tyylinsä. Konsolista tarkastelemalla huomaamme, että komponenttien luokille on generoitunut webpackin css-loaderin generoimat uniikit nimet:

CSS-luokan nimen muotoileva osa on css-loaderin yhteydessä oleva

localIdentName=[name]\_\_[local]\_\_\_[hash:base64:5]

Jos olet aikeissa käyttää CSS-moduuleja, kannattaa vilkaista mitä kirjasto react-css-modules tarjoaa.

Styled components

Mielenkiintoisen näkökulman tyylien määrittelyyn tarjoaa ES6:n tagged template literal -syntaksia hyödyntävä styled components -kirjasto.

Tehdään styled-componentsin avulla esimerkkisovellukseemme muutama tyylillinen muutos:

import styled from 'styled-components'

const Button = styled.button `
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid black;
  border-radius: 3px;
`

const Hello = ({ className, counter }) => (
  <p className={className}>
    hello webpack {counter} clicks
  </p>
)

const StyledHello = styled(Hello) `
  color: blue;
  font-weight: bold;
`

class App extends React.Component {
  //...

  render() {
    return (
      <div>
        <StyledHello counter={this.state.counter} />
        <Button onClick={this.onClick}>click</Button>
      </div>
    )
  }
}

Heti alussa luodaan HTML:n button-elementistä jalostettu versio ja sijoitetaan se muuttujaan Button:

const Button = styled.button `
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid black;
  border-radius: 3px;
`

Tyylien määrittelyn syntaksi on varsin mielenkiintoinen.

Määritelty komponentti toimii kuten normaali button ja sovellus renderöi sen normaaliin tapaan:

<Button onClick={this.onClick}>click</Button>

Seuraavaksi koodi määrittelee normaalin React-komponentin

const Hello = ({ className, counter }) => (
  <p className={className}>
    hello webpack {counter} clicks
  </p>
)

ja lisää tälle tyylit metodin styled avulla:

const StyledHello = styled(Hello) `
  color: blue;
  font-weight: bold;
`

Muuttujaan StyledHello sijoitettua tyyleillä jalostettua komponenttia käytetään kuten alkuperäistä:

<StyledHello counter={this.state.counter} />

Sovelluksen ulkoasu seuraavassa:

Sovelluksen end to end -testaus

Palataan vielä hetkeksi testauksen pariin. Aiemmissa osissa teimme sovelluksille yksikkötestejä sekä integraatiotestejä. Katsotaa nyt erästä tapaa tehdä järjestelmää kokonaisuutena tutkivia End to End (E2E) -testejä.

Web-sovellusten E2E-testaus tapahtuu simuloidun selaimen avulla esimerkiksi Selenium-kirjastoa käyttäen. Toinen vaihtoehto on käyttää ns. headless browseria eli selainta, jolla ei ole ollenkaan graafista käyttöliittymää.

Chrome-selain on jo hetken sisältänyt headless-moodin. Käytetään nyt headless chromea sille Node API:n tarjoavan Puppeteer-kirjaston avulla.

Tehdään muutama testi osan 3 muistiinpanosovelluksen “Full stack”-versiolle, joka sisältää sekä backendin että frontin samassa projektissa.

Asennetan puppeteer komennolla

npm install puppeteer --save-dev

Ennen testejä, tehdään kokeiluja varten tiedosto puppeteer.js ja sille seuraava sisältö

const puppeteer = require('puppeteer')

const main = async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('http://localhost:3000')
  await page.screenshot({ path: 'kuva.png' })

  await browser.close()
}

main()

Kun koodi suoritetaan komennolla node puppeteer.js menee headless chrome osoitteeseen http://localhost:3000 ja tallettaa sivulta ottamansa screenshotin tiedostoon kuva.png:

Muutetaan koodia vielä siten, että se kirjottaa sivulla olevaan input-elementtiin

const main = async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('http://localhost:3000')
  await page.type('input', 'Headless Chrome')
  await page.screenshot({ path: 'kuva.png' })
  await browser.close()
}

Screenshot todistaa että näin on todellakin tapahtunut:

Debugatessa voi olla joskus avuksi myös käynnistää selain normaalimoodissa, ja hidastaa testien suoritusta:

const main = async () => {
  const browser = await puppeteer.launch({
    headless: false,
    slowMo: 250       // jokainen operaatio kestää nyt 0.25 sekuntia
  })
  // ...
}

Tehdään sitten muutama testi. Toimiakseen hyvin Jestin kanssa vaaditaan hieman konfiguraatiota, joka onnistuu Jestin dokumentaation ohjetta noudattaen.

Tehdään ensimmäinen testi

describe('note app', () => {

  it('renders main page', async () => {
    const page = await global.__BROWSER__.newPage()
    await page.goto('http://localhost:3000')
    const textContent = await page.$eval('body', el => el.textContent)

    expect(textContent.includes('Muistiinpanot')).toBe(true)
  })

})

Konfiguraatioiden ansiosta viite selaimeen on muuttujassa global.__BROWSER__

Selaimelta pyydetään aluksi page-olio, ja sen metodilla $eval haetaan sivun elementissä body oleva tekstuaalinen sisältö.

Tehdään toinen testi, refaktoroidaan samalla testin yhteinen koodi beforeEach-metodiin:

describe('note app', () => {
  let page
  beforeEach(async () => {
    page = await global.__BROWSER__.newPage();
    await page.goto('http://localhost:3000')
  })

  it('renders main page', async () => {
    const textContent = await page.$eval('body', el => el.textContent)
    expect(textContent.includes('Muistiinpanot')).toBe(true)
  })

  it('renders a note', async () => {
    const textContent = await page.$eval('body', el => el.textContent)
    expect(textContent.includes('HTML on helppoa')).toBe(true)
  })
})

Testi ei yllättäen mene läpi. Jos testissä tulostetaan konsoliin olion page metodilla content palauttama sivun koko sisältö, huomataan että sivulla ei todellakaan ole yhtään muistiinpanoa:

<body>
  <noscript>
    You need to enable JavaScript to run this app.
  </noscript>
  <div id="root"><div><h1>Muistiinpanot</h1><div><button>näytä vain tärkeät</button></div><div class="notes"></div><form><input value=""><button>tallenna</button></form></div></div>
  <script type="text/javascript" src="/static/js/main.js"></script>
</body></html>

Syynä tälle on se, että puppeteer on ollut liian nopea, ja sivu ei ole ehtinyt renderöityä.

Koska muistiinpanot sisältävällä div-elementillä on CSS-luokka wrapper, testi saadaan menemään läpi odottamalla koko kyseisten elementtien renderöitymistä metodin waitForSelector avulla:

it('renders a note', async () => {
  await page.waitForSelector('.wrapper')
  const textContent = await page.$eval('body', el => el.textContent)
  expect(textContent.includes('HTML on helppoa')).toBe(true)
})

Muutetaan testi hieman parempaan muotoon

it('renders a note', async () => {
  await page.waitForSelector('.wrapper')

  const notes = await page.evaluate(() => {
    const elements = [...document.querySelectorAll('.wrapper')]
    return elements.map((e) => e.textContent)
  })

  expect(notes.length>0).toBe(true)
  expect(notes.join().includes('HTML on helppoa')).toBe(true)
})

Jestin issueista löydetyn neuvon avulla testi hakee sivun kaikkien muistiinpanojen sisällöt ja tekee ekspektaatiot niiden avulla.

Lopuksi tehdään testi, joka luo uuden muistiinpanon

it('allows new notes to be added', async () => {
  const id = Math.random()*10000
  const note = `jestin lisäämä muistiinpano ${id}`
  await page.type('input', note)
  await page.click('form button')

  await page.waitForSelector('.notification')  // ilman tätä testi ei mene läpi

  const notes = await page.evaluate(() => {
    const elements = [...document.querySelectorAll('.wrapper')]
    return elements.map((e) => e.textContent)
  })
  expect(notes.join().includes(note)).toBe(true)
})

Lomakkeen täyttäminen on helppoa. Koska sivulla on useita painikkeita, on käytetty CSS-selektoria form button joka hakee sivulta lomakkeen sisällä olevan napin.

Napin painalluksen jälkeen syntyy jälleen potentiaalinen ajoitusongelma jos uuden muistiinpanon sivulle renderöitymistä testataan liian nopeasti. Ongelma on kierretty sillä, että sovellusta on muutettu siten, että se näyttää ruudulla CSS-luokalla notification merkityssä div-elementissä uuden muistiinpanon lisäämisestä kertovan ilmoituksen.

Testausasetelmamme kaipaisi vielä paljon hiomista. Testejä vartan olisi mm. oltava oma tietokanta, jonka tilan testien pitäisi pystyä nollaamaan hallitusti. Nyt testit luottavat siihen että sovellus on käynnissä portissa 3001. Olisi parempi jos testit itse käynnistäisivät ja sammuttaisivat palvelimen.

Lisää aiheesta Puppeteerin Github-sivujen lisäksi esimerkiksi seuraavassa https://www.valentinog.com/blog/ui-testing-jest-puppetteer/

Tyypitys

Javascriptin muuttujien dynaaminen tyypitys aiheuttaa välillä ikäviä bugeja. Osassa 5 käsittelimme lyhyesti PropTypejä, eli mekanismia, jonka avulla React-komponenteille välitettävile propseille on mahdollista tehdä tyyppitarkastuksia.

Viime aikoina on ollut havaittavissa nousevaa kiinnostusta staattiseen tyypitykseen.

Javascriptistä on olemassa useita staattisesti tyypitettyjä versioita, suosituimmat näistä ovat Facebookin kehittämä flow ja Microsofin typescript.

Flown käyttöönottaminen create-react-app:illa toteutettuun sovellukseen on helppoa. Tiedostossa .flowconfig kannattaa ignoroida hakemistoissa node_modules ja dist olevat tiedostot

[ignore]
.*/node_modules/.*
.*/dist/.*

Flow tarkastaa ainoastaan ne tiedostot, joiden alussa on kommentissa oleva merkintä @flow:

// @flow

function sum(n: number, m: number): number {
  return n + m
}

sum('1', 2)

Tyyppitarkastus tapahtuu komennolla npm run flow. Flow huomauttaa virheestä:

Koodi kuitenkin yllättäen toimii, jos koodiin lisätään konsoliin tehtävä tulostus

const summa = sum('1', 2)
console.log(summa)

tulostuu luku 12.

Flow suorittaa koodille ainoastaan tyyppitarkastuksen, babel kääntää flow-tyyppejä sisältävän koodin normaaliksi Javascriptiksi ja tyyppien tarjoma suoja onkin voimassa ainoastaan jos ohjelmoija suorittaa tyyppitarkastuksia.

Kaikissa tapauksissa tyyppejä ei edes ole tarvetta määritellä, joissain tapauksissa flow osaa päätellä itse mikä muuttujien tyypin tulee olla. Esim. seuraavassa tapauksessa

function square(n) {
  return n * n
}

square('5')

flow osaa varoittaa ongelmasta ilman tyyppien määrittelyä

Flown hyvä puoli on keveys, vanhoihinkin projekteihin on helppo ruveta vähitellen lisäämään tyyppejä Flowlla.

Typescript on jossain määrin laajempi ratkaisu ja sen käyttö vaatii Flowia enemmän konfigurointia. Typescript-koodi kirjoitetaan .ts-päätteisiin tiedostoihin ja se tulee kääntää Javascriptiksi. Käännös pystytään toki hoitamaan helposti webpackilla. Toisin kun flown yhteydessä, Typescriptillä tehdyssä koodissa oleva virheellinen tyyppien käyttö johtaa siihen, että koodi ei käänny.

Internetistä löytyy runsaasti Flowta ja Typescriptiä vertailevia artikkeleja, ks esim.:

Muutamia huomioita liittyen Reactiin, Reduxiin ja Nodeen

React-sovelluksen koodin organisointi

Noudatimme useimmissa sovelluksissa periaatetta, missä komponentit sijoitettiin hakemistoon components, reducerit hakemistoon reducers ja palvelimen kanssa kommunikoiva koodi hakemistoon services. Tälläinen organisoimistapa riittää pienehköihin sovelluksiin, mutta komponenttien määrän kasvaessa tarvitaan muunlaisia ratkaisuja. Yhtä oikeaa tapaa ei ole, artikkeli The 100% correct way to structure a React app (or why there’s no such thing) tarjoaa näkökulmia aiheeseen.

Frontti ja backend samassa repositoriossa

Olemme kurssilla tehneet frontendin ja backendin omiin repositorioihinsa. Kyseessä on varsin tyypillinen ratkaisu. Teimme tosin deploymentin kopioimalla frontin bundlatun koodin backendin repositorion sisälle. Toinen, ehkä järkevämpi tilanne olisi ollut deployata frontin koodi erikseen, create-react-appilla tehtyjen sovellusten osalta se on todella helppoa oman buildpackin ansiosta.

Joskus voi kuitenkin olla tilanteita, missä koko sovellus halutaan samaan repositorioon. Tällöin yleinen ratkaisu on sijoittaa package.json ja webpack.config.js hakemiston juureen ja frontin sekä backendin koodi omiin hakemistoihinsa, esim. client ja server.

Erään hyvän lähtökohdan yksirepositorioisen koodin organisoinnille antaa MERN-projektin ylläpitämä MERN starter.

Palvelimella tapahtuvat muutokset

Jos palvelimella olevassa tilassa tapahtuu muutoksia, esim. blogilistapalveluun lisätään uusia blogeja muiden käyttäjien toimesta, tällä kurssilla tekemämme React-frontendit eivät huomaa muutoksia ennen sivujen uudelleenlatausta. Vastaava tilanne tulee eteen, jos frontendistä käynnistetään jotain kauemmin kestävää laskentaa backendiin, miten laskennan tulokset saadaan heijastettua frontediin?

Eräs tapa on suorittaa frontendissa pollausta, eli toistuvia kyselyitä backendin APIin esim. setInterval-komennon avulla.

Edistyneempi tapa on käyttää WebSocketeja, joiden avulla on mahdollista muodostaa kaksisuuntainen kommunikaatiokanava selaimen ja palvelimen välille. Tällöin frontendin ei tarvitse pollata backendia, riittää määritellä takaisinkutsufunktiot tilanteisiin, joissa palvelin lähettää WebSocketin avulla tietoja tilan päivittämisestä.

WebSocketit ovat selaimen tarjoama rajapinta, jolla ei kuitenkaan ole kaikille selaimille vielä täyttä tukea:

WebSocket API:n suoran käyttämisen sijaan onkin suositeltavaa käyttää Socket.io-kirjastoa, joka tarjoaa erilaisia automaattisia fallback-mahdollisuuksia, jos käytettävässä selaimessa ei ole täyttä WebSocket-tukea.

Virtual DOM

Reactin yhteydessä mainitaan usein käsite Virtual DOM. Mistä oikein on kyseä? Kuten osassa 0 mainittiin, selaimet tarjoavat DOM API:n, jota hyväksikäyttäen selaimessa toimiva Javascript voi muokata sivun ulkoasun määritteleviä elementtejä.

Reactia käyttäessä ohjelmoija ei koskaan (tai parempi sanoa yleensä) manipuloi DOM:ia suoraan. React-komponenttien ulkoasun määrittelevä render-metodi palauttaa joukon React-elementtejä. Vaikka osa elementeistä näyttää normaaleilta HTML-elementeiltä

const element = <h1>Hello, world</h1>

eivät nekään ole HTML:ää vaan pohjimmiltaan Javascriptiä olevia React-elementtejä.

Sovelluksen komponenttien ulkoasun määrittelevät React-elementit muodostavat Virtual DOM:in, joka pidetään suorituksen aikana keskusmuistissa.

ReactDOM-kirjaston avulla komponenttien määrittelevä virtuaalinen DOM renderöidään oikeaksi DOM:iksi eli DOM API:n avulla selaimen näytettäväksi:

ReactDOM.render(
  <App />,
  document.getElementById('root')
)

Kun sovelluksen tila muuttuu, määrittyy komponenttien render-metodien ansiosta uusi virtuaalinen DOM. Reactilla on edellinen versio virtual DOM:ista muistissa ja sensijaan että uusi virtuaalinen DOM renderöitäisiin suoraviivaisesti DOM API:n avulla, React laskee mikä on optimaalisin tapa tehdä DOM:iin muutoksia (eli poistaa, lisätä ja muokata DOM:issa olevia elementtejä) siten, että DOM saadaan vastaamaan uutta Virtual DOM:ia.

Reactin roolista sovelluksissa

Materiaalissa ei ole tuotu ehkä riittävän selkeästi esille sitä, että React on ensisijaisesti tarkoitettu näkymien luomisesta huolehtivaksi kirjastoksi. Jos ajatellaan perinteistä Model View Controller -jaottelua, on Reactin toimialaa juurikin View. React on siis sovellusalueeltaan suppeampi kuin esim. Angular, joka on kaiken tarjoava Frontendin MVC-sovelluskehys. Reactia ei kutsutakaan sovelluskehykseksi (framework) vaan kirjastoksi (library).

Pienissä sovelluksissa React-komponenttien tilaan talletetaan sovelluksen käsittelemää dataa, eli komponenttien tilan voi näissä tapaukissa ajatella vastaavan MVC:n modeleita.

React-sovellusten yheydessä ei kuitenkaan yleensä puhuta MVC-arkkitehtuurista ja jos käytössä on Redux niin silloin sovellukset noudattavat Flux-arkkitehtuuria ja Reactin rooliksi jää entistä enemmän pelkkä näkymien muodostaminen. Varsinainen sovelluslogiikka hallitaan Reduxin tilan ja action creatorien avulla. Jos käytössä on osasta 6 tuttu redux thunk, on sovelluslogiikka mahdollista eristää lähes täysin React-koodista.

Koska sekä React että Flux ovat Facebookilla syntyneinä, voi ajatella, että Reactin pitäminen ainoastaan käyttöliittymästä huolehtivana kirjastona on sen oikeaoppista käyttöä. Flux-arkkitehtuurin noudattaminen tuo sovelluksiin tietyn overheadin ja jos on kyse pienestä sovelluksesta tai prototyypistä, saattaa Reatcin “väärinkäyttäminen” olla järkevää, sillä myöskään overengineering ei yleensä johda optimaaliseen tulokseen.

React/node-sovellusten tietoturva

Emme ole vielä maininneet kurssilla sanaakaan tietoturvaan liittyen. Kovin paljoon ei nytkään ole aikaa, ja onneksi laitoksella on MOOC-kurssi Securing Software tähän tärkeään aihepiiriin.

Katsotaan kuitenkin muutamaa kurssispesifistä seikkaa.

The Open Web Application Security Project eli OWASP julkaisee vuosittain listan Websovellusten yleisimmistä turvallisuusuhista. Tuorein lista on täällä. Samat uhat ovat listalla vuodesta toiseen.

Listaykkösenä on injection, joka tarkoittaa sitä, että sovellukseen esim. lomakkeen avulla lähetettävä teksti tulkitaankin aivan eri tavalla kun sovelluskehittäjä on tarkoittanut. Kuuluisin injektioiden muoto lienevät SQL-injektiot.

Esim. jos ei-turvallisessa koodissa tehtäisiin seuravasti muotoiltu SQL-kysely:

let query = "SELECT * FROM Users WHERE name = '" + userName + "';"

Oletetaan että hieman ilkeämielinen käyttäjä Arto Hellas nyt määrittelisi nimekseen

Arto Hell-as'; DROP TABLE Users; --

eli nimi sisältäisi hipsun ', jonka on SQL:ssä merkkijonon aloitus/lopetusmerkki. Tämän seurauksena tulisi suoritetuksi kaksi SQL-operaatiota, joista jälkimmäinen tuhoaisi tietokannan

SELECT * FROM Users WHERE name = 'Arto Hell-as'; DROP TABLE Users; --'

SQL-injektiot estetään sanitoimalla syöte, eli tarkastamalla, että kyselyjen parametrit eivät sisällä kiellettyjä merkkejä, kuten tässä tapauksessa hipsun. Jos kiellettyjä merkkejä löytyy, ne poistetaan korvataan turvallisilla vastineilla escapettamalla.

Myös NoSQL-kantoihin tehtävät injektiohyökkäykset ovat mahdollisia. Mongoose kuitenkin estää ne sanitoimalla kyselyt. Lisää aiheeta esim. täällä.

Cross-site scripting eli XSS on hyökkäys, missä sovellukseen on mahdollista injektoida suoritettavaksi vihollismielistä Javascript-koodia. Jos kokeilemme injektoida esim. muistiinpanosovellukseen seuraavan

<script>alert('Evil XSS attack');</script>

koodia ei suoriteta, vaan koodi renderöityy sivulle ‘tekstinä’:

sillä React huolehtii muuttujissa olevan datan sanitoinnista. Reactin jotkut versiot ovat mahdollistaneet XSS-hyökkäyksiä, aukot on toki korjattu, mutta mikään ei takaa etteikö niitä voisi vielä löytyä.

Käytettyjen kirjastojen suhteen tuleekin olla tarkkana, jos niihin tulee tietoturvapäivityksiä, on kirjastot syytä päivittää omissa sovelluksissa. Expressin tietoturvapäivitykset löytyvät kirjaston dokumentaatiosta ja Nodeen liittyvät blogista.

Riippuvuuksien ajantasaisuuden voi testata komennolla

npm outdated --depth 0

Riippuvuudet saa ajantasaistettua päivittämällä tiedostoa package.json ja suorittamalla komennon npm install. Riippuvuuksien vanhat versiot eivät tietenkään välttämättä ole tietoturvariski.

Node Security Platform valvoo npm:ssä olevien riippuvuuksien turvallisuutta ja tallettaa tietokantaansa kaikki riippuvuuksissa havaitut tietoturvaongelmat. Oman projektin käyttämien riippuvuuksien turvallisuustilanne on helppo tarkistaa komentoriviltä toimivan nsp-työkalun avulla.

Vaikka työkalumme sisältää muutaman ei-ajantasaisen riippuvuuden, ei tietoturvaongelmia ole:

Toinen vastaava palvelu riippuvuuksien turvallisuuden tarkkailuun on Snyk.

Eräs OWASP:in listan mainitsemista uhista on Broken Authentication ja siihen liittyvä Broken Access Control. Käyttämämme token-perustainen autentikointi on kohtuullisen robusti, jos sovellusta käytetään tietoliikenteen salaavalla HTTPS-protokollalla. Access Controlin eli pääsynhallinnan toteuttamisessa on aina syytä muistaa tehdä esim. käyttäjän identiteetin tarkastus selaimen lisäksi myös palvelimella. Huonoa tietoturvaa olisi estää jotkut toimenpiteet ainoastaan piilottamalla niiden suoritusmahdollisuus selaimessa olevasta koodista.

Mozillan MDN:n erittäin hyvä Website security -guide nostaakin esiin tämän tärkän seikan:

Expressin dokumentaatio sisältää tietoturvaa käsittelvän osan Production Best Practices: Security joka kannattaa lukea läpi. Erittäin suositeltavaa on ottaa backendissa käyttöön Helmet-kirjasto, joka sisältää joukon Express-sovelluksista tunnettuja turvallisuusriskejä eliminoivia middlewareja.

Myös ESlintin security-plugininen käyttöönotto kannattaa.

Tulevaisuuden trendit

Katsotaan vielä lopuksi muutamaa huomisen tai oikeastaan jo tämän päivän tekniikkaa, ja suuntia joihin Web-sovelluskehitys on kulkemassa.

Server side rendering, isomorfiset sovellukset ja universaali koodi

Selain ei ole ainoa paikka missä Reactilla määriteltyjä komponentteja voidaan renderöidä. Renderöinti on mahdollista tehdä myös palvelimella. Tätä hyödynnetäänkin nykyään enenevissä määrin siten, että kun sovellukseen tullaan ensimmäistä kertaa, lähettää palvelin selaimeen jo valmiiksi renderöimänsä Reactilla muodostetun sivun. Tämän jälkeen sovelluksen toiminta jatkuu normaaliin tapaan, eli selain suorittaa Reactia, joka manipuloi selaimen näyttämää DOM:ia. Palvelimella tapahtuvasta renderöinnistä käytetään englanninkielistä nimitystä server side rendering.

Eräs motivaatio server side renderingille on Search Engine Optimization eli SEO. Hakukoneet ovat ainakin perinteisesti olleet huonoja tunnistamaan selaimessa Javascriptillä renderöityä sisältöä, ajat saattavat tosin olla muuttumassa, ks. esim. tämä ja tämä.

Server side rendering ei tietenkään ole mikään React- tai edes Javascript-spesifi asia, saman ohjelmointikielen käyttö kaikkialla koodissa tekee konseptista teoriassa helpommin toteutettavan, sillä samaa koodia voidaan suorittaa sekä backendissä että frontendissä.

Palvelimella tapahtuvaan renderöintiin liittyen on alettu puhua isomorfisista sovelluksista ja universaalista koodista, termien määritelmistä on kiistelty. Joidenkin määritelmien mukaan isomorfinen web-sovellus on sellainen, joka suorittaa renderöintiä sekä selaimessa että backendissa. Universaalinen koodi taas on koodia, joka voidaan suorittaa useimmissa ympäristöissä eli sekä selaimessa että backendissä.

React ja Node tarjoavatkin varteenotettavan vaihtoehdon isomorfisten sovellusten toteuttamiseen universaalina koodina.

Universaalin koodin kirjoittaminen suoraan Reactin avulla on vielä toistaiseksi melko vaivalloista. Viime aikoina paljon huomiota saanut Reactin päälle toteutettu Next.js-kirjasto on hyvä vaihtoehto universaalien sovellusten tekemiseen.

Progessive web apps

Viime aikona on myös ruvettu käyttämään Googlen lanseeraamaa termiä progressive web app (PWA). Googlen sivuilla oleva määritelmä kuulostaa markkinapuheelta ja sen perusteella on hankala saada selkeää käsitystä mistä on kyse. Checklista tuo mukaan konkretiaa.

Tiiviistäen kyse on web-sovelluksista, jotka toimivat mahdollisimman hyvin kaikilla alustoilla ottaen jokaisesta alustasta irti sen parhaat puolet. Mobiililaitteiden pienempi näyttö ei saa heikentää sovellusten käytettävyyttä. PWA-sovellusten tulee myös toimia offline-tilassa tai hitaalla verkkoyhteydellä moitteettomasti. Mobiililaitteilla ne tulee pystyä asentamaan normaalien sovellusten tavoin. Kaiken PWA-sovellusten käyttämän verkkoliikenteen tulee olla salattua.

create-react-app:illa luodut sovellukset ovat oletusarvoisesti progressiivisia. Jos sovellus käyttää palvelimella olevaa dataa, edellyttää sovelluksen progressiiviseksi tekeminen vaivan näkemistä. Offline-toiminnallisuus toteutetaan yleensä service workerien avulla.

Mikropalveluarkkitehtuuri

Tällä kurssilla olemme tehneet palvelinpuolelle ainoastaan matalan pintaraapaisun. Sovelluksissamme on ollut korkeintaan muutaman API-endpointin tarjoava monoliittinen eli yhdellä palvelimella pyörivä kokonaisuuden muodostava backend.

Sovelluksen kasvaessa suuremmaksi monoliittisen backendin malli alkaa muuttua ongelmalliseksi niin suorituskyvyn kuin jatkokehitettävyydenkin kannalta.

Mikropalveluarkkitehtuurilla (microservice) tarkoitetaan tapaa koostaa sovelluksen backend useista erillisistä autonomisista palveluista, jotka kommunikoivat keskenään verkon yli. Yksittäisen mikropalvelun on tarkoituksena hoitaa tietty looginen toiminnallinen kokonaisuus. Puhdasoppisessa mikropalveluarkkitehtuurissa palvelut eivät käytä jaettua tietokantaa.

Esim. blogilistasovelluksen voisi koostaa kahdesta palvelusta, toinen huolehtisi käyttäjistä ja toinen blogeista. Käyttäjäpalvelun vastuulla olisi käyttäjätunnusten luominen ja käyttäjien autentikointi, blogipalvelu taas huolehtisi blogeihin liittyvistä toimista.

Seuraava kuva havainnollistaa mikropalveluarkkitehtuuriin perustuvan sovelluksen rakennetta perinteiseen monoliittiseen rakenteeseen verrattuna:

Frontendin (kuvassa neliöitynä) rooli ei välttämättä poikkea malleissa kovinkaan paljoa, mikropalveluiden ja frontendin välissä on usein API gateway jonka tarjoaa frontendille perinteisen kaltaisen, “yhdessä palvelimessa” olevan näkymän backendiin, esim. Netflix käyttää tätä ratkaisua.

Mikropalveluarkkitehtuurit ovat syntyneet ja kehittyneet suurten internetskaalan sovellusten tarpeisiin. Trendin aloitti Amazon jo kauan ennen termin microservice lanseeraamista. Tärkeä lähtölaukaus oli CEO Jeff Bezosin vuonna 2002 kaikille työntekijöille lähettämä email:

All teams will henceforth expose their data and functionality through service interfaces.

Teams must communicate with each other through these interfaces.

There will be no other form of inter-process communication allowed: no direct linking, no direct reads of another team’s data store, no shared-memory model, no back-doors whatsoever. The only communication allowed is via service interface calls over the network.

It doesn’t matter what technology you use.

All service interfaces, without exception, must be designed from the ground up to be externalize-able. That is to say, the team must plan and design to be able to expose the interface to developers in the outside world.

No exceptions.

Anyone who doesn’t do this will be fired. Thank you; have a nice day!

Nykyään eräs vahvimmista suunnannäyttäjistä mikropalveluiden suhteen on Netflix.

Mikropalveluista on pikkuhiljaa tullut hype, tämän ajan silver bullet, jota yritetään tarjota ratkaisuiksi lähes kaikkiin ongelmiin. Mikropalveluarkkitehtuurin soveltamiseen liittyy kuitenkin suuri määrä haasteita ja voi olla järkevämpi lähteä liikeelle monolith first, eli tehdä aluksi perinteinen kaiken sisältävä backend. Tai sitten ei. Mielipiteitä on monenlaisia. Molemmat linkit johtavat Martin Fowlerin sivuille, eli viisaimmat eivät ole ihan varmoja kumpi näistä oikeista tavoista on oikeampi.

Emme voi valitettavasti syventyä tällä kurssilla tähän tärkeään aihepiiriin tämän tarkemmin. Jo pintapuolinenkin käsittely vaatisi ainakin 5 viikkoa lisää aikaa.

Serverless

Loppuvuodesta 2014 tapahtuneen Amazonin lambda-palvelun julkaisun jälkeen alkoi web-sovellusten kehittämiseen nousta jälleen uusi trendi, serverless.

Kyse on siitä, että lambda ja nyttemmin Googlen Cloud functions ja Azuren vastaava toiminnallisuus mahdollistava yksittäisten funktioiden suorittamisen pilvessä, kun ennen tätä pienin pilvessä suoritettava yksikkö oli käytännössä yksittäinen prosessi, eli esim. Node-backendiä suorittava ajoympäristö.

Esim. Amazonin API-gateway:n avulla on mahdollista tehdä “palvelimettomia” sovelluksia, missä määritellyn HTTP API:n kutsuihin vastataan suoraan pilvifunktioilla. Funktiot yleensä operoivat jo valmiiksi pilvipalvelun tietokantoihin talletetun datan avulla.

Serverlessissä ei siis ole kyse siitä että sovelluksissa ei olisi palvelinta, vaan tavasta määritellä palvelin. Sovelluskehittäjät voivat siirtyä ohjelmoinnissa korkeammalle abstraktiotasolle, ei ole enää tarvetta määritellä ohjelmallisesti HTTP-kutsujen reitityksiä, tietokantayhteyksiä ym., pilvi-infrastruktuuri tarjoaa kaiken tämän. Pilvifunktioilla on myös mahdollista saada helposti aikaan hyvin skaalautuvia järjestelmiä, esim. Amazon Lambda pystyy suorittamaan massiivisen määrän pilvifunktioita sekunnissa. Kaikki tämä tapahtuu infrastruktuurin toimesta automaattisesti, ei ole tarvetta käynnistellä uusia palvelimia ym.

GraphQL

Tälläkin kurssilla moneen kertaan mainittu ja käytetty REST on ollut pitkään vallitseva tapa toteuttaa palvelimen selaimelle tarjoama rajapinta ja yleensäkin verkossa toimivien sovellusten välinen integraatio.

RESTin rinnalle selaimessa (tai mobiililaitteessa) toimivan logiikan ja palvelimien väliseen kommunikointiin on viime vuosina noussut alunperin Facebookin kehittämä GraphQL.

GraphQL on filosofialtaan todella erilainen RESTiin verrattuna. REST on resurssipohjainen, jokaisella resurssilla, esim. käyttäjällä on oma sen identifioiva osoite, esim. users/10, ja kaikki resursseille tehtävät operaatiot toteutetaan tekemällä URL:ille kohdistuvia pyyntöjä, joiden toiminta määrittyy käytetyn HTTP-metodin avulla.

RESTin resurssiperustaisuus toimii hyvin useissa tapauksissa, joissain tapauksissa se voi kuitenkin olla hieman kankea.

Oletetaan että blogilistasovelluksemme sisältäisi somemaista toiminnallisuutta ja haluaisimme esim. näyttää sovelluksessa listan, joka sisältää kaikkien seuraamiemme (follow) käyttäjien blogeja kommentoineiden käyttäjien lisäämien blogien nimet.

Jos palvelin toteuttaisi REST API:n, joutuisimme todennäköisesti tekemään monia HTTP-pyyntöjä selaimen koodista, ennen kuin saisimme muodostettua halutun datan. Pyyntöjen vastauksena tulisi myös paljon ylimääräistä dataa ja halutun datan keräävä selaimen koodi olisi todennäköisesti kohtuullisen monimutkainen.

Jos kyseessä olisi usein käytetty toiminnallisuus, voitaisiin sitä varten toteuttaa oma REST-endpoint. Jos vastaavia skeaarioita olisi paljon, esim. kymmeniä, tulisi erittäin työlääksi toteuttaa kaikille toiminnallisuuksille oma REST-endpoint.

GraphQL:n avulla toteutettava palvelin sopii tämänkaltaisiin tilanteiseen hyvin.

GraphQL:ssä periaatteena, on että selaimen koodi muodostaa kyselyn, joka kuvailee haluttua dataa ja lähettää sen API:lle HTTP POST -pyynnöllä. Toisin kuin REST:issä, GraphQL:ssä kaikki kyselyt kohdistetaan samaan osoitteeseen ja ovat POST-tyyppisiä.

Edellä kuvatun skenaarion data saataisiin haettua (suurinpiirtein) seuraavan kaltaisella kyselyllä:

query FetchBlogsQuery {
  user(username: 'mluukkai') {
    followedUsers {
      blogs {
        comments {
          user {
            blogs {
              title
            }
          }
        }
      }
    }
  }
}

Palvelimen vastaus pyyntöön olisi suunnilleen seuraavanlainen JSON-olio:

{
  data: {
    followedUsers: {
      blogs: {
        comments: {
          user: {
            blogs: [
              'Goto considered harmful',
              'End to End Testing with Puppeteer and Jest',
              'Navigating your transition to GraphQL',
              'From REST to GraphQL'
            ]
          }
        }
      }
    }
  }
}

Sovelluslogiikka säilyy yksinkertaisena ja selaimen koodi saa täsmälleen haluamansa datan yksittäisellä kyselyllä.

GraphQL:ää käytettäessä sovelluksen datasta on määriteltävä skeema, joka kuvailee datan entiteettejä, eli esimerkkimme tapauksessa käyttäjiä, blogeja ja kommentteja sekä niiden suhteita, ks. lisää esim. blogista Navigating your transition to GraphQL.

GraphQL:n nimi tuleekin siitä, että entiteetit ja niiden suhteet muodostavat usein, etenkin sosiaalisen median tapaisissa sovelluksessa eräänlaisen verkon, missä entiteetit liittyvät toisiin entiteetteihin erilaisten suhteiden kautta.

Nimestään huolimatta GraphQL:llä ei ole suoranaisesti mitään tekemistä tietokantojen kanssa, se ei ota mitään kantaa siihen miten data on tallennettu. GraphQL-periaattella toimivan API:n käyttämä data voi siis olla talletettu relaatiotietokantaan, dokumenttitietokantaan tai muille palvelimeille, joita GraphQL-palvelin käyttää vaikkapa REST:in välityksellä. GraphQL on täysin ohjelmointikieliriippumaton, sekä GraphQL-clientien että -servereiden toteuttamisen tueksi on olemassa kirjastoja useilla ohjelmointikielillä.

GraphQL on jo melko iäkäs teknologia, se on ollut Facebookin sisäisessä käytössä jo vuodesta 2012 lähtien, teknologian voi siis todeta olevan “battle tested”. Facebook julkaisi GraphQL:n vuonna 2015 ja se on pikkuhiljaa saanut enenevissä määrin huomiota ja nousee ehkä lähivuosina uhmaamaan REST:in valta-asemaa.

Teen kurssille ehkä tulevaisuudessa uuden, GraphQL:ää käsittelevän osan. Tänä keväänä se ei kuitenkaan tule ilmestymään. Jos haluat kulkea etujoukkojen mukana, voikin olla hyvä idea tutustua GraphQL:n Full Stack -harjoitustyön yhteydessä.

Lisää GraphQL:stä esim. seuraavissa:

Cloud native app

Viime aikoina on myös yleistynyt termin pilvinatiivi, cloud native käyttö. Termillä ei ole mitään yhtenäistä määritelmää.

Pivotal määrittelee käsitteen seuraavasti

Cloud-native is an approach to building and running applications that fully exploits the advantages of the cloud computing delivery model. Cloud-native is about how applications are created and deployed, not where. While today public cloud impacts the thinking about infrastructure investment for virtually every industry, a cloud-like delivery model isn’t exclusive to public environments. It’s appropriate for both public and private clouds. Most important is the ability to offer nearly limitless computing power, on-demand, along with modern data and application services for developers. When companies build and operate applications in a cloud-native fashion, they bring new ideas to market faster and respond sooner to customer demands.

Esim. Netflixin koko infrastruktuuria voi pitää pilvinatiivina. Netflixillä ei ole omia palvelimia, kaikki toimii Amazonin alustalla olevissa virtuaalikoneissa.

Pilvinatiiviuteen liittyvät usein äsken mainitut mikropalvelut ja serverless-arkkitehtuuri. Tärkeä teema pilvinatiiveissa sovelluksissa on myös kontainereiden, kuten Dockerin hyödyntäminen.

Hyödyllisiä kirjastoja ja mielenkiintoisia linkkejä

Facebookin ylläpitämä kirjasto immutable.js tarjoaa muutamista tietorakenteista nimensä mukaisia muuttumattomia toteutuksia. Kirjastosta voi olla hyötyä Reduxia käytettäessä, sillä kuten osasta 5 muistamme reducerien on oltava puhtaita funktioita eli ne eivät saa muuttaa storen tilaa vaan niiden on korvattava se muutostilanteissa uudella.

Redux-saga tarjoaa osassa 5 käsitellylle redux thunkille vaihtoehtoisen tavan tehdä asynkronisia actioneja. Jotkut hypettää ja tykkää, itse en.

Single page -sovelluksissa analytiikkatietojen kerääminen käyttäjien sivuston kanssa käymästä vuorovaikutuksesta on haastavampaa kuin perinteisissä, kokonaiseen sivun lataamiseen perustuvissa web-sovelluksissa. React Google Analytics -kirjasto tuo tähän avun.

Voit hyödyntää React-osaamistasi myös mobiilisovellusten toteuttamiseen Facebookin erittäin suositun React Native -kirjaston avulla.

Javascript-projektien projektinhallintaan ja bundlaamiseen käytettyjen työkalujen rintamalla on ollut tuulista, best practicet ovat vaihdelleet nopeasti (vuosiluvut ovat suuntaa-antavia, kukaan ei enää muista noin kauas menneisyyteen):

Hipsterien suurin into työkalukehitykseen näytti pysähtyneen webpackin vallattua markkinat. Uusi tulokas Parcel on kuitenkin saanut viime aikoina nopeasti paljon huomiota. Parcel markkinoi olevansa yksinkertainen, sitähän webpack ei missään nimessä ole, ja paljon nopeampi kuin webpack. Parcelin kehitystä kannattaa jäädä seuraamaan.

Sivu https://reactpatterns.com/ tarjoaa tiiviissä muodossa listan parhaita react-käytänteitä, joista osa on jo tältäkin kurssilta tuttuja. Toinen samankaltainen lista on react bits.

Jos tiedät jotain suositeltavia linkkejä tai kirjastoja, tee pull request!