DRY e i mixin

Una delle feature più potenti del CSS preprocessor Sass sono i mixin: un’astrazione di un pattern comune in un pezzo (“chunk”, ndr) semantico e riutilizzabile. Pensate di prendere gli stili per un pulsante e, invece di dover ricordare tutte le sue proprietà, poter avere un selettore che includa gli stili del pulsante quando servono. Tali stili si troverebbero in una singola posizione, rendendone semplice l’aggiornamento e facendo in modo che rimangano coerenti.

L’articolo prosegue sotto

Tuttavia, troppo spesso, i mixin sono scritti in una maniera che porta alla duplicazione delle proprietà, creando così più punti di malfunzionamento nel CSS in output e facendo aumentare la dimensione del file. Ora, se usate Sass, siete già sulla strada giusta per la creazione di CSS più piccoli rispetto al programmatore medio. Ma se siete ossessionati dalla performance come lo sono io, non potete accettare alcun elemento in eccesso. In questo articolo, vi mostrerò come portare i vostri mixin Sass al nuovo livello ultra-leggero del CSS in output.

Un po’ di background su Sass#section1

Sass funge da livello intermedio tra il foglio di stile scritto a mano e il file .css generato che viene inviato al browser, ed aggiunge molte feature grandiose che contribuiscono a rendere la scrittura e la manutenzione del CSS più semplice. Una delle cose che Sass permette di fare agli autori di CSS è utilizzare il paradigma DRY, rendendolo più semplice da mantenere. DRY, o “don’t repeat yourself”, è un principio di programmazione, coniato in The Pragmatic Programmer, che stabilisce che:

Ogni pezzo di conoscenza deve avere una rappresentazione singola, non ambigua e autorevole all’interno di un sistema.

CSS non è molto bravo in fatto di DRY: per necessità, si duplicano sempre degli insiemi di proprietà comuni (pensiamo ai pulsanti) Se doveste creare degli stili per un pulsante che ha tre tipi, gli stili dovrebbero o essere ripetuti per ogni tipo di pulsante oppure le proprietà che creano quel pulsante dovrebbero essere suddivise tra più selettori. Gli autori vengono così a trovarsi tra l’incudine e il martello: riscrivere le proprietà per ogni tipo significa fare molto “copia e incolla” di regole CSS (facendo così aumentare la dimensione del file), introdurre molteplici punti di malfunzionamento, verrà a mancare una sorgente univoca di verità e alla fine si avrà un componente molto fragile. D’altro canto, suddividere le proprietà in più selettori significa che non c’è un’unica ed autorevole rappresentazione di ciascun tipo di pulsante nel sistema ma, al contrario, ognuno sarà suddiviso tra due o più selettori. Questo aggiunge fragilità ai componenti, dal momento che adesso sono definiti in maniera ambigua.

Idealmente, quello che vorremmo è un modo per definire gli stili principali in un unico posto (senza duplicazioni) e un singolo selettore con il quale si possano applicare gli stili.

Perché DRY?#section2

Perché crearci anche questo problema? In breve, perché un CSS scritto secondo il principio DRY può migliorare la performance del sito. Quando si progetta un sito, la performance è importante, dai formati delle immagini che scegliamo al modo in cui scriviamo i nostri selettori CSS. Questo è vero, in particolare, quando si parla di mobile, dove cose basilari per il web, come ad esempio una richiesta HTTP , possono porre dei problemi di performance. L’assunzione, per molto tempo ritenuta vera, che la dimensione del file CSS non incidesse sulla performance web su larga scala non vale più quando gli utenti mobile si trovano di fronte a meno di 100MB di shared website cache. È importante ogni piccolo bit che può essere tolto dalla cache.

L’obiettivo, quindi, è quello di creare dei selettori che si possano mantenere nel nostro Sass e nel nostro HTML, la cui rappresentazione CSS sia la più piccola possibile per ridurne la dimensione nella cache.

Mixin ed extend: due mezze soluzioni#section3

