React oli aiemmin 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
├── precache-manifest.8082e70dbf004a0fe961fc1f317b2683.js
├── service-worker.js
└── static
    ├── css
    │   ├── main.f9a47af2.chunk.css
    │   └── main.f9a47af2.chunk.css.map
    └── js
        ├── 1.578f4ea1.chunk.js
        ├── 1.578f4ea1.chunk.js.map
        ├── main.8209a8f2.chunk.js
        ├── main.8209a8f2.chunk.js.map
        ├── runtime~main.229c360f.js
        └── runtime~main.229c360f.js.map

Hakemiston juuressa oleva sovelluksen "päätiedosto" index.html lataa script-tagin avulla bundlatun Javascript-tiedoston (jos ollaan tarkkoja, on bundlattuja Javascript-tiedostoja kaksi kappaletta):

<!doctype html><html lang="en">
<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.8209a8f2.chunk.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.f9a47af2.chunk.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ä seuraavat tiedostot:


├── build
├── 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, 'build'),
    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 build sijoitettava tiedosto main.js:

fullstack content

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:

fullstack content

Kirjoittamamme koodi löytyy erittäin kryptisesti muotoiltuna bundlen lopusta:

/***/ "./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.

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 build/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

fullstack content

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 = () => {
  return <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, 'build'),
    filename: 'main.js',
  },
  module: {    rules: [      {        test: /\.js$/,        loader: 'babel-loader',        query: {          presets: ['@babel/preset-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: ['@babel/preset-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 @babel/core babel-loader @babel/preset-react --save-dev

Nyt bundlaus onnistuu.

Jos katsomme bundlattua koodia ja editoimme hieman koodin ulkoasua, 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 build/index.html selaimen open file -toiminnolla:

fullstack content

On kuitenkin huomionarvoista, että jos sovelluksemme sisältää async/await -toiminnallisuutta, selaimeen ei joillain selaimilla renderöidy mitään. Konsoliin tulostuneen virheviestin googlaaminen valaisee asiaa. Asian korjaamiseksi on asennettava vielä yksi puuttuva riippuvuus, @babel/polyfill.

npm install --save-dev @babel/polyfill

Muutetaan vielä tiedostoon webpack.config.js entry-kohdan määrittelyä seuraavasti:

  entry: ['@babel/polyfill', './src/index.js']

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 joukkoja.

Tällä hetkellä sovelluksemme transpiloinnissa käytetään presetiä @babel/preset-react:

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

Otetaan käyttöön preset @babel/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: ['@babel/preset-env', '@babel/preset-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 = () => {
  return (
    <div className="container">
      hello webpack
    </div>
  )
}

ja importataan se tiedostossa index.js

import './index.css'

Transpilointi hajoaa

fullstack content

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

{
  rules: [
    {
      test: /\.js$/,
      loader: 'babel-loader',
      query: {
        presets: ['@babel/preset-react', '@babel/preset-env'],
      },
    },
    {      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. mini-css-extract-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:

{
  // ...
  "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, 'build'),
    filename: 'main.js',
  },
  devServer: {    contentBase: path.resolve(__dirname, 'build'),    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:

import React, {useState} from 'react'

const App = () => {
  const [counter, setCounter] = useState(0)

  return (
    <div className="container">
      hello webpack {counter} clicks
      <button onClick={() => setCounter(counter + 1)} >press</button>
    </div>
  )
}

export default App

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

fullstack content

Sovellus toimii hyvin ja kehitys on melko sujuvaa.

Sourcemappaus

Erotetaan napin klikkauksenkäsittelijä omaksi funktioksi ja talletetaan tilaan values aiemmat laskurin arvot:

const App = () => {
  const [counter, setCounter] = useState(0)
  const [values, setValues] = useState()
  const handleClick = () => {
    setCounter(counter + 1)
    setValues(values.concat(counter))  }

  return (
    <div className="container">
      hello webpack {counter} clicks
      <button onClick={handleClick} >press</button>
    </div>
  )
}

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

fullstack content

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:27 Uncaught TypeError: Cannot read property 'concat' of undefined
    at handleClick (App.js:27)

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

fullstack content

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ä

fullstack content

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

fullstack content

Korjataan bugi alustamalla tila values tyhjäksi taulukoksi:

const App = () => {
  const [counter, setCounter] = useState(0)
  const [values, setValues] = useState([])
  // ...
}

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 1281572 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ä 1281572 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 tapahtuu 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

$ build ls -l main.js
-rw-r--r--  1 mluukkai  984178727  217450 Jun 21 20:18 main.js

Minifioinnin lopputulos on kuin vanhan liiton c-koodia, kommentit ja jopa turhat välilyönnit ja rivinvaihdot 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 is easy",
      "id": "5a3b8481bb01f9cb00ccb4a9"
    },
    {
      "important": false,
      "content": "Mongo can save js objects",
      "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 tehdään tarvittavat lisäykset sovellukseen. Vaihtelun vuoksi muistiinpanojen hakeminen palvelimelta on toteutettu custom hookin useNotes avulla:

import React, { useState, useEffect } from 'react'
import axios from 'axios'

const useNotes = (url) => {  const [notes, setNotes] = useState([])  useEffect(() => {    axios.get(url).then(response => {      setNotes(response.data)    })  }, [url])  return notes}
const App = () => {
  const [counter, setCounter] = useState(0)
  const [values, setValues] = useState([])
  const url = 'http://localhost:3001/notes'
  const notes = useNotes(url)
  const handleClick = () => {
    setCounter(counter + 1)
    setValues(values.concat(counter))
  }

  return (
    <div className="container">
      hello webpack {counter} clicks
      <button onClick={handleClick} >press</button>
      <div>{notes.length} notes on server {url}</div>    </div>
  )
}

export default App

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, 'build'),
      filename: 'main.js'
    },
    devServer: {
      contentBase: path.resolve(__dirname, 'build'),
      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:

const App = () => {
  const [counter, setCounter] = useState(0)
  const [values, setValues] = useState([])
  const notes = useNotes(BACKEND_URL)
  // ...
  return (
    <div className="container">
      hello webpack {counter} clicks
      <button onClick={handleClick} >press</button>
      <div>{notes.length} notes on server {BACKEND_URL}</div>    </div>
  )
}

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 build, 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:

fullstack content

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

fullstack content

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!