Un ghid pentru gestionarea dependențelor pachetelor web

Publicat: 2022-03-11

Conceptul de modularizare este o parte inerentă a majorității limbajelor de programare moderne. JavaScript, totuși, nu a avut nicio abordare formală a modularizării până la sosirea celei mai recente versiuni a ECMAScript ES6.

În Node.js, unul dintre cele mai populare framework-uri JavaScript de astăzi, pachetele de module permit încărcarea modulelor NPM în browsere web, iar bibliotecile orientate pe componente (cum ar fi React) încurajează și facilitează modularizarea codului JavaScript.

Webpack este unul dintre pachetele de module disponibile care procesează codul JavaScript, precum și toate elementele statice, cum ar fi foile de stil, imaginile și fonturile, într-un fișier grupat. Procesarea poate include toate sarcinile necesare pentru gestionarea și optimizarea dependențelor de cod, cum ar fi compilarea, concatenarea, minimizarea și compresia.

Webpack: Un tutorial pentru începători

Cu toate acestea, configurarea Webpack și dependențele sale poate fi stresantă și nu este întotdeauna un proces simplu, mai ales pentru începători.

Această postare de blog oferă instrucțiuni, cu exemple, despre cum să configurați Webpack pentru diferite scenarii și subliniază cele mai comune capcane legate de gruparea dependențelor de proiect folosind Webpack.

Prima parte a acestei postări de blog explică cum să simplificați definiția dependențelor într-un proiect. În continuare, discutăm și demonstrăm configurația pentru împărțirea codului aplicațiilor cu mai multe și o singură pagină. În cele din urmă, discutăm despre cum să configurați Webpack, dacă vrem să includem biblioteci terțe în proiectul nostru.

Configurarea aliasurilor și a căilor relative

Căile relative nu sunt direct legate de dependențe, dar le folosim atunci când definim dependențe. Dacă structura unui fișier de proiect este complexă, poate fi dificil să rezolvi căile relevante ale modulelor. Unul dintre cele mai fundamentale beneficii ale configurației Webpack este că ajută la simplificarea definirii căilor relative într-un proiect.

Să presupunem că avem următoarea structură de proiect:

 - Project - node_modules - bower_modules - src - script - components - Modal.js - Navigation.js - containers - Home.js - Admin.js

Putem face referire la dependențe prin căi relative către fișierele de care avem nevoie și, dacă dorim să importăm componente în containere din codul nostru sursă, arată astfel:


Home.js

 Import Modal from '../components/Modal'; Import Navigation from '../components/Navigation';


Modal.js

 import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';

De fiecare dată când dorim să importăm un script sau un modul, trebuie să știm locația directorului curent și să găsim calea relativă către ceea ce vrem să importăm. Ne putem imagina cum această problemă poate escalada în complexitate dacă avem un proiect mare cu o structură de fișiere imbricată sau vrem să refactorăm unele părți ale unei structuri de proiect complexe.

Putem rezolva cu ușurință această problemă cu opțiunea resolve.alias a resolve.alias -ului. Putem declara așa-numitele aliasuri – numele unui director sau modul cu locația acestuia și nu ne bazăm pe căi relative în codul sursă al proiectului.


webpack.config.js

 resolve: { alias: { 'node_modules': path.join(__dirname, 'node_modules'), 'bower_modules': path.join(__dirname, 'bower_modules'), } }

În fișierul Modal.js , acum putem importa datapicker mult mai simplu:

 import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';

Divizarea codului

Putem avea scenarii în care trebuie să atașăm un script în pachetul final, sau să împărțim pachetul final sau vrem să încărcăm pachete separate la cerere. Configurarea proiectului nostru și a configurației Webpack pentru aceste scenarii ar putea să nu fie simplă.

În configurația Webpack, opțiunea Entry îi spune lui Webpack unde este punctul de plecare pentru pachetul final. Un punct de intrare poate avea trei tipuri de date diferite: șir, matrice sau obiect.

