Blog in live streaming potenziato - Suddivisione del codice

Nel nostro ultimo Supercharged Livestream abbiamo implementato la suddivisione del codice e la suddivisione in base alle route. Con i moduli HTTP/2 e nativi ES6, queste tecniche diventeranno essenziali per consentire un caricamento e una memorizzazione nella cache efficienti delle risorse di script.

Suggerimenti utili vari in questo episodio

  • asyncFunction().catch() con error.stack: 9:55
  • Moduli e attributo nomodule sui tag <script>: 7:30
  • promisify() in Nodo 8: 17:20

TL;DR

Come eseguire la suddivisione del codice tramite chunking basato su route:

  1. Ottieni un elenco dei tuoi punti di accesso.
  2. Estrai le dipendenze del modulo da tutti questi punti di ingresso.
  3. Trova dipendenze condivise tra tutti i punti di ingresso.
  4. Raggruppa le dipendenze condivise.
  5. Riscrivi i punti di ingresso.

Suddivisione del codice e suddivisione basata su route

La suddivisione del codice e la suddivisione basata su route sono strettamente correlate e sono spesso utilizzati in modo intercambiabile. Questo ha creato confusione. Proviamo a chiarire questo punto:

  • Suddivisione del codice: la suddivisione del codice è il processo di suddivisione del codice in più pacchetti. Se non invii al client un grande bundle con tutto il codice JavaScript, significa che stai eseguendo la suddivisione del codice. Un modo specifico per suddividere il codice è utilizzare la suddivisione in blocchi basati su route.
  • Suddivisione basata su route: il blocco basato su route crea bundle correlati alle route della tua app. Analizzando i tuoi percorsi e le loro dipendenze, possiamo cambiare i moduli da inserire in un bundle.

Perché avviene la suddivisione del codice?

Moduli liberi

Con i moduli ES6 nativi, ogni modulo JavaScript può importare le proprie dipendenze. Quando il browser riceve un modulo, tutte le istruzioni import attivano recuperi aggiuntivi per bloccare i moduli necessari per eseguire il codice. Tuttavia, tutti questi moduli possono avere dipendenze proprie. Il pericolo è che il browser finisca con una serie di recuperi che durano più cicli di andata e ritorno prima che il codice possa essere finalmente eseguito.

Raggruppamento in bundle

Il raggruppamento, che consiste nell'incorporare tutti i moduli in un unico bundle, garantisce che il browser abbia tutto il codice necessario dopo un round trip e possa iniziare a eseguire il codice più rapidamente. Questo, tuttavia, obbliga l'utente a scaricare una grande quantità di codice non necessario, che comporta una perdita di tempo e larghezza di banda. Inoltre, ogni modifica a uno dei nostri moduli originali comporterà una modifica al bundle, invalidando qualsiasi versione memorizzata nella cache del bundle. Gli utenti dovranno riscaricare tutto il contenuto.

Suddivisione del codice

La suddivisione del codice è la via di mezzo. Siamo disposti a investire di più nel ritorno sull'investimento per ottenere efficienza della rete scaricando solo ciò di cui abbiamo bisogno e una migliore efficienza della memorizzazione nella cache riducendo il numero di moduli per bundle molto più ridotto. Se il raggruppamento viene eseguito correttamente, il numero totale di round trip sarà molto inferiore rispetto ai moduli liberi. Infine, potremmo utilizzare meccanismi di precaricamento come link[rel=preload] per risparmiare ulteriori tempi del trio, se necessario.

Passaggio 1: ottieni un elenco dei tuoi punti di contatto

Questo è solo uno dei tanti approcci, ma nell'episodio abbiamo analizzato sitemap.xml del sito web per ottenere i punti di ingresso al nostro sito web. Solitamente si usa un file JSON dedicato che elenca tutti i punti di ingresso.

Utilizzo di babel per elaborare JavaScript

Babel è comunemente usato per la "transpiling", ovvero per consumare codice JavaScript innovativo e convertirlo in una versione precedente di JavaScript, in modo che più browser siano in grado di eseguirlo. Il primo passaggio qui è analizzare il nuovo JavaScript con un parser (Babel usa babylon) che trasforma il codice in un cosiddetto "Abstract Syntax Tree" (AST). Una volta generato l'AST, una serie di plug-in analizza e modifica l'AST.

Faremo un uso intensivo di babel per rilevare (e successivamente manipolare) le importazioni di un modulo JavaScript. Potresti avere la tentazione di ricorrere alle espressioni regolari, ma queste non sono abbastanza potenti per analizzare correttamente un linguaggio e sono difficili da gestire. Affidarsi a strumenti comprovati come Babel ti eviterà molti grattacapi.

Ecco un semplice esempio di esecuzione di Babel con un plug-in personalizzato:

const plugin = {
  visitor: {
    ImportDeclaration(decl) {
      /* ... */
    }
  }
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});

