Modernizzare il tubo di costruzione del frontend: dai CDN al Bundling (Italiano (Italian))

Modernizzare il tubo di costruzione del frontend: dai CDN al Bundling

Tuesday, 11 November 2025

//

30 minute read

Introduzione

Nel corso dell'ultimo anno, ho incappato, sperimentato, rotto le cose, e gradualmente modernizzato il frontend build pipeline per questo blog. Questo non era un qualche masterplan eseguito perfettamente è stato prova ed errore, un sacco di errore, e persistenza ostinata guidata da una chiara visione di ciò che volevo raggiungere.

Non sono un guru frontend o un wizard Webpack. Sono uno sviluppatore .NET che si è frustrato con i limiti delle dipendenze basate su CDN e ha deciso che ci doveva essere un modo migliore. Questo articolo documenta il disordinato, viaggio iterativo da semplice <script> tag che puntano a unpkg ad una moderna pipeline di bundling che funziona davvero.

Ho fatto un sacco di errori lungo la strada (che documenterò), trascorso ore di debug criptico errori Webpack, e probabilmente reinstallato node_modules Ma la perseveranza ha dato i suoi frutti, e ho imparato un'enorme quantita' grazie alla pura determinazione a farlo bene.

Se sei uno sviluppatore .NET che fissa i file di configurazione di Webpack chiedendosi che cosa diavolo avete ottenuto voi stessi in questo articolo è per voi.

I'm a old web performance nutter, in the early days of dial-up the first site I built for money ( that wasn't a porn backend in Perl... a story for another time) was a Snow Conditions site that spun off from a automatic phone line.

In quei giorni le preoccupazioni erano molto diverse. JavaScript era praticamente sconosciuto. Penso che anche i div erano RARE (in quei giorni IE era div e Netscape era layer) e saresti LUCKY se i tuoi utenti avessero una connessione 56K. Così ho imparato ad ottimizzare (anche cose come sfondi di immagini piastrellate per le voci di menu) ecc...

Più tardi a Microsoft ho anche lavorato su uno strumento sprite immagine automatica in moduli Web - è stato fantastico: ha estratto piccole immagini da una pagina, generato CSS e un'immagine sprite automaticamente. Ma ahimè, come il mio tempo a Microsoft, non doveva essere.

In breve, è un'ossessione che ho portato avanti tutta la mia carriera. A nessuno piace un sito lento!

Ad essere onestiI CDN non sono intrinsecamente malvagi.

Divulgazione completa: Questo blog è volutamente esagerato. necessità Tutta questa complessità. Ma costruire in questo modo mi permette di imparare gli strumenti moderni del frontend correttamente e, crucialemente, mi ha dato qualcosa di reale da scrivere e insegnare. A volte il modo migliore per imparare è quello di costruire qualcosa di leggermente ridicolo e documentare il viaggio.

Il problema con i CDN

Inizialmente, il mio approccio era semplice:

<!-- Old CDN-based approach -->
<script src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">

Mentre questo funziona, ha diversi svantaggi che sono diventati sempre più frustranti:

  • Questioni relative all'affidabilità: CDN andare giù. Ho visto unpkg e jsdelivr hanno interruzioni. Quando sono giù, il tuo sito è rotto
  • Comportamento di carico imprevedibile: Le risposte CDN possono variare selvaggiamente. A volte veloci, a volte lenti, a volte introducono condizioni di gara dove le librerie caricano nell'ordine sbagliato
  • Questioni relative al calendario: Non avete alcun controllo su quando gli script caricano l'uno rispetto all'altro. Ciò mi ha causato mal di testa infiniti con inizializzazione della libreria
  • Richieste HTTP multiple: Ogni libreria richiede una richiesta separata, anche con HTTP/2
  • Niente scosse d'albero.: Si scarica l'intera libreria, anche se si utilizza solo una frazione di esso
  • Versione deriva: I CDN potrebbero servire versioni diverse a meno che non li appuntate esplicitamente, e anche le versioni pinned possono comportarsi in modo diverso tra i fornitori di CDN
  • Incertezza del tempo di generazione: Non c'è modo di catturare incompatibilità fino a quando il runtime è spesso quando un utente lo segnala
  • Ottimizzazione limitata: Impossibile ridurre i limiti o rimuovere il codice morto
  • Sviluppo offline: Richiede connettività internet, che è fastidioso quando si lavora su treni o aerei

Quindi volevo controllare... controllare esattamente quello che il mio sito ha usato e NEEDED per funzionare. Volevo impacchettare solo quello che mi serviva, garantire un ordine di carico affidabile e ottimizzare le prestazioni. Ciò significava allontanarmi dai CDN e verso un approccio bundling.

Sono più semplici da configurare, e con il multiplexing HTTP/2 e HTTP/3, l'overhead di richiesta multipla è molto meno critico di quanto non lo fosse una volta. Per molti progetti, soprattutto quelli piccoli o prototipi, i CDN rimangono una scelta perfettamente ragionevole. Ma per un sito di produzione dove volevo controllo, affidabilità e ottimizzazione, il bundling ha avuto più senso.

