Condivisione e raggruppamento di bundle di più fornitori in un unico bundle di fornitori utilizzando la federazione dei moduli di Webpack e i plug-in suddivisi in blocchi

Introduzione

Repository Github per il progetto: https://github.com/IvanGadjo/OneVendorsBundle_ModFedPlugin_SplitChunksPlugin

La federazione dei moduli di Webpack è una tecnica che ci offre un'idea di come potrebbe essere il futuro dell'architettura micro-frontend. Con la possibilità di condividere ed eseguire dinamicamente il codice tra le applicazioni, ModuleFederationPlugin vanta potenti funzionalità che hanno una prospettiva futura (puoi leggere di più qui).

L'idea per questo post sul blog mi è venuta mentre lavoravo a un progetto sul mio tirocinio. Avevo utilizzato ModuleFederationPlugin di Webpack per condividere i moduli della libreria dei componenti e dei fornitori tra due app Web. Il problema era che avevo 14 moduli di fornitori diversi da condividere, ma dovevo averli tutti raggruppati in un blocco di fornitori comuni, al fine di ridurre il carico di rete di avere 14 richieste diverse contemporaneamente. Pertanto, l'idea era di raggruppare tutti i diversi bundle dei fornitori in uno, in modo da avere una sola richiesta dall'app host all'app remota quando è necessaria la libreria del fornitore.

In questo post cercherò di dimostrare la potenza dell'utilizzo del ModuleFederationPlugin di Webpack per condividere moduli tra due semplici applicazioni Web, una che funge da host (app1) e l'altra da remoto (app2). Inoltre, per semplificare, entrambe le app verranno scritte in semplice JavaScript. L'idea è che l'host caricherà i bundle di una funzione, che utilizza un metodo Lodash, nonché un componente pulsante, che utilizza la libreria D3, direttamente dall'app remota utilizzando ModuleFederationPlugin di Webpack. Infine, ti mostrerò come ottenere il raggruppamento di questi due bundle di librerie di fornitori in un unico bundle utilizzando SplitChunksPlugin di Webpack, in modo che possano essere condivisi tra le applicazioni remote e host come un unico blocco e migliorare le prestazioni.

Struttura del progetto

Il progetto è costituito dall'app host – app1, che carica una funzione condivisa, un componente condiviso e un bundle dei fornitori dall'app remota – app2. Questa è solo una semplice demo che mostra il lavoro di ModuleFederationPlugin e SplitChunksPlugin di Webpack. La struttura del progetto finale dovrebbe assomigliare a questa:

Configurazione

Dopo aver creato due cartelle, una per l'host e una per l'app remota, cd nella directory Remote_App

App_Remota
Avremo bisogno di inizializzare un progetto npm e installare webpack in modo da poter produrre bundle del nostro codice, quindi esegui questi 2 comandi dal tuo terminale:

  • inizio npm
  • npm i webpack webpack-cli --save-devIl passaggio successivo consiste nel creare la cartella src che conterrà i nostri moduli condivisi

Remote_App/src
Crea un nuovo file chiamato bootstrap.js e un'altra cartella:sharedModules. Nella cartella sharedModules crea la nostra prima funzione condivisa:mySharedFunction.js. Lascia questo file vuoto per ora.

App_remota/src/bootstrap.js
Compila questo file con la riga successiva:

import('./sharedModules/mySharedFunction');

Affinché la federazione dei moduli Webpack funzioni, il modo migliore per implementare la condivisione tra codice è tramite importazioni dinamiche come questa, sebbene sia possibile anche la condivisione tramite consumo ansioso di moduli e siano supportate anche le importazioni statiche di moduli condivisi. Questo perché i componenti/fornitori condivisi vengono caricati in fase di esecuzione ed è meglio averli importati in modo asincrono. Puoi fare riferimento a questa sezione della documentazione di Webpack in merito.

App_Remota/webpack.config.js
Ora esci dalla cartella di origine e crea un file webpack.config.js che è il file di configurazione per l'utilizzo di Webpack con la nostra app remota:

const path = require('path');

module.exports = {
  entry: './src/bootstrap.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  mode: 'development'
};

Il punto di ingresso sarebbe il nostro file bootstrap.js. Questo file fungerebbe da punto di ingresso per le importazioni dinamiche di tutti i moduli condivisi che potresti avere. Ogni pacchetto verrà inviato alla cartella dist.