Dacă avem un singur punct de plecare, putem folosi oricare dintre aceste formate și obținem același rezultat.

Dacă dorim să anexăm mai multe fișiere și acestea nu depind unul de celălalt, putem folosi un format Array. De exemplu, putem adăuga analytics.js la sfârșitul bundle.js :


webpack.config.js

 module.exports = { // creates a bundle out of index.js and then append analytics.js entry: ['./src/script/index.jsx', './src/script/analytics.js'], output: { path: './build', filename: bundle.js ' } };

Gestionarea mai multor puncte de intrare

Să presupunem că avem o aplicație cu mai multe pagini cu mai multe fișiere HTML, cum ar fi index.html și admin.html . Putem genera mai multe pachete folosind punctul de intrare ca tip de obiect. Configurația de mai jos generează două pachete JavaScript:


webpack.config.js

 module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) } };


index.html

 <script src=”build/index.js”></script>


admin.html

 <script src=”build/admin.js”></script>

Ambele pachete JavaScript pot partaja biblioteci și componente comune. Pentru aceasta, putem folosi CommonsChunkPlugin , care găsește module care apar în mai multe bucăți de intrare și creează un pachet partajat care poate fi stocat în cache între mai multe pagini.


webpack.config.js

 var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js'); module.exports = { entry: { index: './src/script/index.jsx', admin: './src/script/admin.jsx' }, output: { path: './build', filename: '[name].js' // template based on keys in entry above (index.js & admin.js) }, plugins: [commonsPlugin] };

Acum, nu trebuie să uităm să adăugăm <script src="build/common.js"></script> înainte de scripturile grupate.

Activarea Lazy Loading

Webpack poate împărți activele statice în bucăți mai mici, iar această abordare este mai flexibilă decât concatenarea standard. Dacă avem o aplicație mare cu o singură pagină (SPA), simpla concatenare într-un singur pachet nu este o abordare bună, deoarece încărcarea unui pachet uriaș poate fi lentă și, de obicei, utilizatorii nu au nevoie de toate dependențele pentru fiecare vizualizare.

Am explicat mai devreme cum să împărțiți o aplicație în mai multe pachete, să concatenați dependențe comune și să beneficiați de comportamentul de stocare în cache a browserului. Această abordare funcționează foarte bine pentru aplicațiile cu mai multe pagini, dar nu pentru aplicațiile cu o singură pagină.

Pentru SPA, ar trebui să furnizăm numai acele active statice care sunt necesare pentru a reda vizualizarea curentă. Routerul de pe partea clientului din arhitectura SPA este un loc perfect pentru a gestiona divizarea codului. Când utilizatorul introduce o rută, putem încărca numai acele dependențe necesare pentru vizualizarea rezultată. Alternativ, putem încărca dependențe pe măsură ce utilizatorul derulează în jos pe o pagină.

În acest scop, putem folosi funcțiile require.ensure sau System.import , pe care Webpack le poate detecta static. Webpack poate genera un pachet separat pe baza acestui punct de împărțire și îl poate apela la cerere.

În acest exemplu, avem două containere React; o vizualizare de administrator și o vedere de tablou de bord.


admin.jsx

 import React, {Component} from 'react'; export default class Admin extends Component { render() { return <div > Admin < /div>; } }


dashboard.jsx

 import React, {Component} from 'react'; export default class Dashboard extends Component { render() { return <div > Dashboard < /div>; } }

Dacă utilizatorul introduce fie adresa URL /dashboard , fie /admin , este încărcat numai pachetul JavaScript necesar corespunzător. Mai jos putem vedea exemple cu și fără routerul de pe partea clientului.


index.jsx

 if (window.location.pathname === '/dashboard') { require.ensure([], function() { require('./containers/dashboard').default; }); } else if (window.location.pathname === '/admin') { require.ensure([], function() { require('./containers/admin').default; }); }