L'approccio moderno di raggruppamento

I miei pacchetti di configurazione corrente tutte le dipendenze JavaScript attraverso Webpack e processi CSS attraverso PostCSS.

Perché Webpack invece di Vite?

Prima di tuffarmi nei dettagli tecnici, dovrei rivolgermi all'elefante nella stanza: perché sto usando Webpack quando esistono Vite, Rollup, esbuild e altri bundler moderni?

La risposta onesta è semplice: Conoscevo già Webpack..

Avevo ampiamente utilizzato Webpack quando insegnavo il mio corso "Beginning Web Development," ed era lo strumento che avevo capito. Quando ho deciso di modernizzare la build pipeline di questo blog, ero già di fronte a una ripida curva di apprendimento che comprende tree-shaking, divisione del codice, sistemi di modulo, tubazioni PostCSS, e come integrare tutto questo con ASP.NET Core.

È generalmente una buona pratica quando si lavora su progetti; limitare il 'nuovo' al 'gestibile'. È più facile limitare l'incertezza.

L'aggiunta di "imparare uno strumento di build completamente nuovo" in cima a quello sembrava inutile. Webpack funziona. E 'maturo. Ha una documentazione eccellente e un enorme ecosistema. Soprattutto, ho potuto concentrare la mia energia di apprendimento sul concetti di bundling moderno piuttosto che le idiosincrasie di un particolare strumento.

Vite e' piu' veloce? Assolutamente. Il server di sviluppo di Vite con moduli ES nativi ed esbuild-powered bundling è significativamente più veloce di Webpack. Per i grandi progetti con centinaia di moduli, la differenza è drammatica.

Dovresti usare Vite per un nuovo progetto? Probabilmente, sì. Se stai iniziando da zero e non hai conoscenze esistenti di Webpack, Vite è probabilmente la scelta migliore. È più veloce, più semplice da configurare, e rappresenta l'approccio moderno al frontend tooling.

Mi pento di aver usato Webpack? Niente affatto. Mi ha portato dove dovevo essere. Le lezioni che ho imparato su bundling, divisione del codice, e ottimizzazione sono trasferibili a qualsiasi strumento di build. E onestamente, per la scala di questo blog, la differenza di prestazioni tra Webpack e Vite è trascurabile, stiamo parlando di millisecondi nello sviluppo ricostruire i tempi.

E' un aspetto chiave di come costruisco le cose; inizia con quello che è EASY e costruisci da quella fondazione.

La lezione più ampia qui è che progresso batte la perfezione. Avrei potuto trascorrere settimane a cercare il bundler "migliore," a confrontare i benchmark, a leggere articoli di confronto, e agonizzante sulla decisione. Invece, ho scelto lo strumento che conoscevo, ha funzionato, e andato avanti. Quel pragmatismo mi ha tenuto la spedizione piuttosto che all'infinito deliberante.

Forse un giorno potrei migrare a Vite. Forse non lo farò. In ogni caso, questo blog ha un moderno, ottimizzato build pipeline che funziona in modo affidabile e questo è ciò che conta.

Struttura del pacchetto

La package.json Ora mantiene due gruppi di dipendenza distinti. Questa struttura mi ha preso diversi tentativi di ottenere a destra inizialmente, Nuget e npm sono simili KINDA, ma nppm è molto meno amichevole quando l'aggiunta di pacakges.

{
  "dependencies": {
    //NOTE - This is a pre-release of these enhancements, the 1.0.0 release is OUT NOW!
    "@mostlylucid/mermaid-enhancements": "^1.0.0-alpha0",
    "alpinejs": "^3.14.1",
    "codemirror": "5.65.13",
    "core-js": "^3.39.0",
    "easymde": "2.20.0",
    "flatpickr": "^4.6.13",
    "highlight.js": "^11.10.0",
    "highlightjs-cshtml-razor": "^2.1.1",
    "html-to-image": "^1.11.13",
    "htmx.org": "^2.0.1",
    "mermaid": "^11.0.2",
    "regenerator-runtime": "^0.14.1",
    "svg-pan-zoom": "^3.6.2"
  },
  //These are just used for build; not needed to RUN the app so we have a separate place for 'em
  "devDependencies": {
    "@babel/core": "7.26.9",
    "@babel/preset-env": "7.26.9",
    "@tailwindcss/aspect-ratio": "^0.4.2",
    "@tailwindcss/forms": "^0.5.7",
    "@tailwindcss/typography": "^0.5.12",
    "autoprefixer": "10.4.21",
    "babel-loader": "10.0.0",
    "cpx": "1.5.0",
    "css-loader": "7.1.2",
    "cssnano": "7.0.6",
    "daisyui": "^4.12.10",
    "mini-css-extract-plugin": "^2.9.4",
    "npm-run-all": "4.1.5",
    "postcss": "8.5.3",
    "postcss-cli": "11.0.1",
    "postcss-import": "^16.1.0",
    "rimraf": "6.0.1",
    "style-loader": "4.0.0",
    "tailwindcss": "3.4.17",
    "terser-webpack-plugin": "^5.3.10",
    "webpack": "^5.91.0",
    "webpack-cli": "^5.1.4"
  }
}

