Un altro anno è appena cominciato, la tecnologia avanza, così come la specifica ECMAscript che ci permette di utilizzare costrutti sempre più avanzati per scrivere il nostro codice. Spesso però le nostre applicazioni hanno come requisito il supporto di browser anche molto vecchi.
Strumenti come create-react-app
o angular-cli
gestiscono questi aspetti per noi, ma a volte potremmo voler avere il pieno controllo sul nostro workflow ed anche sui browser supportati.
In Madisoft per un recente progetto greenfield abbiamo utilizzato quest’ultimo approccio e in questa serie di post vorrei illustrare il nostro set-up che si avvale di Webpack e Babel partendo dalle basi, passando alle configurazioni più avanzate ed infine agli imprevisti incontrati.
Cominciamo con un po’ di teoria:
Come supportare vecchi browser?
Come possiamo supportare vecchi browser pur scrivendo codice ESNext? La soluzione più naturale è quella di fornire codice che i browser possano comprendere. Possiamo raggiungere questo scopo attraverso due processi affini:
Compilare
Compilare (spesso anche riferito in Inglese come Transpile) è il processo di trasformare una sintassi in un’altra equivalente:
1 2 3 4 5 6 7 8 |
// ESNext const sum = (a, b) => a + b; // Compilato in ES5 var sum = function(a, b) { return a + b }; |
Polyfills
La compilazione si limita a trasformare la sintassi in una equivalente, ma che accade se dovesse mancare interamente una feature? Ad esempio la funzione Object.values è presente nei browser Firefox dalla versione 47. Come fare se dovessimo supportare versioni inferiori?
In questi casi dobbiamo ricorrere ai polyfills che sono vere e proprie implementazioni di una feature mancante. Potremmo scriverle noi stessi, ma generalmente conviene integrare librerie ben testate.
Webpack e Babel
Nel nostro workflow utilizziamo Webpack e Babel. Webpack (in parole molto semplici) si preoccupa di generare un bundle unico partendo dai nostri file sorgenti. Babel invece è uno dei tanti strumenti che ci permette di gestire la compilazione ed i polyfill necessari per la nostra applicazione.
Il progetto
Per assimilare i concetti dietro questi strumenti partiremo da un progetto minimale che andremo ad incrementare gradualmente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// package.json { "name": "master-webpack-babel", "version": "1.0.0", "description": "", "scripts": { "start": "webpack" }, "license": "ISC", "devDependencies": { "@babel/core": "^7.12.10", "babel-loader": "^8.2.2", "webpack": "^5.18.0", "webpack-cli": "^4.4.0" } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// webpack.config.js // configurazione minimale di webpack e babel-loader che produce il file // bundle.js nella cartella dist const path = require('path'); module.exports = { entry: './src/app.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, mode: 'development', module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', }, }, ], }, }; |
1 2 3 4 5 6 |
// src/app.js // il codice sorgente const sum = (a, b) => a + b; console.log(sum(3,5)); |
Per prima cosa proviamo a vedere cosa succede lanciando il comando npm start che farà generare il bundle:
1 2 3 4 |
// dist/bundle.js // Codice semplificato eval("const sum = (a, b) => a + b;\r\n\r\nconsole.log(sum(3,5));\n\n//# sourceURL=webpack:///./src/app.js?"); |
Pur avendo fatto compilare il codice da Babel via babel-loader l’output rimane invariato. Come mai?
La risposta risiede nel funzionamento di Babel. Questo strumento infatti si basa su un sistema di plug-ins. Ogni plugin ha lo scopo di effettuare una trasformazione e senza nessun plug-in Babel non fa assolutamente niente.
@babel/preset-env
Per evitare di dover installare ogni plug-in a mano Babel ci semplifica la vita mettendo a disposizione dei Presets ossia un insieme dei plug-in più comuni. Ai fini del nostro progetto utilizzeremo @babel/preset-env:
npm install --save-dev @babel/preset-env
Per poter utilizzare il preset dobbiamo aggiungere un file di configurazione per Babel nella root del progetto (che verrà automaticamente utilizzato da babel-loader).
1 2 3 4 5 |
// babel.config.js module.exports = { presets: ["@babel/preset-env"], }; |
Lanciando di nuovo npm start osserviamo che questa volta l’output è quello che ci aspettiamo:
1 2 3 4 |
// dist/bundle.js // Codice semplificato eval("var sum = function sum(a, b) {\n return a + b;\n};\n\nconsole.log(sum(3, 5));\n\n//# sourceURL=webpack:///./src/app.js?"); |
Core-js
Ora il codice viene compilato correttamente, ma per quanto riguarda i polyfill?? Proviamo a modificare il codice sorgente come segue:
1 2 3 4 5 |
const sum = (a, b) => a + b; console.log(sum(3,5)); console.log(['a', 'b', 'c', 'd'].includes('d')); |
Provando a caricare il bundle in un browser che non supporta la funzione Array.includes avremo un errore in console:
Uncaught TypeError: ['a', 'b', 'c', 'd'].includes('d') is not a function
npm install --save-dev core-js
1 2 3 4 5 6 7 8 9 |
// app.js import 'core-js'; const sum = (a, b) => a + b; console.log(sum(3,5)); console.log(['a', 'b', 'c', 'd'].includes('d')); |
Perfetto! Ora anche i vecchi browser non avranno più problemi.
Dunque è così semplice? In realtà abbiamo trascurato qualche cosa, infatti andando a vedere le dimensioni del bundle notiamo che è aumentato sensibilmente anche se abbiamo solo tre righe di codice!
La configurazione che abbiamo utilizzato è senza dubbio la più semplice, ma anche la meno ottimizzata. Infatti importiamo tutta la libreria core-js quindi anche un sacco di funzioni che effettivamente non utilizziamo.
Nel prossimo post vedremo come far fronte a questo inconveniente grazie alle configurazioni che Babel ci mette a disposizione.