index.jsx

 ReactDOM.render( <Router> <Route path="/" component={props => <div>{props.children}</div>}> <IndexRoute component={Home} /> <Route path="dashboard" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/dashboard').default) }, "dashboard")}} /> <Route path="admin" getComponent={(nextState, cb) => { require.ensure([], function (require) { cb(null, require('./containers/admin').default) }, "admin")}} /> </Route> </Router> , document.getElementById('content') );

Extragerea stilurilor în pachete separate

În Webpack, încărcătoarele, cum ar fi style-loader și css-loader , preprocesează foile de stil și le încorporează în pachetul JavaScript de ieșire, dar, în unele cazuri, pot provoca Flash-ul conținutului fără stil (FOUC).

Putem evita FOUC cu ExtractTextWebpackPlugin care permite generarea tuturor stilurilor în pachete CSS separate, în loc să le fie încorporate în pachetul final JavaScript.


webpack.config.js

 var ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { loaders: [{ test: /\.css/, loader: ExtractTextPlugin.extract('style', 'css')' }], }, plugins: [ // output extracted CSS to a file new ExtractTextPlugin('[name].[chunkhash].css') ] }

Gestionarea bibliotecilor și pluginurilor terță parte

De multe ori, trebuie să folosim biblioteci terță parte, diverse plugin-uri sau scripturi suplimentare, pentru că nu dorim să petrecem timp dezvoltând aceleași componente de la zero. Există multe biblioteci și pluginuri vechi disponibile care nu sunt întreținute în mod activ, nu înțeleg modulele JavaScript și presupun prezența dependențelor la nivel global sub nume predefinite.

Mai jos sunt câteva exemple cu pluginuri jQuery, cu o explicație despre cum să configurați corect Webpack pentru a putea genera pachetul final.

FurnizațiPlugin

Majoritatea pluginurilor terțe se bazează pe prezența unor dependențe globale specifice. În cazul jQuery, pluginurile se bazează pe variabila $ sau jQuery care este definită și putem folosi pluginuri jQuery apelând $('div.content').pluginFunc() în codul nostru.

Putem folosi pluginul ProvidePlugin pentru a adăuga var $ = require("jquery") de fiecare dată când întâlnește identificatorul global $ .


webpack.config.js

 webpack.ProvidePlugin({ '$': 'jquery', })

Când Webpack procesează codul, caută prezența $ și oferă o referință la dependențele globale fără a importa modulul specificat de funcția require .

Import-încărcare

Unele plugin-uri jQuery presupun $ în spațiul de nume global sau se bazează pe faptul că this este obiectul window . În acest scop, putem folosi imports-loader care injectează variabile globale în module.


example.js

 $('div.content').pluginFunc();

Apoi, putem injecta variabila $ în modul prin configurarea imports-loader :

 require("imports?$=jquery!./example.js");

Acest lucru înaintează pur și simplu var $ = require("jquery"); spre example.js .

În al doilea caz de utilizare:


webpack.config.js

 module: { loaders: [{ test: /jquery-plugin/, loader: 'imports?jQuery=jquery,$=jquery,this=>window' }] }

Folosind simbolul => (a nu se confunda cu funcțiile ES6 Arrow), putem seta variabile arbitrare. Ultima valoare redefinește variabila globală this pentru a indica obiectul window . Este același lucru cu împachetarea întregului conținut al fișierului cu (function () { ... }).call(window); și apelând this funcție cu window ca argument.

De asemenea, putem solicita biblioteci folosind formatul de modul CommonJS sau AMD:

 // CommonJS var $ = require("jquery"); // jquery is available // AMD define(['jquery'], function($) { // jquery is available });

Unele biblioteci și module pot accepta diferite formate de module.

În exemplul următor, avem un plugin jQuery care utilizează formatul de modul AMD și CommonJS și are o dependență de jQuery:


jquery-plugin.js

 (function(factory) { if (typeof define === 'function' && define.amd) { // AMD format is used define(['jquery'], factory); } else if (typeof exports === 'object') { // CommonJS format is used module.exports = factory(require('jquery')); } else { // Neither AMD nor CommonJS used. Use global variables. } });


webpack.config.js

 module: { loaders: [{ test: /jquery-plugin/, loader: "imports?define=>false,exports=>false" }] }

Putem alege ce format de modul vrem să folosim pentru biblioteca specifică. Dacă declarăm define ca fiind equal false , Webpack nu analizează modulul în formatul de modul AMD, iar dacă declarăm exports de variabile la egal false , Webpack nu analizează modulul în formatul de modul CommonJS.

Expune-încărcător

Dacă trebuie să expunem un modul la contextul global, putem folosi expose-loader . Acest lucru poate fi util, de exemplu, dacă avem scripturi externe care nu fac parte din configurația Webpack și se bazează pe simbolul din spațiul de nume global sau folosim pluginuri de browser care trebuie să acceseze un simbol în consola browserului.


webpack.config.js

 module: { loaders: [ test: require.resolve('jquery'), loader: 'expose-loader?jQuery!expose-loader?$' ] }

Biblioteca jQuery este acum disponibilă în spațiul de nume global pentru alte scripturi de pe pagina web.

 window.$ window.jQuery

Configurarea dependențelor externe

Dacă dorim să includem module din scripturi găzduite extern, trebuie să le definim în configurare. În caz contrar, Webpack nu poate genera pachetul final.

Putem configura scripturi externe folosind opțiunea externals din configurația Webpack. De exemplu, putem folosi o bibliotecă dintr-un CDN printr-o etichetă <script> separată, declarând-o totuși în mod explicit ca dependență de modul în proiectul nostru.


webpack.config.js

 externals: { react: 'React', 'react-dom': 'ReactDOM' }

Suport pentru mai multe instanțe ale unei biblioteci

Este grozav să utilizați managerul de pachete NPM în dezvoltarea front-end pentru gestionarea bibliotecilor și dependențelor terțelor părți. Cu toate acestea, uneori putem avea mai multe instanțe ale aceleiași biblioteci cu versiuni diferite și nu se joacă bine împreună într-un singur mediu.

Acest lucru s-ar putea întâmpla, de exemplu, cu biblioteca React, unde putem instala React din NPM și mai târziu poate deveni disponibilă o versiune diferită de React cu un pachet sau plugin suplimentar. Structura proiectului nostru poate arăta astfel:

 project | |-- node_modules | |-- react |-- react-plugin | |--node_modules | |--react

Componentele care provin de la react-plugin au o instanță React diferită de restul componentelor din proiect. Acum avem două copii separate ale React și pot fi versiuni diferite. În aplicația noastră, acest scenariu poate distruge DOM-ul nostru global mutabil și putem vedea mesaje de eroare în jurnalul consolei web. Soluția la această problemă este să aveți aceeași versiune de React pe tot parcursul proiectului. O putem rezolva prin aliasuri Webpack.


webpack.config.js

 module.exports = { resolve: { alias: { 'react': path.join(__dirname, './node_modules/react'), 'react/addons': path.join(__dirname, '/node_modules/react/addons'), } } }

Când react-plugin încearcă să solicite React, folosește versiunea din node_modules al proiectului. Dacă vrem să aflăm ce versiune de React folosim, putem adăuga console.log(React.version) în codul sursă.

Concentrați-vă pe dezvoltare, nu pe configurarea pachetului web

Această postare doar zgârie suprafața puterii și utilității Webpack-ului.

Există multe alte încărcătoare și pluginuri Webpack care vă vor ajuta să optimizați și să eficientizați gruparea JavaScript.

Chiar dacă sunteți începător, acest ghid vă oferă o bază solidă pentru a începe să utilizați Webpack, care vă va permite să vă concentrați mai mult pe dezvoltare, mai puțin pe configurarea grupării.

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