Dipendenze sono le librerie necessarie al runtime (Alpine.js, HTMX, EasyMDE, ecc.), mentre devDependenze sono strumenti di compilazione (Webpack, Babel, processori PostCSS, ecc.).

Genera script

La package.json Gli script si sono evoluti in modo significativo:

{
  "scripts": {
    "clean": "rimraf ./.tmp ./wwwroot/css/dist ./wwwroot/js/dist",
    "copy:static": "cpx \"src/css/{raleway,easymde-overrides}.css\" \"wwwroot/css/dist\"",
    "copy:highlight": "cpx \"src/css/highlight/*.min.css\" \"wwwroot/css/highlight\"",
    "copy:all": "npm-run-all --parallel copy:static copy:highlight",
    "copy:watch": "cpx \"src/css/{raleway,easymde-overrides}.css\" \"wwwroot/css/dist\" --watch & cpx \"src/css/highlight/*.min.css\" \"wwwroot/css/highlight\" --watch",
    "tw:dev": "postcss ./src/css/main.css -o ./wwwroot/css/dist/main.css",
    "tw:prod": "postcss ./src/css/main.css -o ./wwwroot/css/dist/main.css --no-map --verbose",
    "tw:watch": "postcss ./src/css/main.css -o ./wwwroot/css/dist/main.css --watch",
    "js:dev": "webpack --env development",
    "js:prod": "webpack --mode production",
    "js:watch": "webpack watch --mode development",
    "dev": "npm-run-all clean --parallel copy:all tw:dev js:dev",
    "watch": "npm-run-all clean copy:all --parallel copy:watch tw:watch js:watch",
    "build": "npm-run-all clean copy:all --parallel tw:prod js:prod"
  }
}

Questa impostazione si distingue per i seguenti aspetti:

  • puliti: Rimuove gli output di build precedenti utilizzando rimraf
  • copia:* tasks: Copia i file CSS statici che non necessitano di elaborazione (questi sono 'importa') per uno scopo speciale come caricare il mio font Funky Raleway o quelli adattivi come miglioramenti di commutazione tema.
  • tw:* attività: Processi CSS del vento di coda attraverso PostCSS
  • js:* attività: Bundles JavaScript attraverso Webpack
  • dev/watch/build: Orchestra l'intero gasdotto

La npm-run-all pacchetto consente l'esecuzione parallela per build più veloci.

Puoi impostare npm run build per eseguire automaticamente durante la costruzione locale, ma è un po 'di un dolore per CI (è necessario assicurarsi che sia disabilitato ecc).

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <SpaRoot>ClientApp\</SpaRoot>
  </PropertyGroup>

  <!-- Run npm install only when package.json changes -->
  <Target Name="NpmInstall" Inputs="$(SpaRoot)package.json" Outputs="$(SpaRoot)node_modules" BeforeTargets="Build">
    <Message Importance="high" Text="Running npm install in $(SpaRoot)" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm ci" />
  </Target>

  <!-- Run npm build before the .NET build -->
  <Target Name="NpmBuild" DependsOnTargets="NpmInstall" BeforeTargets="Build">
    <Message Importance="high" Text="Running npm run build in $(SpaRoot)" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
  </Target>

</Project>

Per farlo basta aggiungere questo al tuo csproj... Puoi anche dire che esegui solo durante il debug' etc...ma lo trovo disordinato. Preferisco semplicemente eseguirlo manualmente. npm run watch fa proprio questo, quando qualsiasi file css / js cambia esso auto-eseguire la generazione.

NOTA: Nel mondo JS quasi utilizzano hot-reload dev funziona che è abbastanza slick e ti fa un po 'odio ASP.NET Core debole tentativo.

Webpack Configurazione immersione profonda

La configurazione Webpack è dove avviene la magia e dove ho trascorso la maggior parte del mio tempo a risolvere i problemi. Questo non è venuto all'esistenza completamente formata. È il risultato di innumerevoli iterazioni, ricerche Stack Overflow, e la lettura attraverso la documentazione Webpack alle 2 del mattino cercando di capire perché la mia build stava generando 47 file di chunk.

Ecco la configurazione corrente (da webpack.config.js) con spiegazioni dettagliate di ciò che ogni parte fa e perché è lì:

const TerserPlugin = require('terser-webpack-plugin');
const path = require('path');

