Au cours de la dernière année, j'ai trébuché, expérimenté, cassé des choses, et progressivement modernisé le pipeline de construction de frontend pour ce blog. Ce n'était pas un plan directeur exécuté sans faille – c'était un essai et une erreur, beaucoup d'erreur, et la persistance obstinée animée par une vision claire de ce que je voulais réaliser.
Je ne suis pas un gourou frontal ou un assistant Webpack. Je suis un développeur .NET qui a été frustré par les limites des dépendances basées sur CDN et a décidé qu'il devait y avoir une meilleure façon. Cet article documente le voyage messy, itérative de simple <script> tags pointant vers dépkg vers un pipeline de groupement moderne qui fonctionne réellement.
J'ai fait beaucoup d'erreurs le long du chemin (que je vais documenter), passé des heures à déboger les erreurs cryptographiques de Webpack, et probablement réinstallé node_modules Plus de fois que je ne veux l'admettre, mais la persévérance a payé, et j'ai appris une somme énorme par la volonté pure d'obtenir ce droit.
Si vous êtes un développeur de .NET regardant les fichiers de configuration Webpack se demandant dans quoi sur terre vous vous êtes mis dans – cet article est pour vous.
Je suis un vieux nutter de performance web, dans les premiers jours de la mise en ligne le premier site que j'ai construit pour l'argent (ce n'était pas un backend porno dans Perl... une histoire pour une autre fois) était un site de conditions de neige qui a jailli d'une ligne téléphonique automatisée.
À l'époque, les préoccupations étaient tout à fait différentes. JavaScript était pratiquement inconnu. Je pense que même les divs étaient RARE (à l'époque IE était div et Netscape était layer) et vous seriez LUCKY si vos utilisateurs avaient une connexion 56K. Donc j'ai appris à optimiser (même des choses comme les arrière-plans d'images carrelés pour les éléments de menu) etc...
Plus tard chez Microsoft, j'ai même travaillé sur un outil automatisé de sprite d'image dans Web Forms - c'était génial: il a extrait de petites images d'une page, généré CSS et une image sprite automatiquement. Mais hélas, comme mon temps chez Microsoft, il ne devait pas être.
Bref, c'est une obsession que j'ai porté toute ma carrière. Personne n'aime un site lent !
Pour être justeLes CDN ne sont pas intrinsèquement mauvais.
Divulgation complète: Ce blog est délibérément surexploité. besoin Mais la construction de cette façon me permet d'apprendre l'outillage frontal moderne correctement et, d'une manière cruciale, m'a donné quelque chose de réel à écrire et à enseigner. Parfois, la meilleure façon d'apprendre est de construire quelque chose de légèrement ridicule et documenter le voyage.
Au début, mon approche était simple :
<!-- 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">
Bien que cela fonctionne, il a plusieurs inconvénients qui sont devenus de plus en plus frustrants:
Donc je voulais contrôler... contrôler exactement ce que mon site a utilisé et NEEDED pour travailler. Je voulais regrouper seulement ce dont j'avais besoin, assurer un ordre de chargement fiable, et optimiser pour les performances. Cela signifiait s'éloigner des CDN et vers une approche de groupement.
Ils sont plus simples à configurer, et avec le multiplexage HTTP/2 et HTTP/3, les frais généraux de requêtes multiples sont beaucoup moins critiques qu'auparavant. Pour de nombreux projets, en particulier les petits ou les prototypes, les CDN restent un choix parfaitement raisonnable. Mais pour un site de production où je voulais le contrôle, la fiabilité et l'optimisation, le regroupement était plus logique.
Ma configuration actuelle regroupe toutes les dépendances JavaScript via Webpack et traite CSS via PostCSS.
Avant de plonger dans les détails techniques, je devrais m'adresser à l'éléphant dans la pièce: pourquoi suis-je en utilisant Webpack lorsque Vite, Rollup, esbuild, et d'autres faisceaux modernes existent?
La réponse honnête est simple: J'ai déjà connu Webpack.
J'avais beaucoup utilisé Webpack lors de l'enseignement de mon cours "Début du développement Web", et c'était l'outil que j'ai compris. Quand j'ai décidé de moderniser le pipeline de construction de ce blog, j'étais déjà confronté à une courbe d'apprentissage raide – comprendre le façonnage des arbres, le fractionnement des codes, les systèmes de modules, les pipelines PostCSS, et comment intégrer tout cela avec ASP.NET Core.
C'est généralement une bonne pratique lorsqu'on travaille sur des projets ; limiter le « nouveau » au « maniable ». Il est plus facile de limiter l'incertitude.
Ajouter "apprendre un outil de construction entièrement nouveau" en plus de cela semblait inutile. Webpack fonctionne. Il est mature. Il a une excellente documentation et un écosystème massif. Plus important, je pourrais concentrer mon énergie d'apprentissage sur le concepts d'un groupement moderne plutôt que les idiosyncrasies d'un outil particulier.
Vite est-il plus rapide ? Tout à fait. Le serveur de développement de Vite avec des modules ES natifs et un groupement alimenté par esbuild est significativement plus rapide que Webpack. Pour les grands projets avec des centaines de modules, la différence est dramatique.
Devriez-vous utiliser Vite pour un nouveau projet ? Probablement, oui. Si vous commencez frais et n'avez pas de connaissances Webpack existantes, Vite est probablement le meilleur choix. C'est plus rapide, plus simple à configurer, et représente l'approche moderne de l'outillage frontend.
Est-ce que je regrette d'utiliser Webpack? Les leçons que j'ai apprises sur le regroupement, le fractionnement de code et l'optimisation sont transférables à n'importe quel outil de construction. Et honnêtement, pour l'échelle de ce blog, la différence de performance entre Webpack et Vite est négligeable – nous parlons de millisecondes dans les temps de reconstruction du développement.
C'est un aspect clé de la façon dont je construis des choses; commencer par ce qui est EASY et construire à partir de cette fondation.
La leçon la plus large ici est que le progrès bat la perfection. J'aurais pu passer des semaines à chercher le "meilleur" bundler, à comparer les points de repère, à lire des articles de comparaison, et à m'agoniser sur la décision. Au lieu de cela, j'ai choisi l'outil que je connaissais, je l'ai fait fonctionner, et j'ai avancé. Ce pragmatisme m'a permis d'expédier plutôt que de délibérer sans fin.
Peut-être qu'un jour je pourrais migrer vers Vite. Peut-être que je ne le ferai pas. De toute façon, ce blog a un pipeline de construction moderne et optimisé qui fonctionne de façon fiable – et c'est ce qui compte.
Les package.json maintenant maintient deux groupes de dépendance distincts. Cette structure m'a pris plusieurs tentatives pour obtenir à droite - initialement, Nuget et npm sont similaires KINDA mais nppm est beaucoup moins sympathique lors de l'ajout de 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"
}
}
Dépendances sont des bibliothèques nécessaires à l'exécution (Alpine.js, HTMX, EasyMDE, etc.), tandis que devDépendances sont des outils de construction (Webpack, Babel, processeurs PostCSS, etc.).
Les package.json Les scripts ont considérablement évolué :
{
"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"
}
}
Cette configuration sépare les questions suivantes:
rimrafLes npm-run-all paquet permet l'exécution parallèle pour des constructions plus rapides.
Vous pouvez définir npm run build pour courir automatiquement pendant votre construction locale, mais c'est un peu une douleur pour l'IC (vous devez vous assurer qu'il est désactivé etc).
<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>
Pour le faire, il suffit d'ajouter ceci à votre csproj...Vous pouvez même dire que n'exécuter que pendant debug' etc... mais je le trouve désordonné. Je préfère simplement l'exécuter manuellement. npm run watch fait juste cela, quand n'importe quel fichier css / js change il lance automatiquement la build.
REMARQUE: Dans le monde JS ils utilisent presque dev runs hot-reload qui est assez slick et vous fait plutôt détester la tentative faible d'ASP.NET Core.
La configuration du Webpack est l'endroit où la magie se produit, et où j'ai passé la plupart de mon temps à dépanner. Cela n'est pas venu à l'existence complètement formée. C'est le résultat d'innombrables itérations, recherches Stack Overflow, et la lecture à travers la documentation Webpack à 2h du matin essayant de comprendre pourquoi ma construction générait 47 fichiers morceaux.
Voici la configuration actuelle (à partir de webpack.config.js) avec des explications détaillées sur ce que chaque partie fait et pourquoi elle est 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,
}
};
};
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 100000,
name: false,
},
runtimeChunk: {
name: 'runtime',
},
}
Cette configuration divise automatiquement votre code en petits morceaux :
Dans la pratique, cela génère plusieurs fichiers dans wwwroot/js/dist/:
main.js - Point d'entrée de votre demanderuntime.js - Logique de chargement du module Webpack[vendor].[contenthash].js - Couper automatiquement les morceaux des fournisseurs{
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 transpiles moderne JavaScript to support older browsers:
Cela signifie que je peux écrire le JavaScript moderne (async/attendu, enchaînement optionnel, fusion nullish) tout en maintenant un large support de navigateur.
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 réduit agressivement les constructions de production :
console.log() DéclarationsSur ce blog, cela réduit généralement les tailles de paquets JavaScript de 60-70% par rapport au code non-minifié.
Tailwind CSS est traité par PostCSS avec plusieurs plugins. postcss.config.js) est miséricordieuxment simple par rapport à Webpack:
// postcss.config.js
module.exports = {
plugins: {
'postcss-import': {},
tailwindcss: {},
autoprefixer: {},
cssnano: { preset: 'default' }
}
}
Le pipeline fonctionne comme suit:
@import les déclarations dans les fichiers CSSLes tailwind.config.js fichier spécifie où Tailwind devrait chercher les noms de classe. content chemins droit était critique — manquer un chemin et Tailwind ne générera pas de classes pour ces fichiers:
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"),
],
};
Principales caractéristiques:
.cshtml fichiers pour les noms de classe (y compris les vues Razor et les modèles de courriel)Tailwind scanne ces fichiers au moment de la construction, en extrayant seulement les classes d'utilité que vous utilisez réellement. C'est pourquoi le fichier CSS final est beaucoup plus petit que la bibliothèque complète Tailwind.
Les main.js Ce fichier importe et initialise toutes les dépendances, et il est cultivé organiquement comme j'ai ajouté des fonctionnalités au 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();
Cette approche présente plusieurs avantages :
windowDans _Layout.cshtml, je ne fais maintenant référence qu'aux actifs groupés:
<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>
Note I : Préciser module pour que le navigateur sache quel type de fichier JS c'est & asp-append-version un petit helper de balise ASP.NET qui ajoute une version hashed du contenu du fichier à la requête; efficacement cache-busting lorsque le fichier change.
Les seules dépendances restantes du RNC sont :
Voici comment l'ensemble du processus de construction passe des fichiers sources aux actifs de production :
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
Il semble assez complexe mais vraiment c'est ce que WebPack fait, il gère la plupart de ceci lui-même (d'une manière compliquée que les semblables de Vite n'ont pas besoin mais.. )
Encore une fois, pas vraiment le POINT. Mais il ne charge comme une balle absolue maintenant. Vous pouvez toujours obtenir des problèmes avec le 'Rocket Loader' de Cloudflare (qui manipule les événements de chargement de page un peu) mais c'est TIGHT.
Au-delà de la performance, le pipeline de construction moderne améliore considérablement le flux de travail de développement :
Le bundling via Webpack permet une résolution correcte du module JavaScript, ce qui signifie :
QUAND la construction se brise (avec npm run watch) Je sais INSTANTLY, combiné avec l'unité (peu mais en croissance) JS teste cela signifie des boucles de rétroaction plus serrées.
En cours de développement, npm run watch permet une rétroaction quasi instantanée:
npm run watch
Ceci exécute Webpack en mode montre, reconstruisant seulement les modules modifiés:
fichier de verrouillage de npm (package-lock.json) assure des constructions cohérentes entre les environnements:
npm ci # Clean install from lock file
Cela garantit les mêmes versions de dépendance dans les environnements de développement, de CI/CD et de production.
Ils sont appelés «pinning» dans le monde JS, où les dépendances sont mises en pierre. Vous pouvez le faire sans serrure de paquet, mais ensuite vous dépendez des auteurs pacakge; souvent HUNDREDS de personnes différentes qui ne gâchent pas une certaine libération mineure.
Les scripts npm organisés facilitent les tâches communes:
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
Certaines bibliothèques ont besoin d'une exposition globale pour être utilisées dans les vues de Razor ou les scripts en ligne:
// Make library available globally
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
Puis dans .cshtml fichiers & #160;:
<div x-data="{ open: false }">
<!-- Alpine.js works because it's on window -->
</div>
Certaines bibliothèques comprennent le CSS qui a besoin d'importer :
import EasyMDE from "easymde";
import 'easymde/dist/easymde.min.css'; // Imported CSS is processed by Webpack
Les Webpacks css-loader et style-loader traiter ces importations:
<style> tags ou fichiers CSS séparésPour les bibliothèques uniquement nécessaires sur des pages spécifiques, utilisez des importations dynamiques :
// 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 envoie automatiquement des code-splits d'importations dynamiques dans des morceaux séparés, chargés à la demande.
Certaines bibliothèques plus anciennes utilisent CommonJS au lieu des modules ES6 :
// CommonJS require syntax
const hljs = require('highlight.js');
// Or use dynamic import
import('highlight.js').then(hljs => {
// Use hljs
});
Webpack gère les deux systèmes de module de manière transparente, convertissant CommonJS en ES6 au besoin.
Veiller à ce que devtool est configuré dans webpack.config.js:
devtool: isProduction ? false : 'eval-source-map',
Cela génère des cartes sources en développement pour faciliter le débogage.
Si Tailwind supprime les cours que vous utilisez, vérifiez content configuration & #160;:
// tailwind.config.js
module.exports = {
content: [
'./Views/**/*.cshtml',
'./Components/**/*.cshtml',
// Add any other paths where classes are used
],
}
Alternativement, utiliser le safelist pour les classes dynamiques:
safelist: [
'bg-blue-500',
'text-red-600',
{
pattern: /bg-(red|green|blue)-(400|500|600)/,
}
]
Si Webpack ne peut pas résoudre un module, vérifiez :
./ ou ../.mjs à l ' Assemblée générale resolve.extensions si nécessaireresolve.alias pour les chemins complexesresolve: {
extensions: ['.js', '.mjs', '.json'],
alias: {
'@components': path.resolve(__dirname, 'src/js/components/'),
}
}
Si les constructions deviennent lentes:
thread-loader pour la transpilation multithreadée// Enable Webpack caching
cache: {
type: 'filesystem',
},
Permettez-moi de partager quelques-unes des erreurs que j'ai commises pendant ce voyage, afin que vous puissiez les éviter:
Ma première tentative consistait à retirer toutes les références CDN d'un seul coup et à essayer de tout rassembler dans Webpack. La construction s'est effondrée de façon spectaculaire. J'ai appris que la migration progressive est votre ami – déplacer une bibliothèque à la fois, tester soigneusement, puis passer à la suivante.
J'ai perdu des heures à essayer de comprendre pourquoi certaines bibliothèques ne se chargeraient pas.require()) et les modules ES6 (import) sans comprendre comment Webpack les gère conduit à des erreurs cryptographiques. experiments: { outputModule: true } La configuration n'était pas dans ma configuration initiale, et je n'ai pas réussi à comprendre pourquoi mes modules ne sont pas chargés.
La solution est venue de la lecture à travers les problèmes GitHub sur le dépôt Webpack à minuit.
À un moment donné, ma configuration générait des morceaux pour tout. minSize: 10000 (10KB) ce qui signifiait que Webpack créait des fichiers séparés pour de petites fonctions d'utilité. Le sur-corrigé et est allé à 2MB... Le chargement de page est devenu une cascade de 50+ petites requêtes de morceaux ou accroché en attendant que d'énormes morceaux à télécharger. J'ai appris que le fractionnement de code est bon, mais vous avez besoin de seuils raisonnables. C'est pourquoi ma config actuelle utilise minSize: 20000 (20Ko).
.mjs ProlongationCertains paquets npm distribuent des modules ES6 avec .mjs extension. Webpack ne les résoudra pas par défaut. J'ai passé un temps embarrassant à déboger pourquoi @mostlring I needed to add .mjstoRésoudre.extensions'.
Une fois que vous le savez, ça a pris du temps.
Les premières constructions ont pris 30-60 secondes parce que je n'avais pas activé le cache système de fichiers de Webpack. Une fois que j'ai ajouté cache, les temps de reconstruction sont tombés à 2-3 secondes. C'était un changement de jeu pour le flux de travail de développement, mais m'a pris des semaines à découvrir.
Mon expérience de débogage préférée : les classes CSS qui fonctionnaient bien dans le développement ont soudainement disparu dans la production. Tailwind les purgeait parce qu'elles ont été générées dynamiquement dans JavaScript. La solution était la safelist La configuration, mais seulement après que j'ai gaspillé des heures à me demander si je devenais fou.
I ALSO (par exemple dans mostlylucid.pagingtaghelper project) utilisent des 'blocs de dummy' où je simplifie mes changements de configuration Tailwind en ayant juste un bloc caché.
<!--
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>
Lorsque Tailwind construit il scanne les fichiers cshtml pour les classes; s'il ne les voit pas, il les supprime. Donc, avoir ce bloc caché assure qu'il voit les classes dont j'ai besoin même si elles ne sont utilisées que dans les vues de bibliothèques intégrées.
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: {...}
}
Comme mentionné, l'approche « Tailwind Approuvé » est d'ajouter les classes à une liste de sécurité... mais je suis un gars ASP.NET, donc HTML a gagné.
La seule chose que j'ai bien fait était de refuser d'abandonner. Chaque message d'erreur était une opportunité d'apprentissage. Chaque construction cassée m'a appris quelque chose de nouveau sur le fonctionnement de ces outils.
La migration des dépendances basées sur le CDN vers un pipeline de regroupement moderne a été transformée pour ce blog, mais ce n'était pas facile. Les améliorations de performance sont substantielles, l'expérience du développeur est significativement meilleure, et j'ai beaucoup plus de contrôle sur l'optimisation — mais il a fallu des mois d'essai et d'erreur pour arriver ici.
Prises à emporter clés:
Pour les développeurs ASP.NET Core habitués au simple CDN comprend, cela se sentira accablant au début. C'est normal. Je me suis senti de la même manière. Commencez petit, migrez une dépendance à la fois, et n'ayez pas peur de casser les choses dans le développement. Vous apprendrez plus de débogage des constructions cassées que vous ne le ferez jamais de la documentation de lecture.
La configuration que j'ai partagée ici représente des mois d'itération. Votre voyage sera différent, et c'est bien. Utilisez ceci comme point de départ, pas comme évangile. Adaptez-le à vos besoins, expérimenter, et ne soyez pas découragé par les échecs – ils font partie du processus.
Ce blog est-il surexploité ? Absolument. Aurais-je pu continuer à utiliser les CDN et passer mon temps sur d'autres choses? Bien sûr. Mais je n'aurais pas appris la moitié autant, et je n'aurais pas eu cet article à partager avec vous. Parfois, la suringénierie n'est pas à propos de la destination — c'est à propos de ce que vous apprenez le long du chemin et être en mesure d'enseigner aux autres de cette expérience.
Si vous comptez toujours sur les CDN pour vos dépendances frontales, je vous encourage à donner un coup d'essai. Commencez par une bibliothèque. Voyez comment ça se sent. Itérer à partir de là. L'écosystème a mûri de façon significative, et bien que la courbe d'apprentissage soit réelle, le gain en vaut la peine.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.