I mixin di Sass forniscono la risposta a uno di questi problemi: permettono agli autori di fogli di stile di avere un unico posto in cui definire e referenziare gli stili principali. I mixin possono anche avere degli argomenti, che permettono dei piccoli cambiamenti tra una chiamata al mixin e l’altra rendendo possibile la creazione di diversi tipi dello stesso pattern. C’è comunque un problema se si utilizzano solo i mixin: se gliene si dà l’occasione, i mixin scriveranno le loro proprietà ogni volta che verranno chiamati, intasando il CSS finale. I mixin risolvono solo la parte della singola e autorevole rappresentazione del principio DRY applicato alle dichiarazioni Sass, ma spesso lasceranno delle proprietà duplicate nel CSS in output.

Sass introduce un altro concetto che ci aiuta ad applicare DRY al nostro CSS: gli extend. Usati tramite la direttiva @extend, gli extend permetto all’autore di un foglio di stile di dire “Voglio che il selettore A abbia gli stessi stili del selettore B”. Sass quindi separa il selettore A dal selettore B mediante una virgola, permettendo ad essi di condividere le proprietà del selettore B e di scrivere le rimanenti proprietà come al solito. A differenza dei mixin, agli extend non si possono assegnare argomenti. È una situazione del tipo “o tutto o niente”.

Sass#section4

.couch {
	padding: 2em;
	height: 37in;
	width: 88in;
	z-index: 40;  
}
  
.couch-leather {
	@extend .couch;
	background: saddlebrown;
}

.couch-fabric {
	@extend .couch;
	background: linen;
}

CSS#section5

.couch, 
.couch-leather, 
.couch-fabric {
	padding: 2em;
	height: 37in;
	width: 88in;
	z-index: 40;
}

.couch-leather {
	background: saddlebrown;
}

.couch-fabric {
	background: linen;
}

Gli extend risolvono il problema delle proprietà duplicate e quello del singolo selettore nel CSS in uscita, ma gli autori dello stylesheet devono ancora mantenere due insiemi separati di stili nel proprio Sass e devono ricordarsi quali proprietà devono essere aggiunte a ciascun tipo di componente, come se avessero scritto due selettori per cominciare.

Sia i mixin sia gli extend risolvono ciascuno metà del problema. Combinando i mixin e gli extend con un po’ di architettura creativa e alcune feature interessanti di Sass, si può creare un vero mixin DRY che combina entrambe le metà in una rappresentazione singola, non ambigua e autorevole. sia nel modo in cui viene usato e mantenuto un pattern in Sass, sia nel modo in cui vengono applicati e rappresentati gli stili di un componente nel CSS finale.

Gli elementi costitutivi di DRY#section6

Ci sono quattro caratteristiche che costituiscono le fondamenta della creazione di mixin DRY: i selettori segnaposto (placeholder), tipi di dati map, la direttiva @at-root e la funzione unique-id().

Selettori segnaposto#section7

I selettori segnaposto sono un tipo unico di selettore da utilizzare con la direttiva @extend di Sass. Si scrivono come i selettori di class, ma cominciano con un % invece che con un .. Si comportano esattamente come un extend normale ma non verranno stampati nel foglio di stile a meno che non vengano estesi. Proprio come gli extend normali, il selettore viene messo nel foglio di stile nel punto in cui è dichiarato il segnaposto.

Sass#section8

%foo {
	color: red;
}

.bar {
	@extend %foo;
	background: blue;
}

.baz {
	@extend %foo;
	background: yellow;
}

CSS#section9

.bar, 
.baz {
	color: red;
}

.bar {
	background: blue;
}

.baz {
	background: yellow;
}

Maps#section10

Le maps sono dei tipi di dati (come i numeri, le stringhe e le liste) di in Sass 3.3 che si comportano in maniera simile agli oggetti in JavaScript. Sono composti da coppie di chiave/valore, in cui le chiavi e i valori possono essere un qualunque tipo di dato di Sass (incluse le mappe stesse). Le chiavi sono sempre univoche e possono essere richiamate per nome, rendendole ideali per la memorizzazione e il recupero univoci.

Sass#section11

