Un ghid pentru gestionarea dependențelor pachetelor web
Publicat: 2022-03-11Conceptul 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.
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.