Menține controlul: un ghid pentru Webpack și React, Pt. 1

Publicat: 2022-03-11

Când începeți un nou proiect React, aveți multe șabloane din care să alegeți: Create React App, react-boilerplate și React Starter Kit, pentru a numi câteva.

Aceste șabloane, adoptate de mii de dezvoltatori, sunt capabile să susțină dezvoltarea de aplicații la scară foarte mare. Dar ele lasă experiența dezvoltatorului și ieșirea pachetului cu diverse setări implicite, care ar putea să nu fie ideale.

Dacă doriți să mențineți un grad mai mare de control asupra procesului dvs. de construire, atunci puteți alege să investiți într-o configurație personalizată Webpack. După cum veți afla din acest tutorial Webpack, această sarcină nu este foarte complicată și cunoștințele ar putea fi chiar utile atunci când depanați configurațiile altor persoane.

Webpack: Noțiuni introductive

Modul în care scriem JavaScript astăzi este diferit de codul pe care îl poate executa browserul. Ne bazăm frecvent pe alte tipuri de resurse, limbi translate și funcții experimentale care nu sunt încă acceptate în browserele moderne. Webpack este un bundler de module pentru JavaScript care poate reduce acest decalaj și poate produce cod compatibil între browsere fără cheltuială atunci când vine vorba de experiența dezvoltatorului.

Înainte de a începe, ar trebui să rețineți că tot codul prezentat în acest tutorial Webpack este disponibil și sub forma unui exemplu de fișier de configurare Webpack/React complet pe GitHub. Vă rugăm să nu ezitați să îl consultați acolo și să reveniți la acest articol dacă aveți întrebări.

Configurație de bază

De la Legato (versiunea 4), Webpack nu necesită nicio configurație pentru a rula. Alegerea unui mod de construcție va aplica un set de valori implicite mai potrivite pentru mediul țintă. În spiritul acestui articol, vom elimina aceste valori implicite și vom implementa o configurație sensibilă pentru fiecare mediu țintă.

Mai întâi, trebuie să instalăm webpack și webpack-cli :

 npm install -D webpack webpack-cli

Apoi trebuie să webpack.config.js cu o configurație care să includă următoarele opțiuni:

  • devtool : Permite generarea hărții sursă în modul de dezvoltare.
  • entry : Fișierul principal al aplicației noastre React.
  • output.path : directorul rădăcină în care să stocați fișierele de ieșire.
  • output.filename : Modelul numelui de fișier care trebuie utilizat pentru fișierele generate.
  • output.publicPath : Calea către directorul rădăcină unde fișierele vor fi implementate pe serverul web.
 const path = require("path"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; const isDevelopment = !isProduction; return { devtool: isDevelopment && "cheap-module-source-map", entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "assets/js/[name].[contenthash:8].js", publicPath: "/" } }; };

Configurația de mai sus funcționează bine pentru fișierele JavaScript simple. Dar când folosim Webpack și React, va trebui să efectuăm transformări suplimentare înainte de a expedia codul către utilizatorii noștri. În secțiunea următoare, vom folosi Babel pentru a schimba modul în care Webpack încarcă fișierele JavaScript.

JS Loader

Babel este un compilator JavaScript cu multe plugin-uri pentru transformarea codului. În această secțiune, îl vom introduce ca încărcător în configurația noastră Webpack și îl vom configura pentru a transforma codul JavaScript modern într-un astfel de lucru care este înțeles de browserele obișnuite.

Mai întâi, va trebui să instalăm babel-loader și @babel/core :

 npm install -D @babel/core babel-loader

Apoi vom adăuga o secțiune de module la configurația Webpack, făcându-l pe babel-loader responsabil pentru încărcarea fișierelor JavaScript:

 @@ -11,6 +11,25 @@ module.exports = function(_env, argv) { path: path.resolve(__dirname, "dist"), filename: "assets/js/[name].[contenthash:8].js", publicPath: "/" + }, + module: { + rules: [ + { + test: /\.jsx?$/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + cacheDirectory: true, + cacheCompression: false, + envName: isProduction ? "production" : "development" + } + } + } + ] + }, + resolve: { + extensions: [".js", ".jsx"] } }; };