$properties: (
	background: red,
	color: blue,
	font-size: 1em,
	font-family: (Helvetica, arial, sans-serif)
);

.foo {
	color: map-get($properties, color);
}

At-root#section12

La direttiva @at-root, novità di Sass 3.3, mette delle definizioni riservate alla radice del foglio di stile, indipendentemente dall’annidamento attuale.

Unique ID#section13

La funzione unique-ide() in Sass 3.3 restituisce un identificatore CSS garantito come univoco nel run attuale di Sass.

Creare un mixin di base#section14

Convertire un pattern in un mixin richiede l’osservazione degli stili che lo compongono e la determinazione di cosa viene condiviso e cosa proviene dall’input dell’utente. Per i nostri scopi, utilizziamo come esempio un pulsante molto semplice:

Sass#section15

.button {
	background-color: #b4d455;
	border: 1px solid mix(black, #b4d455, 25%);
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
		background-color: mix(black, #b4d455, 15%);
		border-color: mix(black, #b4d455, 40%);
	}
}

Per farlo diventare un mixin, scegliamo quali proprietà sono controllate dall’utente (dinamiche) e quali non lo sono (statiche). Le proprietà dinamiche saranno controllate dagli argomenti passati al mixin, mentre le proprietà statiche verranno semplicemente scritte. Vogliamo che solo il colore sia dinamico per il nostro pulsante. Poi possiamo chiamare il mixin con il nostro argomento e il CSS verrà stampato come ci aspettiamo:

Sass#section16

@mixin button($color) {
	background-color: $color;
	border: 1px solid mix(black, $color, 25%);
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
		background-color: mix(black, $color, 15%);
		border-color: mix(black, $color, 40%);
	}
}

.button {
	@include button(#b4d455);
}

Funziona bene, ma produce molte proprietà duplicate. Supponiamo di voler creare una nuova variante di colore per il nostro pulsante. Il nostro Sass (senza includere dal definizione del mixin) e il CSS in output saranno così:

Sass#section17

.button-badass {
	@include button(#b4d455);
}

.button-coffee {
	@include button(#c0ffee);
}

CSS#section18

.button-badass {
	background-color: #b4d455;
	border: 1px solid #879f3f;
	border-radius: 5px;
	padding: .25em .5em;
}
.button-badass:hover {
	cursor: pointer;
	background-color: #99b448;
	border-color: #6c7f33;
}

.button-coffee {
	background-color: #c0ffee;
	border: 1px solid #90bfb2;
	border-radius: 5px;
	padding: .25em .5em;
}
.button-coffee:hover {
	cursor: pointer;
	background-color: #a3d8ca;
	border-color: #73998e;
}

Qui troviamo molte proprietà duplicate, che creano intasamento nel nostro CSS in output. Non vogliamo che succeda questo, per cui utilizziamo in maniera creativa i selettori segnaposto.

Applicare DRY a un mixin#section19

Applicare DRY a un mixin significa semplicemente suddividerlo in parti statiche e dinamiche. Il mixin dinamico è quello che verrà chiamato dall’utente, quello statico conterrà solo i pezzi che altrimenti sarebbero duplicati.

Sass#section20

@mixin button($color) {
		@include button-static;

	background-color: $color;
	border-color: mix(black, $color, 25%);
  
	&:hover {
		background-color: mix(black, $color, 15%);
		border-color: mix(black, $color, 40%);
	}
}

@mixin button-static {
	border: 1px solid;
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
	}
}

Adesso che il nostro mixin è suddiviso in due parti, vogliamo estendere gli item in button-static per impedirne la duplicazione. Potremmo farlo usando un selettore segnaposto invece di un mixin, ma questo significa che il selettore verrebbe spostato nel nostro foglio di stile. Al contrario, vogliamo creare lì un placeholder in maniera dinamica, così che venga creato la prima volta in cui serve il selettore e mantenga l’ordine sorgente come ci aspettiamo. Per fare questo, il nostro primo passo consiste nel creare una variabile globale che trattenga i nomi dei nostri selettori dinamici.

Sass#section21

$Placeholder-Selectors: ();

Poi, in button-static, controlliamo per vedere se esiste una chiave per il nostro selettore. Chiameremo questa chiave “button” per ora. Usando la funzione map-get, o otterremo indietro il valore della nostra chiave o riceveremo null se la chiave non esiste. In quest’ultimo caso, la imposteremo al valore di uno unique ID usando map-merge. Usiamo la flag !global dal momento che vogliamo scrivere su una variabile globale.

Sass#section22

$Placeholder-Selectors: ();
// ...
@mixin button-static {
	$button-selector: map-get($Placeholder-Selectors, 'button');
	
	@if $button-selector == null {
		$button-selector: unique-id();
		$Placeholder-Selectors: map-merge($Placeholder-Selectors, 
          ('button': $button-selector)) !global;
	}
	
	border: 1px solid;
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
	}
}

Una volta che abbiamo determinato se esiste già un ID per il nostro segnaposto, dobbiamo creare il nostro placeholder. Lo facciamo con la direttiva @at-root e con l’interpolation #{} per creare un selettore segnaposto alla root della nostra directory con il nome del nostro unique ID. Il contenuto di quel selettore segnaposto sarà una chiamata al nostro mixin statico (mixin ricorsivo, o cielo!). Poi estendiamo quello stesso selettore segnaposto, attivandolo e scrivendo le proprietà nel nostro CSS.

Usando un selettore segnaposto qui invece di estendere un intero selettore come una class, questo contenuto sarà incluso solo se il selettore verrà esteso, riducendo pertanto il CSS in output. Usando un’estensione invece di scrivere le proprietà, evitiamo anche proprietà duplicate. Questo, a sua volta, riduce la fragilità del nostro CSS in uscita: ogni volta che viene chiamato questo mixin, queste proprietà condivise sono in realtà condivise nel CSS in uscita invece di essere legate in maniera grossolana attraverso gli step di preprocessing del CSS.

Sass#section23

$Placeholder-Selectors: ();
// ...
@mixin button-static {
	$button-selector: map-get($Placeholder-Selectors, 'button');
	@if $button-selector == null {
		$button-selector: unique-id();
		$Placeholder-Selectors: map-merge($Placeholder-Selectors, ('button': $button-selector)) !global;
	  
		@at-root %#{$button-selector} {
			@include button-static;
		}
	}
	@extend %#{$button-selector};   
	
	
	border: 1px solid;
	border-radius: 5px;
	padding: .25em .5em;
	
	&:hover {
		cursor: pointer;
	}
}

Ancora un attimo di pazienza, perché non abbiamo del tutto finito. Se ci fermassimo qui, otterremmo ancora dell’output duplicato, che è qualcosa che non vogliamo (e otterremo un selettore che estende sé stesso, anche questa è una cosa che non vogliamo). Per prevenire ciò, aggiungiamo un argomento a button-static per determinare se passare attraverso il processo di estensione o meno. Aggiungeremo anche questo al nostro mixin dinamico e lo passeremo al nostro mixin statico. Alla fine, avremo i seguenti mixin:

Sass#section24

$Placeholder-Selectors: ();

@mixin button($color, $extend: true) {
	@include button-static($extend);
	
	background-color: $color;
	border-color: mix(black, $color, 25%);
	
	&:hover {
		background-color: mix(black, $color, 15%);
		border-color: mix(black, $color, 40%);
	}
}

@mixin button-static($extend: true) {
	$button-selector: map-get($Placeholder-Selectors, 'button');
	
	@if $extend == true {
		@if $button-selector == null {
			$button-selector: unique-id();
			$Placeholder-Selectors: map-merge($Placeholder-Selectors, ('button': $button-selector)) !global;
			
			@at-root %#{$button-selector} {
				@include button-static(false);
			}
		}
		@extend %#{$button-selector};
		}
		@else {
		border: 1px solid;
		border-radius: 5px;
		padding: .25em .5em;
		
		&:hover {
			cursor: pointer;
		}
	}
}

