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()
conerror.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:
- Ottieni un elenco dei tuoi punti di accesso.
- Estrai le dipendenze del modulo da tutti questi punti di ingresso.
- Trova dipendenze condivise tra tutti i punti di ingresso.
- Raggruppa le dipendenze condivise.
- 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!