Vom configura Babel folosind un fișier de configurare separat, babel.config.js . Va folosi următoarele caracteristici:

  • @babel/preset-env : Transformă caracteristicile JavaScript moderne în cod compatibil cu versiunea inversă.
  • @babel/preset-react : Transformă sintaxa JSX în apeluri de funcții JavaScript simple.
  • @babel/plugin-transform-runtime : Reduce duplicarea codului prin extragerea ajutoarelor Babel în module partajate.
  • @babel/plugin-syntax-dynamic-import : Activează sintaxa dynamic import() în browserele care nu au suport nativ pentru Promise .
  • @babel/plugin-proposal-class-properties : Activează suportul pentru propunerea de sintaxă a câmpului instanței publice, pentru scrierea componentelor React bazate pe clasă.

De asemenea, vom activa câteva optimizări ale producției specifice React:

  • babel-plugin-transform-react-remove-prop-types elimină tipurile de prop inutile din codul de producție.
  • @babel/plugin-transform-react-inline-elements evaluează React.createElement în timpul compilării și aliniază rezultatul.
  • @babel/plugin-transform-react-constant-elements extrage elementele React statice ca constante.

Comanda de mai jos va instala toate dependențele necesare:

 npm install -D @babel/preset-env @babel/preset-react @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-class-properties babel-plugin-transform-react-remove-prop-types @babel/plugin-transform-react-inline-elements @babel/plugin-transform-react-constant-elements

Apoi vom popula babel.config.js cu aceste setări:

 module.exports = { presets: [ [ "@babel/preset-env", { modules: false } ], "@babel/preset-react" ], plugins: [ "@babel/plugin-transform-runtime", "@babel/plugin-syntax-dynamic-import", "@babel/plugin-proposal-class-properties" ], env: { production: { only: ["src"], plugins: [ [ "transform-react-remove-prop-types", { removeImport: true } ], "@babel/plugin-transform-react-inline-elements", "@babel/plugin-transform-react-constant-elements" ] } } };

Această configurație ne permite să scriem JavaScript modern într-un mod compatibil cu toate browserele relevante. Există și alte tipuri de resurse de care am putea avea nevoie într-o aplicație React, pe care le vom acoperi în secțiunile următoare.

Încărcător CSS

Când vine vorba de stilarea aplicațiilor React, trebuie să putem include cel puțin fișiere CSS simple. Vom face acest lucru în Webpack folosind următoarele încărcătoare:

  • css-loader : analizează fișierele CSS, rezolvând resurse externe, cum ar fi imagini, fonturi și importuri suplimentare de stil.
  • style-loader : în timpul dezvoltării, injectează stiluri încărcate în document în timpul rulării.
  • mini-css-extract-plugin : extrage stilurile încărcate în fișiere separate pentru utilizare în producție pentru a profita de memorarea în cache a browserului.

Să instalăm încărcătoarele CSS de mai sus:

 npm install -D css-loader style-loader mini-css-extract-plugin

Apoi vom adăuga o nouă regulă la secțiunea module.rules din configurația Webpack:

 @@ -1,4 +1,5 @@ const path = require("path"); +const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -25,6 +26,13 @@ module.exports = function(_env, argv) { envName: isProduction ? "production" : "development" } } + }, + { + test: /\.css$/, + use: [ + isProduction ? MiniCssExtractPlugin.loader : "style-loader", + "css-loader" + ] } ] },

Vom adăuga, de asemenea, MiniCssExtractPlugin la secțiunea de plugins , pe care o vom activa numai în modul producție:

 @@ -38,6 +38,13 @@ module.exports = function(_env, argv) { }, resolve: { extensions: [".js", ".jsx"] - } + }, + plugins: [ + isProduction && + new MiniCssExtractPlugin({ + filename: "assets/css/[name].[contenthash:8].css", + chunkFilename: "assets/css/[name].[contenthash:8].chunk.css" + }) + ].filter(Boolean) }; };

