Hallo Habr!
Vor nicht allzu langer Zeit wurde mir klar, dass die Arbeit mit CSS in all meinen Anwendungen für Entwickler und Benutzer schmerzhaft ist.
Unter dem Schnitt sind meine Probleme, eine Menge seltsamer Codes und Fallstricke auf dem Weg zur korrekten Arbeit mit Stilen.
CSS-Problem
In den von mir durchgeführten React- und Vue-Projekten war die Herangehensweise an Stile ungefähr dieselbe. Das Projekt wird per Webpack zusammengestellt, eine CSS-Datei wird vom Haupteinstiegspunkt importiert. Diese Datei importiert die restlichen CSS-Dateien, die BEM zum Benennen von Klassen verwenden, in sich.
styles/ indes.css blocks/ apps-banner.css smart-list.css ...
Ist das bekannt? Ich habe diese Implementierung fast überall verwendet. Und alles war in Ordnung, bis einer der Standorte so weit gewachsen war, dass die Probleme mit den Stilen meine Augen stark irritierten.
1. Das Problem des Hot-ReloadDas Importieren von Stilen voneinander erfolgte über das Postcss-Plugin oder den Stylus-Loader.
Der Haken ist:
Wenn wir Importe über das Postcss- oder Stylus-Loader-Plugin lösen, ist die Ausgabe eine große CSS-Datei. Selbst bei einer geringfügigen Änderung in einem der Stylesheets werden jetzt alle CSS-Dateien erneut verarbeitet.
Die Geschwindigkeit des Hot-Reload wird wirklich beeinträchtigt: Die Verarbeitung von ~ 950 KByte Stiftdateien dauert ca. 4 Sekunden.
Hinweis zum CSS-LoaderWenn der Import von CSS-Dateien über CSS-Loader gelöst worden wäre, wäre ein solches Problem nicht aufgetreten:
CSS-Loader verwandelt CSS in JavaScript. Es werden alle Stilimporte durch erfordern ersetzt. Das Ändern einer CSS-Datei wirkt sich dann nicht auf andere Dateien aus, und das Hot-Reload erfolgt schnell.
Zum CSS-Loader
@import './test.css'; html, body { margin: 0; padding: 0; width: 100%; height: 100%; } body { background-color: red; }
Nachher
2. Das Problem der Code-AufteilungWenn Stile aus einem separaten Ordner geladen werden, kennen wir nicht den Verwendungskontext der einzelnen Stile. Mit diesem Ansatz ist es nicht möglich, CSS in mehrere Teile zu zerlegen und diese nach Bedarf zu laden.
3. Großartige CSS-KlassennamenJeder BEM-Klassenname sieht folgendermaßen aus: Blockname__Elementname. Ein so langer Name wirkt sich stark auf die endgültige CSS-Dateigröße aus: Auf der Habr-Website beispielsweise nehmen CSS-Klassennamen 36% der Größe der Style-Datei ein.
Google ist sich dieses Problems bewusst und verwendet in all seinen Projekten seit langem die Namensminimierung:
Ein Stück google.comAll diese Probleme haben mich in Ordnung gebracht, ich habe mich schließlich entschlossen, sie zu beenden und das perfekte Ergebnis zu erzielen.
Entscheidungsauswahl
Um alle oben genannten Probleme zu beseitigen, habe ich zwei Lösungen gefunden: CSS In JS (Styled-Components) und CSS-Module.
Ich habe keine kritischen Fehler in diesen Lösungen gesehen, aber am Ende fiel meine Wahl aus mehreren Gründen auf CSS-Module:
- Sie können CSS in eine separate Datei einfügen, um JS und CSS separat zwischenzuspeichern.
- Weitere Optionen für Linterstile.
- Es ist üblicher, mit CSS-Dateien zu arbeiten.
Die Wahl ist getroffen, es ist Zeit zu kochen!
Grundeinstellung
Konfigurieren Sie das Webpack ein wenig. Fügen Sie CSS-Loader hinzu und aktivieren Sie CSS-Module darin:
module.exports = { module: { rules: [ { test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, } }, ], }, ], }, };
Jetzt werden wir CSS-Dateien in Ordner mit Komponenten verteilen. Innerhalb jeder Komponente importieren wir die erforderlichen Stile.
project/ components/ CoolComponent/ index.js index.css
.contentWrapper { padding: 8px 16px; background-color: rgba(45, 45, 45, .3); } .title { font-size: 14px; font-weight: bold; } .text { font-size: 12px; }
import React from 'react'; import styles from './index.css'; export default ({ text }) => ( <div className={styles.contentWrapper}> <div className={styles.title}> Weird title </div> <div className={styles.text}> {text} </div> </div> );
Nachdem wir die CSS-Dateien beschädigt haben, werden beim Hot-Reload nur Änderungen an einer Datei verarbeitet. Problem Nummer 1 gelöst, Prost!
Teilen Sie CSS in Stücke
Wenn ein Projekt viele Seiten hat und der Client nur eine davon benötigt, ist es nicht sinnvoll, alle Daten abzupumpen. React hat dafür eine großartige reaktionsladbare Bibliothek. Sie können eine Komponente erstellen, die bei Bedarf die benötigte Datei dynamisch herunterlädt.
import Loadable from 'react-loadable'; import Loading from 'path/to/Loading'; export default Loadable({ loader: () => import('path/to/CoolComponent'), loading: Loading, });
Webpack wandelt die CoolComponent-Komponente in eine separate JS-Datei (Chunk) um, die beim Rendern von AsyncCoolComponent heruntergeladen wird.
Gleichzeitig enthält CoolComponent eigene Stile. CSS liegt bisher als JS-Zeichenfolge darin und wird mit dem Style-Loader als Style eingefügt. Aber warum schneiden wir die Stile nicht in eine separate Datei?
Wir werden unsere eigene CSS-Datei sowohl für die Hauptdatei als auch für jeden der Chunks erstellen.
Installieren Sie das Mini-CSS-Extract-Plugin und zaubern Sie mit der Webpack-Konfiguration:
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ (isDev ? 'style-loader' : MiniCssExtractPlugin.loader), { loader: 'css-loader', options: { modules: true, }, }, ], }, ], }, plugins: [ ...(isDev ? [] : [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', chunkFilename: '[name].[contenthash].css', }), ]), ], };
Das ist alles! Lassen Sie uns das Projekt im Produktionsmodus zusammenstellen, den Browser öffnen und die Registerkarte Netzwerk anzeigen:
// GET /main.aff4f72df3711744eabe.css GET /main.43ed5fc03ceb844eab53.js // CoolComponent , JS CSS GET /CoolComponent.3eaa4773dca4fffe0956.css GET /CoolComponent.2462bbdbafd820781fae.js
Problem Nummer 2 ist vorbei.
Wir minimieren CSS-Klassen
Css-loader ändert die darin enthaltenen Klassennamen und gibt eine Variable mit der Zuordnung lokaler Klassennamen zu global zurück.
Nach unserer Grundeinstellung generiert der CSS-Loader einen langen Hash basierend auf dem Namen und dem Speicherort der Datei.
Im Browser sieht unsere CoolComponent jetzt folgendermaßen aus:
<div class="rs2inRqijrGnbl0txTQ8v"> <div class="_2AU-QBWt5K2v7J1vRT0hgn"> Weird title </div> <div class="_1DaTAH8Hgn0BQ4H13yRwQ0"> Lorem ipsum dolor sit amet consectetur. </div> </div>
Das reicht uns natürlich nicht.
Es ist notwendig, dass es während der Entwicklung Namen gibt, unter denen der ursprüngliche Stil gefunden werden kann. Im Produktionsmodus sollten Klassennamen minimiert werden.
Mit Css-Loader können Sie die Änderung von Klassennamen über die Optionen localIdentName und getLocalIdent anpassen. Im Entwicklungsmodus setzen wir den beschreibenden localIdentName - '[Pfad] _ [Name] _ [Lokal]' und für den Produktionsmodus erstellen wir eine Funktion, die Klassennamen minimiert:
const getScopedName = require('path/to/getScopedName'); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ (isDev ? 'style-loader' : MiniCssExtractPlugin.loader), { loader: 'css-loader', options: { modules: true, ...(isDev ? { localIdentName: '[path]_[name]_[local]', } : { getLocalIdent: (context, localIdentName, localName) => ( getScopedName(localName, context.resourcePath) ), }), }, }, ], }, ], }, };
Und hier haben wir in der Entwicklung schöne visuelle Namen:
<div class="src-components-ErrorNotification-_index_content-wrapper"> <div class="src-components-ErrorNotification-_index_title"> Weird title </div> <div class="src-components-ErrorNotification-_index_text"> Lorem ipsum dolor sit amet consectetur. </div> </div>
Und in der Produktion minimierte Klassen:
<div class="e_f"> <div class="e_g"> Weird title </div> <div class="e_h"> Lorem ipsum dolor sit amet consectetur. </div> </div>
Das dritte Problem ist überwunden.
Entfernen Sie unnötige Cache-Ungültigkeit
Versuchen Sie mit der oben beschriebenen Klassenminimierungstechnik, das Projekt mehrmals zu erstellen. Achten Sie auf Datei-Caches:
/* */ app.bf70bcf8d769b1a17df1.js app.db3d0bd894d38d036117.css /* */ app.1f296b75295ada5a7223.js app.eb2519491a5121158bd2.css
Es scheint, dass wir nach jedem neuen Build Caches ungültig gemacht haben. Wie so?
Das Problem ist, dass Webpack nicht die Reihenfolge garantiert, in der Dateien verarbeitet werden. Das heißt, CSS-Dateien werden in einer unvorhersehbaren Reihenfolge verarbeitet. Für denselben Klassennamen mit unterschiedlichen Assemblys werden unterschiedliche minimierte Namen generiert.
Um dieses Problem zu beheben, speichern wir die Daten zu den generierten Klassennamen zwischen Assemblys. Aktualisieren Sie die Datei getScopedName.js ein wenig:
const incstr = require('incstr');
Die Implementierung der Datei generatorHelpers.js spielt keine Rolle, aber wenn Sie interessiert sind, ist hier meine:
generatorHelpers.js const fs = require('fs'); const path = require('path'); const getGeneratorDataPath = generatorIdentifier => ( path.resolve(__dirname, `meta/${generatorIdentifier}.json`) ); const getGeneratorData = (generatorIdentifier) => { const path = getGeneratorDataPath(generatorIdentifier); if (fs.existsSync(path)) { return require(path); } return {}; }; const saveGeneratorData = (generatorIdentifier, uniqIds) => { const path = getGeneratorDataPath(generatorIdentifier); const data = JSON.stringify(uniqIds, null, 2); fs.writeFileSync(path, data, 'utf-8'); }; module.exports = { getGeneratorData, saveGeneratorData, };
Die Caches sind zwischen den Builds gleich geworden, alles ist in Ordnung. Ein weiterer Punkt zu unseren Gunsten!
Entfernen Sie die Laufzeitvariable
Da ich mich für eine bessere Entscheidung entschieden habe, wäre es schön, diese Variable bei der Zuordnung von Klassen zu entfernen, da wir alle erforderlichen Daten in der Kompilierungsphase haben.
Babel-Plugin-React-CSS-Module helfen uns dabei. Zum Zeitpunkt der Kompilierung gilt Folgendes:
- Findet den CSS-Import in der Datei.
- Diese CSS-Datei wird geöffnet und die CSS-Klassennamen werden wie beim CSS-Loader geändert.
- Es werden JSX-Knoten mit dem styleName-Attribut gefunden.
- Ersetzt lokale Klassennamen von styleName durch globale.
Richten Sie dieses Plugin ein. Spielen wir mit der Babel-Konfiguration:
Aktualisieren Sie unsere JSX-Dateien:
import React from 'react'; import './index.css'; export default ({ text }) => ( <div styleName="content-wrapper"> <div styleName="title"> Weird title </div> <div styleName="text"> {text} </div> </div> );
Deshalb haben wir die Verwendung der Variablen für die Anzeige von Stilnamen eingestellt. Jetzt haben wir sie nicht mehr!
... oder ist da?
Wir werden das Projekt sammeln und die Quellen untersuchen:
function(e, t, n) { e.exports = { "content-wrapper": "e_f", title: "e_g", text: "e_h" } }
Es sieht so aus, als ob die Variable noch vorhanden ist, obwohl sie nirgendwo verwendet wird. Warum ist das passiert?
Das Webpack unterstützt verschiedene Arten von modularen Strukturen. Die beliebtesten sind ES2015 (Import) und CommonJS (erforderlich).
ES2015-Module unterstützen im Gegensatz zu commonJS aufgrund ihrer statischen Struktur das Baumschütteln.
Sowohl der CSS-Loader als auch der Mini-CSS-Extract-Plugin-Loader verwenden die CommonJS-Syntax, um Klassennamen zu exportieren, sodass die exportierten Daten nicht aus dem Build gelöscht werden.
Wir werden unseren kleinen Lader schreiben und die zusätzlichen Daten im Produktionsmodus löschen:
const path = require('path'); const resolve = relativePath => path.resolve(__dirname, relativePath); const isDev = process.env.NODE_ENV === 'development'; module.exports = { module: { rules: [ { test: /\.css$/, use: [ ...(isDev ? ['style-loader'] : [ resolve('path/to/webpack-loaders/nullLoader'), MiniCssExtractPlugin.loader, ]), { loader: 'css-loader', }, ], }, ], }, };
Überprüfen Sie die zusammengestellte Datei erneut:
function(e, t, n) {}
Sie können erleichtert ausatmen, alles hat funktioniert.
Fehler beim Löschen der KlassenzuordnungsvariablenZunächst schien es mir am offensichtlichsten, das bereits vorhandene
Null-Loader- Paket zu verwenden.
Aber alles stellte sich als nicht so einfach heraus:
export default function() { return '// empty (null-loader)'; } export function pitch() { return '// empty (null-loader)'; }
Wie Sie sehen können, exportiert der Nulllader neben der Hauptfunktion auch die Pitch-Funktion. Ich habe
aus der Dokumentation gelernt, dass Pitch-Methoden früher als andere aufgerufen werden und alle nachfolgenden Loader abbrechen können, wenn sie einige Daten von dieser Methode zurückgeben.
Mit einem Nulllader sieht die CSS-Produktionssequenz folgendermaßen aus:
- Die Pitch-Methode des Nullladers wird aufgerufen, die eine leere Zeichenfolge zurückgibt.
- Aufgrund des Rückgabewerts der Pitch-Methode werden nicht alle nachfolgenden Lader aufgerufen.
Ich sah die Lösungen nicht mehr und beschloss, meinen eigenen Lader zu bauen.
Verwendung mit Vue.js.Wenn Sie nur eine Vue.js zur Hand haben, aber wirklich die Klassennamen komprimieren und die Laufzeitvariable entfernen möchten, dann habe ich einen großartigen Hack!
Wir brauchen nur zwei Plugins: babel-plugin-transform-vue-jsx und babel-plugin-react-css-modules. Wir benötigen das erste, um JSX in Renderfunktionen zu schreiben, und das zweite, wie Sie bereits wissen, um Namen in der Kompilierungsphase zu generieren.
module.exports = { plugins: [ 'transform-vue-jsx', ['react-css-modules', {
import './index.css'; const TextComponent = { render(h) { return( <div styleName="text"> Lorem ipsum dolor. </div> ); }, mounted() { console.log('I\'m mounted!'); }, }; export default TextComponent;
Komprimieren Sie CSS vollständig
Stellen Sie sich vor, das folgende CSS erschien im Projekt:
.component1__title { color: red; } .component2__title { color: green; } .component2__title_red { color: red; }
Sie sind ein CSS-Minifier. Wie würden Sie es drücken?
Ich denke, Ihre Antwort lautet ungefähr so:
.component2__title{color:green} .component2__title_red, .component1__title{color:red}
Jetzt werden wir überprüfen, was die üblichen Minifikatoren tun. Geben Sie unseren Code in einen
Online-Minifier ein :
.component1__title{color:red} .component2__title{color:green} .component2__title_red{color:red}
Warum konnte er nicht?
Der Minifier befürchtet, dass aufgrund einer Änderung der Reihenfolge der Stildeklaration etwas kaputt geht. Wenn das Projekt beispielsweise diesen Code hat:
<div class="component1__title component2__title">Some weird title</div>
Wegen dir wird der Titel rot und der Online-Minifier verlässt die richtige Reihenfolge der Stildeklaration und wird grün. Natürlich wissen Sie, dass es niemals den Schnittpunkt von component1__title und component2__title geben wird, da sie sich in verschiedenen Komponenten befinden. Aber wie soll man das dem Minifier sagen?
Nachdem ich die Dokumentation durchsucht hatte, fand ich die Möglichkeit, den Kontext für die Verwendung von Klassen nur mit
csso anzugeben . Und er hat keine bequeme Lösung für ein sofort einsatzbereites Webpack. Um weiter zu gehen, brauchen wir ein kleines Fahrrad.
Sie müssen die Klassennamen jeder Komponente in separaten Arrays kombinieren und in csso angeben. Etwas früher haben wir minimierte Klassennamen nach diesem Muster generiert: '[componentId] _ [classNameId]'. Klassennamen können also einfach durch den ersten Teil des Namens kombiniert werden!
Schnallen Sie sich an und schreiben Sie Ihr Plugin:
const cssoLoader = require('path/to/cssoLoader'); module.exports = { plugins: [ new cssoLoader(), ], };
const csso = require('csso'); const RawSource = require('webpack-sources/lib/RawSource'); const getScopes = require('./helpers/getScopes'); const isCssFilename = filename => /\.css$/.test(filename); module.exports = class cssoPlugin { apply(compiler) { compiler.hooks.compilation.tap('csso-plugin', (compilation) => { compilation.hooks.optimizeChunkAssets.tapAsync('csso-plugin', (chunks, callback) => { chunks.forEach((chunk) => {
const csso = require('csso'); const getComponentId = (className) => { const tokens = className.split('_');
Und es war nicht so schwierig, oder? Normalerweise komprimiert diese Minimierung das CSS zusätzlich um 3-6%.
War es das wert?
Natürlich.
In meinen Anwendungen trat schließlich ein schnelles Hot-Reload auf, und CSS begann, in Stücke zu zerfallen und durchschnittlich 40% weniger zu wiegen.
Dies beschleunigt das Laden der Website und verkürzt die Zeit für das Parsen von Stilen, was nicht nur Benutzer, sondern auch CEOs betrifft.
Der Artikel ist stark gewachsen, aber ich bin froh, dass jemand ihn bis zum Ende scrollen konnte. Danke für deine Zeit!
Verwendete Materialien