App_host
Proprio come prima, dobbiamo inizializzare un progetto npm e un pacchetto web di installazione:

  • inizio npm
  • npm i webpack webpack-cli --save-dev

Host_App/src
Per gli stessi motivi del telecomando, crea un file bootstrap.js. Crea anche un file mainLogic.js vuoto. Questo file conterrà in seguito le importazioni dinamiche dei moduli condivisi.

Host_App/src/bootstrap.js

import('./mainLogic');

Host_App/webpack.config.js
Puoi copiare e incollare il file di configurazione per Webpack in questa app host dall'app remota. Contiene quasi la stessa configurazione, ad eccezione del nome file prop, verrà chiamato solo bundle.js poiché avremo solo quel bundle relativo all'app.

filename: 'bundle.js'

Hosting delle app

Per ottenere l'hosting delle app utilizziamo webpack-dev-server (è uno strumento basato su CLI per avviare un server statico per le tue risorse). Oltre all'installazione di webpack-dev-server, abbiamo anche bisogno di HtmlWebpackPlugin in modo da poter eseguire il rendering di file html. Pertanto, è necessario eseguire il cd nelle directory dell'app host e remota ed eseguire i seguenti comandi:

  • npm i webpack-dev-server --save-dev
  • npm i html-webpack-plugin --save-dev

Successivamente è necessario aggiungere estendere entrambi i file di configurazione del webpack, dell'app host e del telecomando:

Host_App/webpack.config.js

devServer: {
    static: path.join(__dirname,'dist'),
    port: 3001
  },

Dopo aver incluso questa opzione nel nostro file di configurazione webpack dell'host, il contenuto della cartella dist verrà visualizzato sulla porta 3001. Creiamo ora una pagina html:

Host_App/src/template.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><%= htmlWebpackPlugin.options.title %> </title>
</head>
<body>
    HOST APP
</body>
</html>

htmlWebpackPlugin.options.title deriva dalla proprietà title di HtmlWebpackPlugin che definiremo nel passaggio successivo.

Host_App/webpack.config.js
In alto abbiamo bisogno di un'importazione per il plugin:

const HtmlWebpackPlugin = require('html-webpack-plugin');

Creiamo anche un plug-in prop nel file di configurazione del webpack contenente la nostra configurazione HtmlWebpackPlugin in questo modo:

plugins: [
    new HtmlWebpackPlugin({
      title: 'Host app',
      template: path.resolve(__dirname, './src/template.html')
    })
  ]

Ora puoi aggiungere questo comando ai tuoi script npm che avvieranno il server. In package.json, sotto gli script aggiungi "start": "webpack serve --open" . Ora se esegui npm start nel terminale, il server dovrebbe essere avviato alla porta localhost:3001. Verrà visualizzato solo uno sfondo bianco con la scritta "HOST APP" scritta sullo schermo.

App_Remota
Gli stessi passaggi vengono replicati nell'app remota. Innanzitutto installa i pacchetti npm richiesti, quindi crea un template.html e aggiungi lo script npm per avviare il server in package.json

App_Remota/webpack.config.js
Aggiorna il file webpack.config.js dell'app remota in modo che assomigli al seguente:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
  entry: './src/bootstrap.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  mode: 'development',
  devServer: {
    static: path.join(__dirname,'dist'),
    port: 3000
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'Remote app',
      template: path.resolve(__dirname, './src/template.html')
    })
  ]
}; 

Utilizzo di Module Federation e aggiunta di librerie di fornitori

Fino a questo punto abbiamo impostato solo il codice di partenza per entrambe le app e le abbiamo ospitate su porte diverse. Ora dobbiamo utilizzare veramente il plug-in di federazione dei moduli di Webpack e la prossima cosa che faremmo è condividere due moduli:la normale funzione JS che utilizza una funzionalità della nostra prima libreria del fornitore condivisa:Lodash e un pulsante in stile con la libreria D3 (D3 è una libreria JS per manipolare documenti in base ai dati, ma nel nostro caso, per semplicità la useremo solo per lo stile del pulsante).

App_Remota
Cominciamo con il telecomando. Innanzitutto npm installa le librerie Lodash e D3

  • npm install lodash d3

Remote_App/src/sharedModules/mySharedFunction.js
La funzione che verrà condivisa si chiama myFunction(). Utilizzerà il metodo sortedUniq() di Lodash per rimuovere i duplicati da una matrice di numeri:

import _ from 'lodash';

export const myFunction = () => {
    let sampleArray = [1,1,2,2,2,3,4,5,5,6];
    let sortedArray = _.sortedUniq(sampleArray);
    console.log('My resulting array: ' + sortedArray);
}

Remote_App/src/sharedModules/mySharedButton.js

import * as d3 from 'd3';  

// create button & fill with text and id param
let d3Btn = document.createElement('button');
d3Btn.setAttribute('id','btn-d3');
d3Btn.appendChild(document.createTextNode('D3 Button'));

// append to the body
let container = document.getElementsByTagName('body');
container[0].appendChild(d3Btn);

// use d3
// change color of text to orange
d3.select('#btn-d3').style('color','orange');   

Creiamo semplicemente un pulsante e utilizziamo D3 per cambiarne il colore interno del testo.

App_remota/src/bootstrap.js
Il prossimo passo è importare i moduli in modo dinamico, in modo che il file bootstrap sia simile a questo ora:

import('./sharedModules/mySharedFunction');
import('./sharedModules/mySharedButton');

App_Remota/webpack.config.js
Per abilitare l'utilizzo del ModuleFederationPlugin è necessario registrarlo nel file di configurazione. Importa nella parte superiore del file:

const { ModuleFederationPlugin } = require('webpack').container;

Nella sezione plugin della configurazione registriamo il plugin:

new ModuleFederationPlugin({
      name: 'remoteApp_oneVendorsBundle',
      library: {
        type: 'var',
        name: 'remoteApp_oneVendorsBundle'
      },
      filename: 'remoteEntry.js',
      exposes: {
        './mySharedFunction':'./src/sharedModules/mySharedFunction.js',
        './mySharedButton':'./src/sharedModules/mySharedButton.js'
      },
      shared: [
        'lodash', 'd3'
      ]
    })

Registriamo un nome per la nostra applicazione:verrebbe utilizzato dall'app host per connettersi con il telecomando. Registriamo anche uno script con il nome di remoteEntry.js. Questo sarà lo script "magico" che abilita la condivisione di moduli tra le nostre due app e verrà generato automaticamente durante la creazione della nostra app. Per dirla in breve, attraverso l'uso di più plug-in Webpack sotto il cofano di ModuleFederationPlugin, il grafico delle dipendenze di Webpack può anche mappare le dipendenze in remoto e richiedere quei bundle JS durante il runtime.
Abbiamo anche bisogno di una sezione condivisa in cui inseriamo le librerie dei fornitori che ci piace condividere con l'app host.

Host_App/webpack.config.js
L'unica cosa che dobbiamo fare nell'applicazione host è aggiungere del codice per configurare ModuleFederationPlugin per funzionare con l'app remota. Per prima cosa abbiamo bisogno del plugin:

const { ModuleFederationPlugin } = require('webpack').container;

E nella sezione dei plugin dovremmo avere il seguente codice:

new ModuleFederationPlugin({
      name: 'hostApp_oneVendorsBundle',
      library: {
        type: 'var',
        name: 'hostApp_oneVendorsBundle'
      },
      remotes: {
        remoteApp: 'remoteApp_oneVendorsBundle'
      },
      shared: [
        'lodash', 'd3'
      ]
    })

Qui è necessario registrare l'app remota per condividere i moduli. Nella nostra app host faremmo riferimento al telecomando con il nome "remoteApp", poiché lo registriamo in questo modo nella sezione remotes del ModuleFederationPlugin. Abbiamo anche bisogno che il Lodash e il D3 siano condivisi. I bundle del fornitore verranno caricati insieme al bundle per la funzione e il pulsante condivisi.

Host_App/src/template.html
Dobbiamo solo aggiungere un <script> tag nel <head> di template.html per far funzionare tutto:

<script src='http://localhost:3000/remoteEntry.js'></script>

La myFunction() condivisa verrà caricata con un clic di un pulsante e abbiamo bisogno di un <div> che fungerà da contenitore per il rendering del pulsante, ecco perché abbiamo bisogno di questo codice nel <body> :

<button id="btn-shared-modules-loader" 
  style="display: block; margin-top: 10px;">Load shared modules</button>
<div id='shared-btn-container' style="margin-top: 10px;"></div>  

Host_App/src/mainLogic.js
Con document.getElementById() otteniamo il pulsante da template.html e aggiungiamo un listener di eventi onClick che carica dinamicamente la funzione condivisa e il pacchetto di pulsanti:

let loadSharedModulesBtn = document.getElementById('btn-shared-modules-loader');
loadSharedModulesBtn.addEventListener('click', async () => {
    let sharedFunctionModule = await import('remoteApp/mySharedFunction');
    sharedFunctionModule.myFunction();
    let sharedButtonModule = await import('remoteApp/mySharedButton');
    let sharedButton = document.createElement(sharedButtonModule.name);
    let sharedButtonContainer = document.getElementById('shared-btn-container');
    sharedButtonContainer.appendChild(sharedButton);
})

Ora è una buona idea raggruppare il nostro codice. Aggiungi il seguente script npm al package.json di entrambe le app:"build": "webpack --config webpack.config.js" . Dopo aver eseguito npm run build in entrambe le app vedrai le cartelle dist risultanti contenenti tutti i bundle prodotti da Webpack.
Inoltre, se ora avvii entrambe le app e nell'host fai clic sul pulsante Carica moduli condivisi, verrà visualizzato il pulsante D3, il registro della console dalla funzione condivisa mostrerà l'array filtrato ed entrambi i bundle dei fornitori verranno caricati dal telecomando. È importante avviare prima l'app remota o semplicemente ricaricare l'host se hai avviato le app nell'ordine diverso.
Se apri la scheda di rete degli strumenti per sviluppatori nel browser, possiamo vedere che i bundle Lodash, D3 e moduli condivisi non vengono caricati senza un clic sul pulsante. Dopo il click vengono caricati tutti i bundle e nella console riceviamo il messaggio da myFunction() dal telecomando, ma vediamo anche il pulsante shared. Se passi il mouse sopra il nome dei bundle puoi vedere che provengono effettivamente dal telecomando, da localhost:3000.

Raggiungere un pacchetto fornitori

L'uso iniziale di SplitChunksPlugin di Webpack è quello di ottenere la suddivisione del codice, suddividendo il codice in bundle più piccoli e controllando il carico delle risorse. Tuttavia, nel mio caso ho invertito questo processo:ho escogitato un modo astuto di usarlo per raggruppare tutti i codici dei fornitori in un unico pacchetto. In questo esempio abbiamo solo un piccolo numero di bundle dei fornitori, ma questo può essere piuttosto vantaggioso e ottimizzare le prestazioni quando si lavora su una scala più ampia con molti moduli dei fornitori più piccoli, supponendo che dobbiamo caricare tutti i bundle dei fornitori contemporaneamente.

App_Remota/webpack.config.js

optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/](lodash|d3|delaunator|internmap|robust-predicates)/,
          name: 'Vendors_Lodash_D3',
          chunks: 'all'
        }
      }
    }
}

Nel caso ti stavi chiedendo delaunator, internmap ... Questi sono moduli che vengono aggiunti durante l'installazione di D3, se non li includi nella regex produrranno moduli del fornitore separati per se stessi nella directory dist, che non è quello che volevamo ottenere . Questo può essere evitato anche se D3 viene importato in modo più selettivo (non avere import * as d3 from d3 ).
Ora in esecuzione npm run build nell'app remota risulterà con un bundle fornitore comune nella cartella dist denominata Vendors_Lodash_D3.bundle.js.
Infine, se avvii entrambe le app, il telecomando caricherà l'intero bundle Vendors_Lodash_D3 da solo e non caricherà altri moduli del fornitore:

Dopo aver fatto clic sul pulsante Carica moduli condivisi nell'app host, caricherà entrambi i bundle per la funzione condivisa e il pulsante D3 condiviso, ma caricherà anche un solo bundle del fornitore:Vendors_Lodash_D3:

Conclusione

In questo post ho dimostrato la potenza e il potenziale dell'utilizzo del ModuleFederationPlugin di Webpack per condividere il codice tra due applicazioni web. Inoltre, utilizzando un'intelligente combinazione di ModuleFederationPlugin e SplitChunksPlugin di Webpack, possiamo raggruppare più moduli del fornitore in uno, quindi alleviare il carico di rete e migliorare le prestazioni di caricamento del bundle tra le app.
Spero che questo post sia stato utile a molti di voi della comunità e che utilizzerete questa implementazione nei vostri progetti. Grazie mille a Zack Jackson @scriptedalchemy per avermi convinto a scrivere un post sul blog su questo argomento.