Dopo tutti questi sforzi, abbiamo creato un modo per mantenere facilmente i nostri stili in Sass, fornire un singolo selettore in HTML e tenere al minimo la dimensione totale del CSS. Non importa quante volte includiamo il mixin button, non duplicheremo mai le nostre proprietà statiche.

La prima volta che usiamo il nostro mixin, gli stili verranno creati nel CSS nel punto in cui è stato chiamato il mixin, preservando il “cascading” come lo volevamo e riducendo la fragilità. Inoltre, dal momento che permettiamo più chiamate allo stesso mixin, possiamo facilmente creare e mantenere delle variazioni sia in Sass sia in HTML.

Con queste aggiunte, le chiamate al mixin del nostro esempio originale adesso producono il seguente CSS:

Sass#section25

.button-badass {
	@include button(#b4d455);
}

.button-coffee {
	@include button(#c0ffee);
}

.button-decaff {
	@include button(#decaff);
}

CSS#section26

.button-badass {
	background-color: #b4d455;
	border-color: #879f3f;
}
.button-badass, 
.button-coffee, 
.button-decaff {
	border: 1px solid;
	border-radius: 5px;
	padding: .25em .5em;
}
.button-badass:hover, 
.button-coffee:hover, 
.button-decaff:hover {
	cursor: pointer;
}
.button-badass:hover {
	background-color: #99b448;
	border-color: #6c7f33;
}

.button-coffee {
	background-color: #c0ffee;
	border-color: #90bfb2;
}
.button-coffee:hover {
	background-color: #a3d8ca;
	border-color: #73998e;
}

.button-decaff {
	background-color: #decaff;
	border-color: #a697bf;
}
.button-decaff:hover {
	background-color: #bcabd8;
	border-color: #857999;
}

Le nostre proprietà statiche vengono separate da virgole nei punti in cui sono state definite, rendendo più semplice il debugging, preservando l’ordine della sorgente e riducendo la dimensione del file CSS in output. Inoltre, solo le proprietà che cambiano avranno un nuovo selettore. Forte, un mixin DRY!

Spingersi oltre#section27

Ora, riscrivere più e più volte lo stesso pattern per ogni mixin non è per nulla DRY, anzi è piuttosto WET (“write everything twice” – i programmatori sono dei mattacchioni). Non vogliamo ovviamente fare così. Invece, pensiamo alla creazione di un mixin per la generazione di segnaposto, così si possono richiamare quelli. Oppure, se usate l’estensione Toolkit di Sass (sia via Bower sia come estensione di Compass), il mixin dynamic-extend può essere usato per trovare, creare ed estendere un placeholder dinamico. Passategli semplicemente un nome come stringa da cercare, come “button”.

Sass#section28

@import "toolkit";

@mixin button($color, $extend: true) {
	@include button-static($extend);
	
	background-color: $color;
	border-color: mix(black, $color, 25%);
	
	&:hover {
		background-color: mix(black, $color, 15%);
		border-color: mix(black, $color, 40%);
	}
}

@mixin button-static($extend: true) {
	$button-selector: map-get($Placeholder-Selectors, 'button');
	
	@if $extend == true {
		@include dynamic-extend('button') {
			@include button-static(false);
		}
	}
	@else {
		border: 1px solid;
		border-radius: 5px;
		padding: .25em .5em;

		&:hover {
			cursor: pointer;
		}
	}
}

Con questo, potete ridurre il vostro pattern mixin DRY, riducendo così i files Sass di input così come i files CSS in output e assicurandovi il titolo di meta-programmatore in gran forma.

Illustrazioni: {carlok}

Sull’autore

Sam Richard

Sam Richard, meglio noto come Snugug su Internet, è un developer tendente al designer a con una passione per la creazione di tool open source di supporto in entrambe i campi. È autore di North, chair di SassConf e un esperto conoscitore del bacon.

Nessun commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Altro da ALA

Webwaste

In questo estratto da World Wide Waste, Gerry McGovern esamina l'impatto ambientale di siti web pieni zeppi di asset inutili. Digital is physical. Sembra economico e gratis ma non lo è: ci costa la Terra.
Industry