Această configurație funcționează pentru fișiere CSS simple și poate fi extinsă pentru a funcționa cu diferite procesoare CSS, cum ar fi Sass și PostCSS, despre care vom discuta în articolul următor.

Încărcător de imagini

Webpack poate fi folosit și pentru a încărca resurse statice, cum ar fi imagini, videoclipuri și alte fișiere binare. Cel mai generic mod de a gestiona astfel de tipuri de fișiere este utilizarea file-loader sau url-loader , care va oferi consumatorilor săi o referință URL pentru resursele necesare.

În această secțiune, vom adăuga url-loader pentru a gestiona formatele comune de imagine. Ceea ce diferențiază url-loader de file-loader este că, dacă dimensiunea fișierului original este mai mică decât un anumit prag, acesta va încorpora întregul fișier în adresa URL ca conținut codificat base64, eliminând astfel necesitatea unei cereri suplimentare.

Mai întâi instalăm url-loader :

 npm install -D url-loader

Apoi adăugăm o nouă regulă la secțiunea module.rules din configurația Webpack:

 @@ -34,6 +34,16 @@ module.exports = function(_env, argv) { isProduction ? MiniCssExtractPlugin.loader : "style-loader", "css-loader" ] + }, + { + test: /\.(png|jpg|gif)$/i, + use: { + loader: "url-loader", + options: { + limit: 8192, + name: "static/media/[name].[hash:8].[ext]" + } + } } ] },

SVG

Pentru imaginile SVG, vom folosi @svgr/webpack loader, care transformă fișierele importate în componente React.

Instalăm @svgr/webpack :

 npm install -D @svgr/webpack

Apoi adăugăm o nouă regulă la secțiunea module.rules din configurația Webpack:

 @@ -44,6 +44,10 @@ module.exports = function(_env, argv) { name: "static/media/[name].[hash:8].[ext]" } } + }, + { + test: /\.svg$/, + use: ["@svgr/webpack"] } ] },

Imaginile SVG ca componente React pot fi convenabile, iar @svgr/webpack efectuează optimizarea folosind SVGO.

Notă: pentru anumite animații sau chiar efecte de trecere cu mouse-ul, poate fi necesar să manipulați SVG-ul folosind JavaScript. Din fericire, @svgr/webpack încorporează în mod implicit conținutul SVG în pachetul JavaScript, permițându-vă să ocoliți restricțiile de securitate necesare pentru aceasta.

Încărcător de fișiere

Când trebuie să facem referire la orice alte tipuri de fișiere, file-loader generic va face treaba. Funcționează în mod similar cu url-loader , oferind o adresă URL a materialului codului care o solicită, dar nu încearcă să-l optimizeze.

Ca întotdeauna, mai întâi instalăm modulul Node.js. În acest caz, file-loader :

 npm install -D file-loader

Apoi adăugăm o nouă regulă la secțiunea module.rules din configurația Webpack. De exemplu:

 @@ -48,6 +48,13 @@ module.exports = function(_env, argv) { { test: /\.svg$/, use: ["@svgr/webpack"] + }, + { + test: /\.(eot|otf|ttf|woff|woff2)$/, + loader: require.resolve("file-loader"), + options: { + name: "static/media/[name].[hash:8].[ext]" + } } ] },

Aici am adăugat un dispozitiv de încărcare file-loader pentru încărcarea fonturilor, pe care le puteți consulta din fișierele dvs. CSS. Puteți extinde acest exemplu pentru a încărca orice alte tipuri de fișiere de care aveți nevoie.

Plugin de mediu