module.exports = (env, argv) => {
    const isProduction = argv.mode === 'production';

    return {
        mode: isProduction ? 'production' : 'development',
        entry: {
            main: './src/js/main.js',
        },
        output: {
            filename: '[name].js',
            chunkFilename: '[name].[contenthash].js',
            path: path.resolve(__dirname, 'wwwroot/js/dist'),
            publicPath: '/js/dist/',
            module: true,
            clean: true,
        },
        experiments: {
            outputModule: true,
        },
        module: {
            rules: [
                {
                    test: /\.css$/i,
                    use: ['style-loader', 'css-loader'],
                },
                {
                    test: /\.js$/,
                    exclude: /node_modules/,
                    use: {
                        loader: 'babel-loader',
                        options: {
                            presets: [
                                ['@babel/preset-env', {
                                    targets: '> 0.25%, not dead',
                                    modules: false,
                                    useBuiltIns: 'usage',
                                    corejs: 3,
                                }],
                            ],
                        },
                    },
                },
            ],
        },
        resolve: {
            extensions: ['.js', '.mjs'],
            alias: {
                '@mostlylucid/mermaid-enhancements$': path.resolve(__dirname, 'node_modules/@mostlylucid/mermaid-enhancements/dist/index.min.js')
            }
        },
        optimization: {
            splitChunks: {
                chunks: 'all',
                minSize: 20000,
                maxSize: 100000,
                name: false,
            },
            runtimeChunk: {
                name: 'runtime',
            },
            minimize: isProduction,
            minimizer: isProduction ? [
                new TerserPlugin({
                    terserOptions: {
                        ecma: 2020,
                        compress: {
                            drop_console: true,
                            passes: 3,
                            toplevel: true,
                            pure_funcs: ['console.info', 'console.debug'],
                        },
                        mangle: {
                            toplevel: true,
                        },
                        format: {
                            comments: false,
                        },
                    },
                    extractComments: true,
                }),
            ] : [],
        },
        devtool: isProduction ? false : 'eval-source-map',
        performance: {
            hints: isProduction ? 'warning' : false,
        }
    };
};

Sezioni di configurazione chiave spiegate

Dividere e tagliare i codici

optimization: {
    splitChunks: {
        chunks: 'all',
        minSize: 20000,
        maxSize: 100000,
        name: false,
    },
    runtimeChunk: {
        name: 'runtime',
    },
}

Questa configurazione divide automaticamente il codice in pezzi più piccoli:

  • pezzi: 'tutti': Analizza le importazioni sia sincrone che asincrone
  • minDimensione: 20000: Crea solo pezzi per moduli più grandi di 20KB
  • dimensione massima: 100000: Tentativi di dividere pezzi più grandi di 100KB
  • runtimeChunk: Estrae Webpack runtime in un file separato per migliorare la cache a lungo termine

In pratica, questo genera più file in wwwroot/js/dist/:

  • main.js - Il punto d'ingresso della domanda
  • runtime.js - Logica di caricamento del modulo Webpack
  • [vendor].[contenthash].js - Dividi automaticamente i pezzi del venditore
  • Ulteriori pezzi per percorsi code-split o moduli pigri

Traspilazione di Babele

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: [
                ['@babel/preset-env', {
                    targets: '> 0.25%, not dead',
                    modules: false,
                    useBuiltIns: 'usage',
                    corejs: 3,
                }],
            ],
        },
    },
}

Babel traspiala JavaScript moderno per supportare i browser più vecchi:

  • obiettivi: Supporta i browser con una quota di mercato >0,25% che sono ancora mantenuti
  • moduli: falsi: Preserva i moduli ES6 per Webpack tree-shaking
  • useBuiltIns: 'usage': Include automaticamente i polifill solo per le caratteristiche che si utilizzano
  • corejs: 3: Utilizza core-js versione 3 per polifills

Questo significa che posso scrivere JavaScript moderno (async/await, incatenamento opzionale, coalescenza nulla) pur mantenendo ampio supporto del browser.

Minificazione della produzione

minimizer: isProduction ? [
    new TerserPlugin({
        terserOptions: {
            ecma: 2020,
            compress: {
                drop_console: true,
                passes: 3,
                toplevel: true,
                pure_funcs: ['console.info', 'console.debug'],
            },
            mangle: {
                toplevel: true,
            },
            format: {
                comments: false,
            },
        },
        extractComments: true,
    }),
] : []

Terser riduce aggressivamente le costruzioni di produzione:

  • drop_console: Rimuove tutto console.log() dichiarazioni
  • Passaggi: 3: Corre la compressione tre volte per ridurre al massimo le dimensioni
  • livello superiore: vero: Mangles nomi delle variabili di primo livello
  • pure_funcs: Rimuove i metodi specifici della console anche se assegnati alle variabili
  • commenti: falso: Strisce tutti i commenti dall'output

Su questo blog, questo riduce tipicamente le dimensioni dei pacchetti JavaScript del 60-70% rispetto al codice non minificato.

Tubo PostCSS per vento di coda

CSS Tailwind viene elaborato tramite PostCSS con diversi plugin. Questa configurazione (da postcss.config.js) è misericordiosamente semplice rispetto a Webpack:

// postcss.config.js
module.exports = {
    plugins: {
        'postcss-import': {},
        tailwindcss: {},
        autoprefixer: {},
        cssnano: { preset: 'default' }
    }
}

Il gasdotto funziona come segue:

  1. Postcss-import: Risolve @import istruzioni nei file CSS
  2. chiodatura: Process Tailwind direttive e genera le classi di utilità
  3. prefissore automatico: Aggiunge i prefisso del fornitore per la compatibilità del browser
  4. cssnano: Minifica l'uscita finale CSS