Un plug-in può fornire un oggetto visitor. Il visitatore contiene una funzione per qualsiasi tipo di nodo che il plug-in vuole gestire. Quando viene rilevato un nodo di quel tipo durante il attraversamento dell'AST, la funzione corrispondente nell'oggetto visitor viene richiamata con quel nodo come parametro. Nell'esempio precedente, viene richiamato il metodo ImportDeclaration() per ogni dichiarazione import nel file. Per avere un'idea più chiara dei tipi di nodi e di AST, vai su astexplorer.net.

Passaggio 2: estrai le dipendenze del modulo

Per creare la struttura delle dipendenze di un modulo, analizzeremo il modulo in questione e creeremo un elenco di tutti i moduli che importa. Dobbiamo anche analizzare queste dipendenze, dato che a loro volta potrebbero avere dipendenze. Un caso classico per la ricorsione!

async function buildDependencyTree(file) {
  let code = await readFile(file);
  code = code.toString('utf-8');

  // `dep` will collect all dependencies of `file`
  let dep = [];
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        // Recursion: Push an array of the dependency’s dependencies onto the list
        dep.push((async function() {
          return await buildDependencyTree(`./app/${importedFile}`);
        })());
        // Push the dependency itself onto the list
        dep.push(importedFile);
      }
    }
  }
  // Run the plugin
  babel.transform(code, {plugins: [plugin]});
  // Wait for all promises to resolve and then flatten the array
  return flatten(await Promise.all(dep));
}

Passaggio 3: trova le dipendenze condivise tra tutti i punti di ingresso

Poiché abbiamo un insieme di alberi delle dipendenze, una foresta di dipendenze, se vuoi, possiamo trovare le dipendenze condivise cercando i nodi che appaiono in ogni albero. Intendiamo e deduplicare la foresta e lo filtreremo in modo da mantenere solo gli elementi presenti in tutti gli alberi.

function findCommonDeps(depTrees) {
  const depSet = new Set();
  // Flatten
  depTrees.forEach(depTree => {
    depTree.forEach(dep => depSet.add(dep));
  });
  // Filter
  return Array.from(depSet)
    .filter(dep => depTrees.every(depTree => depTree.includes(dep)));
}

Passaggio 4: raggruppa le dipendenze condivise

Per raggruppare il nostro set di dipendenze condivise, basta concatenare tutti i file dei moduli. Quando si utilizza questo approccio, sorgono due problemi: il primo problema è che il bundle conterrà ancora istruzioni import che faranno tentare al browser di recuperare le risorse. Il secondo problema è che le dipendenze delle dipendenze non sono state raggruppate. Poiché l'abbiamo già fatto, scriveremo un altro plug-in babel.

Il codice è abbastanza simile al nostro primo plug-in, ma invece di estrarre semplicemente le importazioni, le rimuoveremo e inseriremo una versione in bundle del file importato:

async function bundle(oldCode) {
  // `newCode` will be filled with code fragments that eventually form the bundle.
  let newCode = [];
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        newCode.push((async function() {
          // Bundle the imported file and add it to the output.
          return await bundle(await readFile(`./app/${importedFile}`));
        })());
        // Remove the import declaration from the AST.
        decl.remove();
      }
    }
  };
  // Save the stringified, transformed AST. This code is the same as `oldCode`
  // but without any import statements.
  const {code} = babel.transform(oldCode, {plugins: [plugin]});
  newCode.push(code);
  // `newCode` contains all the bundled dependencies as well as the
  // import-less version of the code itself. Concatenate to generate the code
  // for the bundle.
  return flatten(await Promise.all(newCode)).join('\n');
}

Passaggio 5: riscrivi i punti di ingresso

Per l'ultimo passaggio, scriveremo un altro plug-in Babel. Il suo compito è rimuovere tutte le importazioni dei moduli che si trovano nel bundle condiviso.

async function rewrite(section, sharedBundle) {
  let oldCode = await readFile(`./app/static/${section}.js`);
  oldCode = oldCode.toString('utf-8');
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        // If this import statement imports a file that is in the shared bundle, remove it.
        if(sharedBundle.includes(importedFile))
          decl.remove();
      }
    }
  };
  let {code} = babel.transform(oldCode, {plugins: [plugin]});
  // Prepend an import statement for the shared bundle.
  code = `import '/static/_shared.js';\n${code}`;
  await writeFile(`./app/static/_${section}.js`, code);
}

Termina

Era bellissimo, vero? Ricorda che l'obiettivo di questo episodio era spiegare e chiarire la suddivisione del codice. Il risultato funziona, ma è specifico per il nostro sito dimostrativo e non andrà a buon fine nel caso generico. Per la produzione, ti consiglio di affidarti a strumenti consolidati come WebPack, RollUp e così via.

Puoi trovare il nostro codice nel repository di GitHub.

a presto!