Putem folosi DefinePlugin() de la Webpack pentru a expune variabilele de mediu din mediul de compilare la codul aplicației noastre. De exemplu:

 @@ -1,5 +1,6 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const webpack = require("webpack"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -65,7 +66,12 @@ module.exports = function(_env, argv) { new MiniCssExtractPlugin({ filename: "assets/css/[name].[contenthash:8].css", chunkFilename: "assets/css/[name].[contenthash:8].chunk.css" - }) + }), + new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify( + isProduction ? "production" : "development" + ) + }) ].filter(Boolean) }; };

Aici am înlocuit process.env.NODE_ENV cu un șir reprezentând modul de construire: "development" sau "production" .

Plugin HTML

În absența unui fișier index.html , pachetul nostru JavaScript este inutil, doar stând acolo fără ca nimeni să-l găsească. În această secțiune, vom introduce html-webpack-plugin pentru a genera un fișier HTML pentru noi.

Instalăm html-webpack-plugin :

 npm install -D html-webpack-plugin

Apoi adăugăm html-webpack-plugin la secțiunea de plugins din configurația noastră Webpack:

 @@ -1,6 +1,7 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const webpack = require("webpack"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -71,6 +72,10 @@ module.exports = function(_env, argv) { "process.env.NODE_ENV": JSON.stringify( isProduction ? "production" : "development" ) + }), + new HtmlWebpackPlugin({ + template: path.resolve(__dirname, "public/index.html"), + inject: true }) ].filter(Boolean) };

Fișierul public/index.html generat va încărca pachetul nostru și va porni aplicația noastră.

Optimizare

Există mai multe tehnici de optimizare pe care le putem folosi în procesul nostru de construire. Vom începe cu minificarea codului, un proces prin care putem reduce dimensiunea pachetului nostru fără cheltuială în ceea ce privește funcționalitatea. Vom folosi două pluginuri pentru a minimiza codul nostru: terser-webpack-plugin pentru codul JavaScript și optimize-css-assets-webpack-plugin pentru CSS.

Să le instalăm:

 npm install -D terser-webpack-plugin optimize-css-assets-webpack-plugin

Apoi vom adăuga o secțiune de optimization la configurația noastră:

 @@ -2,6 +2,8 @@ const path = require("path"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const webpack = require("webpack"); const HtmlWebpackPlugin = require("html-webpack-plugin"); +const TerserWebpackPlugin = require("terser-webpack-plugin"); +const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin"); module.exports = function(_env, argv) { const isProduction = argv.mode === "production"; @@ -75,6 +77,27 @@ module.exports = function(_env, argv) { isProduction ? "production" : "development" ) }) - ].filter(Boolean) + ].filter(Boolean), + optimization: { + minimize: isProduction, + minimizer: [ + new TerserWebpackPlugin({ + terserOptions: { + compress: { + comparisons: false + }, + mangle: { + safari10: true + }, + output: { + comments: false, + ascii_only: true + }, + warnings: false + } + }), + new OptimizeCssAssetsPlugin() + ] + } }; };

Setările de mai sus vor asigura compatibilitatea codului cu toate browserele moderne.

Divizarea codului

Divizarea codului este o altă tehnică pe care o putem folosi pentru a îmbunătăți performanța aplicației noastre. Divizarea codului se poate referi la două abordări diferite:

  1. Folosind o instrucțiune import() dinamică, putem extrage părți ale aplicației care reprezintă o parte semnificativă din dimensiunea pachetului nostru și le putem încărca la cerere.
  2. Putem extrage cod care se modifică mai rar, pentru a profita de stocarea în cache a browserului și pentru a îmbunătăți performanța pentru vizitatorii care repetă.

Vom completa secțiunea optimization.splitChunks a configurației noastre Webpack cu setări pentru extragerea dependențelor terțelor părți și a fragmentelor comune în fișiere separate:

 @@ -99,7 +99,29 @@ module.exports = function(_env, argv) { sourceMap: true }), new OptimizeCssAssetsPlugin() - ] + ], + splitChunks: { + chunks: "all", + minSize: 0, + maxInitialRequests: 20, + maxAsyncRequests: 20, + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + name(module, chunks, cacheGroupKey) { + const packageName = module.context.match( + /[\\/]node_modules[\\/](.*?)([\\/]|$)/ + )[1]; + return `${cacheGroupKey}.${packageName.replace("@", "")}`; + } + }, + common: { + minChunks: 2, + priority: -10 + } + } + }, + runtimeChunk: "single" } }; };