Configurazione vento di coda

La tailwind.config.js file specifica dove Tailwind dovrebbe cercare i nomi della classe. Ottenere il content i percorsi a destra erano critici perdono un percorso e Tailwind non genera classi per quei file:

module.exports = {
    content: ["./Views/**/*.cshtml", "./EmailSubscription/**/*.cshtml"],
    safelist: ["dark", "light"],
    darkMode: "class",
    theme: {
        fontFamily: {
            body: ["Raleway", "sans-serif"],
        },
        extend: {
            colors: {
                "custom-light-bg": "#ffffff",
                "custom-dark-bg": "#1d232a",
                primary: "#072344",
                secondary: "#00aaa1",
                // ... more custom colours
            },
        },
    },
    plugins: [
        require("@tailwindcss/aspect-ratio"),
        require("@tailwindcss/typography"),
        require("daisyui"),
    ],
};

Caratteristiche principali:

  • contenuto: Scansioni .cshtml file per i nomi di classe (comprese le viste Razor e i modelli di posta elettronica)
  • safelist: Include sempre queste classi anche se non trovate nelle scansioni
  • darkMode: "classe": Consente la commutazione della modalità scura basata sulla classe
  • theme.extend: Aggiunge colori personalizzati e valori di spaziatura
  • plugin: Include plugin a vento di coda e libreria componenti DaisyUI

Tailwind esegue la scansione di questi file al tempo di compilazione, estraendo solo le classi utility che si effettivamente utilizzare. Ecco perché il file CSS finale è molto più piccolo della libreria completa Tailwind.

Punto di entrata JavaScript

La main.js file è dove tutto si unisce. Questo file importa e inizializza tutte le dipendenze, ed è cresciuto organicamente come ho aggiunto funzionalità al blog:

// src/js/main.js
import hljsRazor from "highlightjs-cshtml-razor";
import mermaid from "mermaid";
import Alpine from 'alpinejs';
import htmx from "htmx.org";
import hljs from "highlight.js";
import EasyMDE from "easymde";
import 'easymde/dist/easymde.min.css';
import flatpickr from "flatpickr";
import 'flatpickr/dist/flatpickr.min.css';

// Expose libraries globally for Razor views
window.Alpine = Alpine;
window.hljs = hljs;
window.htmx = htmx;
window.mermaid = mermaid;
window.EasyMDE = EasyMDE;
window.flatpickr = flatpickr;

// Import custom modules
import { typeahead } from "./typeahead";
import { submitTranslation, viewTranslation } from "./translations";
import { codeeditor } from "./simplemde_editor";
import { globalSetup } from "./global";
import { comments } from "./comments";

// Attach to namespace
window.mostlylucid = window.mostlylucid || {};
window.mostlylucid.typeahead = typeahead;
window.mostlylucid.comments = comments();
window.mostlylucid.translations = {
    submitTranslation: submitTranslation,
    viewTranslation: viewTranslation
};

// Initialise Alpine
Alpine.start();

Questo approccio offre diversi vantaggi:

  1. Punto unico di importazione: Tutte le dipendenze caricate attraverso un file di entrata
  2. Versioni esplicite: Package.json blocca le versioni specifiche
  3. Tree-shaking: Webpack rimuove il codice inutilizzato dalle librerie
  4. Esposizione globale: Biblioteche disponibili per le viste Razor via window
  5. Moduli personalizzati: Codice specifico dell'applicazione organizzato in moduli importabili

Integrazione dei modelli di disposizione

Dentro _Layout.cshtml, mi riferisco ora solo alle attività in bundle:

<head>
    <link href="/css/dist/main.css" asp-append-version="true" rel="stylesheet" />
</head>
<body>
    <!-- Content -->
    <script src="~/js/dist/main.js" type="module" asp-append-version="true"></script>
</body>

Nota da specificare module per far sapere al browser che tipo di file JS è & asp-append-version un piccolo aiutante di tag ASP.NET pulito che aggiunge una versione hashed del contenuto del file alla querystring; efficacemente cache-busting quando il file cambia.

Le sole dipendenze CDN rimanenti sono:

  • Accedi a Google: Richiede CDN esterno per funzionalità OAuth
  • BoxiconsCity name (optional, probably does not need a translation): Carattere icona (potrebbe essere in bundle, ma minimo beneficio)
  • Umami AnalyticsCity name (optional, probably does not need a translation): Script di analisi di terze parti - self hosted così posso vedere la vostra visita non Google ??

Visualizzazione del tubo di generazione

Ecco come l'intero processo di compilazione fluisce dai file sorgenti alle risorse di produzione:

graph TD
    A[Source Files] --> B[npm run build]
    B --> C[Clean Task]
    C --> D[rimraf wwwroot/css/dist wwwroot/js/dist]

    B --> E[Copy Task]
    E --> F[cpx: Copy static CSS files]
    F --> G[wwwroot/css/dist/raleway.css]
    F --> H[wwwroot/css/dist/easymde-overrides.css]

    B --> I[Tailwind Task]
    I --> J[PostCSS Pipeline]
    J --> K[postcss-import]
    K --> L[Tailwind CSS]
    L --> M[Autoprefixer]
    M --> N[cssnano]
    N --> O[wwwroot/css/dist/main.css]

    B --> P[Webpack Task]
    P --> Q[Entry: src/js/main.js]
    Q --> R[Babel Transpilation]
    R --> S[Module Resolution]
    S --> T[Tree Shaking]
    T --> U[Code Splitting]
    U --> V[Terser Minification]
    V --> W[wwwroot/js/dist/main.js]
    V --> X[wwwroot/js/dist/runtime.js]
    V --> Y[wwwroot/js/dist/vendor chunks]

    O --> Z[ASP.NET Core Static Files]
    W --> Z
    X --> Z
    Y --> Z
    G --> Z
    H --> Z

    Z --> AA[Browser]

    style A stroke:#0ea5e9,stroke-width:3px
    style Z stroke:#f59e0b,stroke-width:3px
    style AA stroke:#10b981,stroke-width:3px

Sembra piuttosto complesso, ma in realtà questo è ciò che WebPack fa, gestisce la maggior parte di questo stesso (in un modo complicato che i tipi di Vite non hanno bisogno, ma... )

Miglioramenti delle prestazioni

Ancora una volta, non proprio il PUNTO. Ma si carica come un proiettile assoluto ora. È ancora possibile ottenere problemi con il 'Rocket Loader' di Cloudflare (che strangola in qualche modo gli eventi di carico della pagina) ma è TIGHT.

Vantaggi dell'esperienza degli sviluppatori

Oltre alle prestazioni, la moderna pipeline di costruzione migliora significativamente il flusso di lavoro di sviluppo:

Tipo Sicurezza e IntelliSense

Bundling attraverso Webpack consente una corretta risoluzione del modulo JavaScript, il che significa:

  • IntelliSense in Codice VS: Completamento automatico per moduli importati
  • Vai alla definizione: Naviga direttamente al codice sorgente della libreria
  • Documentazione in linea: I commenti di JSDoc dalle librerie appaiono in IDE

QUANDO la costruzione si rompe (con npm run watch) So INSTANTLY, combinato con i (pochi ma in crescita) test unità JS significa loop di feedback più stretti.

Sostituzione del modulo caldo

Durante lo sviluppo, npm run watch consente un feedback quasi istantaneo:

npm run watch

Questo funziona Webpack in modalità orologio, la ricostruzione ha cambiato solo i moduli:

  • Tipico tempo di ricostruzione: 200-400m
  • Aggiorna automaticamente il browser: Utilizzando browser-sync o strumenti simili
  • Stato di applicazione conservato: HMR mantiene lo stato dell'applicazione durante gli aggiornamenti (dove supportato)

Gestione della dipendenza

File di lock npm (package-lock.json) assicura build coerenti in tutti gli ambienti:

npm ci  # Clean install from lock file

Ciò garantisce le stesse versioni di dipendenza negli ambienti di sviluppo, CI/CD e produzione.

Questi sono indicati come 'pinning' nel mondo JS, dove le dipendenze sono impostate in pietra. Si può fare a meno di un blocco pacchetto, ma poi si sta a seconda degli autori pacakge; spesso HUNDREDS di diverse tpeople non rovinare qualche rilascio minore...

Genera script

Gli script npm organizzati semplificano le attività comuni:

npm run dev      # One-time development build
npm run watch    # Continuous development builds
npm run build    # Production build with optimisations
npm run clean    # Remove build artefacts

Modelli e soluzioni comuni

Esporre le biblioteche miste in tutto il mondo

Alcune librerie hanno bisogno di un'esposizione globale per l'uso nelle viste Razor o negli script in linea:

// Make library available globally
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

Poi in .cshtml file:

<div x-data="{ open: false }">
    <!-- Alpine.js works because it's on window -->
</div>

Gestione dei CSS da moduli JavaScript

Alcune librerie includono il CSS che deve essere importato:

import EasyMDE from "easymde";
import 'easymde/dist/easymde.min.css';  // Imported CSS is processed by Webpack

Webpack css-loader e style-loader gestire queste importazioni:

  • Il CSS viene estratto durante la generazione
  • Iniettata nella pagina tramite <style> tag o file CSS separati
  • Prefisso automatico e minifisso

Caricamento pigro Biblioteche pesanti

Per le librerie necessarie solo su pagine specifiche, utilizzare importazioni dinamiche:

// Only load Mermaid when needed
async function initMermaid() {
    const mermaid = await import('mermaid');
    mermaid.default.initialize({ startOnLoad: true });
}

// Call when needed
if (document.querySelector('.mermaid')) {
    initMermaid();
}

Webpack suddivide automaticamente le importazioni dinamiche in pezzi separati, caricati su richiesta.

Trattare con i moduli CommonJS

Alcune librerie più vecchie usano CommonJS invece dei moduli ES6:

// CommonJS require syntax
const hljs = require('highlight.js');

// Or use dynamic import
import('highlight.js').then(hljs => {
    // Use hljs
});

Webpack gestisce entrambi i sistemi modulo in modo trasparente, convertendo CommonJS in ES6 ove necessario.

Risoluzione dei problemi comuni

Mappe di origine mancanti nello sviluppo

Assicurare devtool è configurato in webpack.config.js:

devtool: isProduction ? false : 'eval-source-map',

Questo genera mappe sorgente in sviluppo per un debug più facile.

Problemi di purgatura di classe CSS con vento di coda

Se Tailwind rimuove le classi che stai usando, controlla la content configurazione:

// tailwind.config.js
module.exports = {
    content: [
        './Views/**/*.cshtml',
        './Components/**/*.cshtml',
        // Add any other paths where classes are used
    ],
}

In alternativa, utilizzare il safelist per le classi dinamiche:

safelist: [
    'bg-blue-500',
    'text-red-600',
    {
        pattern: /bg-(red|green|blue)-(400|500|600)/,
    }
]

Modulo non trovato errori

Se Webpack non riesce a risolvere un modulo, controllare:

  1. Percorso di importazione corretto: I percorsi relativi iniziano con ./ oppure ../
  2. Estensioni file: Aggiungi .mjs a resolve.extensions se necessario
  3. Pseudonimi: Uso resolve.alias per percorsi complessi
resolve: {
    extensions: ['.js', '.mjs', '.json'],
    alias: {
        '@components': path.resolve(__dirname, 'src/js/components/'),
    }
}

Crea problemi di prestazioni

Se i build diventano lenti:

  1. Abilita la cache: La cache di Webpack accelera notevolmente la ricostruzione
  2. Ridurre il campo di applicazione di Babele: Escludere più directory dall'elaborazione di Babele
  3. Strumenti di aggiornamento: Le versioni più recenti di Webpack e Babel sono più veloci
  4. Generazioni parallele: Uso thread-loader per la traspilazione multifilata
// Enable Webpack caching
cache: {
    type: 'filesystem',
},

Lezioni imparate nel modo difficile

Lasciatemi condividere alcuni degli errori che ho commesso durante questo viaggio, in modo da poterli evitare:

Errore #1: Cercare di combinare tutto in una volta

Il mio primo tentativo ha coinvolto strappare tutti i riferimenti CDN in una sola volta e cercando di impacchettare tutto attraverso Webpack. La costruzione si è rotta in modo spettacolare. Ho imparato che la migrazione incrementale è il vostro amico Spostare una libreria alla volta, testare accuratamente, poi passare al successivo.

Errore #2: non capire i tipi di modulo

Ho sprecato ore cercando di capire perché alcune librerie non avrebbero caricato. A quanto pare, mescolando CommonJS (require()) e moduli ES6 (import) senza capire come Webpack li gestisce porta ad errori criptici. experiments: { outputModule: true } la configurazione non era nella mia configurazione iniziale, e non riuscivo a capire perché i miei moduli non fossero caricati.

La soluzione è venuta dalla lettura attraverso i problemi di GitHub sul repository Webpack a mezzanotte.

Errore #3: Sdoppiamento del codice eccessivo-aggressivo

Ad un certo punto, la mia configurazione stava generando pezzi per tutto. minSize: 10000 (10KB) che significava Webpack stava creando file separati per piccole funzioni di utilità. Il sovracorretto ed è andato a 2MB... Il carico di pagina è diventato una cascata di 50+ piccole richieste di pezzi o appeso in attesa di enormi pezzi da scaricare. Ho imparato che la divisione del codice è buona, ma avete bisogno di soglie ragionevoli. Ecco perché la mia configurazione attuale utilizza minSize: 20000 (20KB).

Errore # 4: dimenticarsi della .mjs Estensione

Alcuni pacchetti npm distribuiscono moduli ES6 con il .mjs estensione. Webpack non risolverli per impostazione predefinita. Ho trascorso un tempo imbarazzante debugging perché @mostlring I needed to add .mjsCity name (optional, probably does not need a translation)tosolution.extensions [56].

Semplice soluzione una volta che lo sai, ma scoprirlo?

Errore #5: Non Caching Webpack Costruisce

I build iniziali hanno richiesto 30-60 secondi perché non avevo abilitato la cache del filesystem di Webpack. Una volta aggiunto il caching, i tempi di ricostruzione sono scesi a 2-3 secondi. Questo è stato un cambio di gioco per il flusso di lavoro di sviluppo, ma mi ci sono volute settimane per scoprire.

Errore #6: lezioni di purgatura a vento di coda che ho effettivamente usato

La mia esperienza di debug preferita: le classi CSS che hanno funzionato bene nello sviluppo improvvisamente sono scomparse nella produzione. Tailwind li stava purgando perché sono stati generati dinamicamente in JavaScript. La soluzione era la safelist configurazione, ma solo dopo aver sprecato ore chiedendomi se stavo impazzendo. I ANCHE (per esempio nella mostlylucid.pagingtaghelper project) usa i 'blocchi dummy' dove semplifico i cambiamenti di configurazione Tailwind semplicemente avendo un blocco nascosto.


