Siirry sisältöön

d

Webpack

React oli alkuaikoina jossain määrin kuuluisa siitä, että sovelluskehityksen edellyttämien työkalujen konfigurointi oli hyvin hankalaa. Tilannetta helpottamaan kehitettiin Create React App:in, joka poisti konfigurointiin liittyvät ongelmat. Kurssillakin käytettävä Vite on viime aikoina korvannut Create React Appin uusissa sovelluksissa.

Sekä Vite että Create React App hyödyntävät varsinaisen työn tekemiseen bundlereita. Tutustumme nyt Create React Appin käyttämään Webpack-nimiseen bundleriin. Webpack oli vuosia ylivoimaisesti suosituin bundler-ohjelmisto. Viime aikoina on kuitenkin syntynyt useita uuden sukupolven bundlereita kuten Viten käyttämä esbuild, jotka ovat Webpackia huomattavasti nopeampia ja helppokäyttöisempiä. Esim. esbuildista kuitenkin puuttuu vielä eräitä hyödyllisiä ominaisuuksia (kuten selaimessa olevan koodin hotreload), joten tutustumme seuraavassa bundlereiden vanhaan hallitsijaan Webpackiin.

Bundlaus

Olemme toteuttaneet sovelluksia jakamalla koodin moduuleihin, joita on importattu niitä tarvitseviin paikkoihin. Vaikka ES6-moduulit ovatkin JavaScript-standardissa määriteltyjä, eivät vanhemmat selaimet 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, ja tuloksena on joukko hakemistoon dist sijoitettavia tiedostoja:


├── assets
│   ├── index-d526a0c5.css
│   ├── index-e92ae01e.js
│   └── react-35ef61ed.svg
├── index.html
└── vite.svg

Hakemiston dist juuressa oleva sovelluksen "päätiedosto" index.html lataa script-tagin avulla bundlatun JavaScript-tiedoston:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React</title>
    <script type="module" crossorigin src="/assets/index-e92ae01e.js"></script>
    <link rel="stylesheet" href="/assets/index-d526a0c5.css">
  </head>
  <body>
    <div id="root"></div>
    
  </body>
</html>

Kuten esimerkistä näemme, Vitellä tehdyssä sovelluksessa bundlataan JavaScriptin lisäksi sovelluksen CSS-määrittelyt tiedostoon /assets/index-d526a0c5.csss.

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, importattujen koodien importtaamat koodit jne.

Koska osa importeista on kirjastoja kuten React, Redux tai 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 pääosin nykyään 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 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:

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 = () => {
  return {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'build'),
      filename: 'main.js'
    }
  }
}

module.exports = config

Huom: määrittely olisi mahdollista tehdä funktion sijaan myös suoraan oliona:

const path = require('path')

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

module.exports = config

Olio riittää monissa tilanteissa, mutta tulemme myöhemmin tarvitsemaan tiettyjä ominaisuuksia, jotka edellyttävät sen että määrittely on tehty funktiona.

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:

eval("const hello = name => {\n  console.log(`hello ${name}`)\n}\n\n//# sourceURL=webpack://webpack-osa7/./src/index.js?");

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

const App = () => {
  return null
}

export default App

Importataan moduuli App ja käytetään sitä 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:

fullstack content

Konfiguraatiotiedosto

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

const path = require('path')

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

module.exports = config

Konfiguraatio on JavaScriptia ja tapahtuu eksporttaamalla määrittelyt palauttava funktio Noden moduulisyntaksilla.

Tämänhetkinen 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, mikä 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 react react-dom

Liitetään tavanomaiset loitsut tiedostoon index.js

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

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

ja muutetaan App.js muotoon

import React from 'react' // tarvitsemme importin nyt myös kompontentin määrittelyn yhteydessä

const App = () => {
  return (
    <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-tagia 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 path = require('path')

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

module.exports = config

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

Yksittäisen loaderin määrittely on kolmiosainen:

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

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

Asennetaan loader ja sen tarvitsemat kirjastot kehitysaikaisiksi riippuvuuksiksi:

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. Ongelma korjaantuu asentamalla kirjastot core-js ja regenerator-runtime

npm install core-js regenerator-runtime

ja importtaamalla ne tiedostossa index.js:

import 'core-js/stable/index.js'
import 'regenerator-runtime/runtime.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-syntaksi 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',
  options: {
    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',
  options: {
    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',
      options: {
        presets: ['@babel/preset-react', '@babel/preset-env'],
      },
    },
    {      test: /\.css$/,      use: ['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 koodi 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 serve --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: {    static: 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, sillä dev-serveriä käytettäessä webpack ei bundlaa koodia normaaliin tapaan tiedostoksi main.js, vaan bundlauksen tuotos on olemassa ainoastaan keskusmuistissa.

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

import React, { useState } from 'react'
import './index.css'

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

Sovellus toimii hyvin ja kehitys on melko sujuvaa.

Sourcemappaus

Erotetaan napin klikkauksenkäsittelijä omaksi funktiokseen ja talletetaan tilaan values laskurin aiemmat 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

sillä se viittaa itse kirjoittamaamme koodiin:

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 1009487 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ä 1009487 tavua ei sinänsä ole ongelma, mutta jos mukaan sisällytetään enemmän kirjastoja, alkaa sovelluksen lataaminen pikkuhiljaa hidastua etenkin mobiilikäytössä.

Tiedoston sisältöä tarkastelemalla huomaa, että tiedostoa 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 serve --mode=development"
  },
  "license": "MIT",
  "dependencies": {
    // ...
  },
  "devDependencies": {
    // ...
  }
}

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

$ ls -l build/main.js
-rw-r--r--  1 mluukkai  ATKK\hyad-all  146237 Feb  7 15:58 build/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://notes2023.fly.dev/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 = 'https://notes2023.fly.dev/api/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?

Webpackin konfiguraatiofunktiolla on kaksi parametria, env ja argv, joista jälkimmäisen avulla saamme selville npm-skriptissä määritellyn moden:

const path = require('path')

const config = (env, argv) => {  console.log('argv.mode:', argv.mode)
  return {
    // ...
  }
}

module.exports = config

Nyt voimme siis halutessamme säätää Webpackin toimimaan eri tavalla riippuen siitä onko sovelluksen käyttöympäristö eli "mode" arvoltaan production vai development.

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.mode:', argv.mode)

  const backend_url = argv.mode === 'production'    ? 'https://notes2023.fly.dev/api/notes'    : 'http://localhost:3001/notes'
  return {
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'build'),
      filename: 'main.js'
    },
    devServer: {
      static: 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>
  )
}

Nyt siis jos sovellus on käynnistetty komennolla npm start development-moodissa, hakee se muistiinpanot osoitteesta http://localhost:3001/notes. Komennolla npm run build bundlattu versio taas käyttää osoitetta https://notes2023.fly.dev/api/notes muistiinpanojen hakemiseen.

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, mutta mikään IE:n versio ei kuitenkaan niitä tue:

fullstack content

On paljon muitakin 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 lisäämällä koodiin 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.