Să aruncăm o privire mai profundă asupra opțiunilor pe care le-am folosit aici:

  • chunks: "all" : În mod implicit, extragerea fragmentelor comune afectează numai modulele încărcate cu un import() dinamic. Această setare permite optimizarea și pentru încărcarea punctului de intrare.
  • minSize: 0 : În mod implicit, numai bucățile peste un anumit prag de dimensiune devin eligibile pentru extracție. Această setare permite optimizarea pentru tot codul obișnuit, indiferent de dimensiunea acestuia.
  • maxInitialRequests: 20 și maxAsyncChunks: 20 : Aceste setări măresc numărul maxim de fișiere sursă care pot fi încărcate în paralel pentru importurile de puncte de intrare și, respectiv, importurile de puncte de împărțire.

În plus, specificăm următoarea configurație cacheGroups :

  • vendors : Configurați extracția pentru module terțe.
    • test: /[\\/]node_modules[\\/]/ : model de nume de fișier pentru potrivirea dependențelor de la terți.
    • name(module, chunks, cacheGroupKey) : Grupează bucățile separate din același modul, dându-le un nume comun.
  • common : Configurați extragerea bucăților comune din codul aplicației.
    • minChunks: 2 : O bucată va fi considerată obișnuită dacă se face referire din cel puțin două module.
    • priority: -10 : Atribuie o prioritate negativă grupului cache common , astfel încât bucățile pentru grupul cache al vendors să fie luate în considerare mai întâi.

De asemenea, extragem codul de rulare Webpack într-o singură bucată care poate fi partajată între mai multe puncte de intrare, prin specificarea runtimeChunk: "single" .

Dev Server

Până acum, ne-am concentrat pe crearea și optimizarea versiunii de producție a aplicației noastre, dar Webpack are și propriul server web cu reîncărcare live și raportare a erorilor, ceea ce ne va ajuta în procesul de dezvoltare. Se numește webpack-dev-server și trebuie să-l instalăm separat:

 npm install -D webpack-dev-server

În acest fragment, introducem o secțiune devServer în configurația Webpack:

 @@ -120,6 +120,12 @@ module.exports = function(_env, argv) { } }, runtimeChunk: "single" + }, + devServer: { + compress: true, + historyApiFallback: true, + open: true, + overlay: true } }; };

Aici am folosit următoarele opțiuni:

  • compress: true : permite comprimarea activelor pentru reîncărcări mai rapide.
  • historyApiFallback: true : Activează o alternativă la index.html pentru rutarea bazată pe istoric.
  • open: true : Deschide browserul după lansarea serverului de dezvoltare.
  • overlay: true : afișează erorile Webpack în fereastra browserului.

De asemenea, ar putea fi necesar să configurați setările proxy pentru a redirecționa solicitările API către serverul de backend.

Webpack și React: Performanță optimizată și gata!

Tocmai am învățat cum să încărcăm diferite tipuri de resurse cu Webpack, cum să folosim Webpack într-un mediu de dezvoltare și câteva tehnici pentru optimizarea unei versiuni de producție. Dacă aveți nevoie, puteți oricând să examinați fișierul de configurare complet pentru a vă inspira propria configurație React/Webpack. Ascuțirea unor astfel de abilități este tariful standard pentru oricine oferă servicii de dezvoltare React.

În următoarea parte a acestei serii, vom extinde această configurație cu instrucțiuni pentru cazuri de utilizare mai specifice, inclusiv utilizarea TypeScript, preprocesoare CSS și tehnici avansate de optimizare care implică randarea pe server și ServiceWorkers. Rămâi la curent pentru a afla tot ce trebuie să știi despre Webpack pentru a duce aplicația React la producție.