<!--
    Preserve Tailwind & DaisyUI classes used in embedded pager views.
    Without this, TailwindCSS tree-shaking removes classes from the embedded library views.
-->
<span class="hidden
    btn btn-sm btn-active btn-disabled join join-item badge badge-sm select select-bordered select-sm label label-text
    px-3 py-2 py-1 text-sm font-medium border rounded whitespace-nowrap cursor-not-allowed
    text-gray-700 text-gray-600 text-gray-400 text-white bg-white bg-gray-100 bg-blue-600 border-gray-300 border-blue-600
    hover:bg-gray-50 hover:bg-blue-700
    dark:bg-gray-800 dark:bg-gray-700 dark:bg-blue-500 dark:text-gray-300 dark:text-gray-500 dark:text-gray-200
    dark:border-gray-600 dark:border-blue-500 dark:hover:bg-gray-700 dark:hover:bg-blue-600">
</span>

Quando Tailwind costruisce scansiona i file cshtml per le classi; se non li vede li rimuove. Quindi, avendo questo blocco nascosto assicura che vede le classi di cui ho bisogno anche se sono utilizzati solo nelle viste librerie embedded. Gli sviluppatori JS comunemente anche la scansione di file JS / TS per consentire l'utilizzo di stili CSS che Tailwind altrimenti perderebbe.


module.exports = {
content: ["./Views/**/*.cshtml", "./EmailSubscription/**/*.cshtml"],
-//^^^^ This is what Tailwing knows where to look for classes duritng the tree Shake. 
+//^^^^ This is what Tailwind knows where to look for classes during the tree-shake.
  
  ...

future: {...}

}

Come detto, l'approccio 'Approvato al vento' è quello di aggiungere le classi a una safelist... ma io sono un tipo ASP.NET, quindi HTML ha vinto.

Quello che ho fatto bene: persistenza

L'unica cosa che ho fatto bene era rifiutare di rinunciare. Ogni messaggio di errore era un'opportunità di apprendimento. Ogni build rotto mi ha insegnato qualcosa di nuovo su come funzionano questi strumenti. Avevo una visione chiara in bundle, asset ottimizzati che caricano veloce e ho continuato a iterare fino a quando non ci sono arrivato.

Conclusione

Migrare da dipendenze basate su CDN a una moderna pipeline di bundling è stato trasformativo per questo blog, ma non è stato facile. I miglioramenti delle prestazioni sono sostanziali, l'esperienza degli sviluppatori è significativamente migliore, e ho un controllo molto più grande sull'ottimizzazione... ma ci sono voluti mesi di tentativi ed errori per arrivare qui.

Accompagnamenti chiave:

  1. Bundling riduce la dimensione del carico utile: Tree-shaking e minification eliminare codice inutilizzato
  2. La divisione del codice migliora le prestazioni: Browser caricare solo ciò che è necessario
  3. Strumenti di costruzione abilitano JavaScript moderno: Babel transpilation supporta le ultime funzionalità linguistiche mantenendo la compatibilità del browser
  4. Il gasdotto PostCSS ottimizza il CSS: Il compilatore JIT di Tailwind e cssnano forniscono piccoli fogli di stile
  5. L'integrazione con .NET è senza soluzione di continuità: Obiettivi MSBuild eseguire automaticamente i build del frontend
  6. La persistenza batte la perfezione: Non c'è bisogno di essere un esperto di frontend hai solo bisogno di una visione chiara e la volontà di iterare

Per ASP.NET Core sviluppatori abituati a semplice CDN include, questo si sentirà schiacciante in un primo momento. Questo è normale. Ho sentito lo stesso modo. Avviare piccolo, migrare una dipendenza alla volta, e non avere paura di rompere le cose in sviluppo. Imparerai di più dal debug build rotti che mai dalla lettura della documentazione.

La configurazione che ho condiviso qui rappresenta mesi di iterazione. Il vostro viaggio sarà diverso, e va bene. Usate questo come punto di partenza, non come vangelo. Adattatelo alle vostre esigenze, sperimentate, e non scoraggiatevi dai fallimenti che fanno parte del processo.

Questo blog e' troppo ingegnerizzato? Assolutamente. Avrei potuto continuare a usare i CDN e passare il mio tempo su altre cose? Certo. Ma non avrei imparato la metà di tanto, e non avrei questo articolo da condividere con voi. A volte l'ingegnerizzazione eccessiva non è circa la destinazione si tratta di ciò che si impara lungo la strada e di essere in grado di insegnare gli altri da quell'esperienza.

Se fai ancora affidamento sui CDN per le tue dipendenze sul frontend, ti incoraggio a provare a combinare. Inizia con una libreria. Vedi come ci si sente. Iterate da lì. L'ecosistema è maturato in modo significativo, e mentre la curva di apprendimento è reale, la vincita ne vale la pena.

Ulteriore lettura

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.