Scss migratie header
Blog

De toekomst van SCSS: Migreren naar @use en @forward

SCSS blijft zich ontwikkelen en biedt nieuwe mogelijkheden voor structuur en onderhoudbaarheid. Een belangrijke ontwikkeling is de uitfasering van @import. Binnen enkele jaren zullen alle @import's vervangen moeten zijn door @use en @forward. Wij zetten alvast de stap! Jij ook?


Al vanaf het moment dat SCSS bestaat wordt alles in SCSS bestanden in de globale namespace gezet door @import te gebruiken. Dit betekent dat variabelen, mixins en functies overal beschikbaar zijn. Dat lijkt heel erg fijn en gemakkelijk, maar kan ook leiden tot onverwachte fouten en verminderde herbruikbaarheid. Om dit probleem op te lossen, hebben de makers van SCSS @use en @forward geïntroduceerd, waarmee je een betere structuur kunt aanbrengen en niet alles zomaar in de globale namespace wordt gezet. Bovendien zal @import binnen enkele jaren worden behandeld als een standaard CSS-@import, zonder SCSS-functionaliteiten, waardoor migratie naar @use en @forward op den duur essentieel wordt.

Wat is het verschil tussen @import, @use en @forward?

Met @import kun je je SCSS bestanden opsplitsen in meerdere bestanden. Door deze bestanden met @import te laden is alle (S)CSS binnen deze bestanden beschikbaar. In feite bouw je met @import één groot bestand waarin alles onder elkaar gezet wordt. Hierdoor is alles globaal beschikbaar; behalve als je een variabele bijvoorbeeld binnen een selector zet zoals:

.button {
  $color: green;
  color: $green;
}

Op die manier is de $green variabele alleen beschikbaar binnen .button. De volgorde van het inladen is dus wel belangrijk. Een variabele, mixin of functie is pas beschikbaar nadat deze is ingeladen.

Hierdoor hebben veel ontwikkelaars de gewoonte ontwikkeld om vanuit 1 hoofdbestand te werken waar als eerste de variabelen, mixins en functies worden ingeladen. Op deze manier zijn al deze tools beschikbaar in de globale namespace en kun je die door heel je project heen gebruiken.

Met @use en @forward wordt niet alles meer automatisch in de globale namespace geladen. Hierdoor kun je variabelen en mixins beter structureren en voorkomen dat namen elkaar overlappen. Dit is vooral handig in grotere projecten of frameworks.

Voorbeeld met @import (oude aanpak):

style.scss

@import "tools/variables";
@import "molecules/button";

tools/_variables.scss

$button-color: green !default;

molecules/_button.scss

.button {
  color: $button-color;
}

Voorbeeld met @use en @forward (nieuwe aanpak):

style.scss

@forward "tools/variables";
@forward "molecules/button";

tools/_variables.scss

$button-color: green !default;

molecules/_button.scss

@use "../tools";
.button {
  color: tools.$button-color;
}

Zoals je ziet, is dit vooral extra code. We voegen een @use toe en passen de variabele namen aan zodat de juiste namespace gebruikt wordt. Maar in grote bestaande projecten; waar je als ontwikkelaar al rekening hebt gehouden met unieke namen voor variabelen, mixins of functies; of waar je zelfs actief gebruik maakt van het overschrijven hiervan op bepaalde plekken binnen je project, kan het erg vervelend zijn om dit allemaal aan te passen. Gelukkig kun je met @use er ook voor zorgen dat alles in de globale namespace komt door het volgende te doen:

@use "../tools" as *;
.button {
  color: $button-color;
}

Wat doet @use?

@use laadt een SCSS-bestand en maakt de inhoud beschikbaar binnen de lokale namespace (standaard de naam van het geladen bestand, tenzij anders aangegeven). Zo voorkom je conflicten en kun je met dezelfde variabele in verschillende bestanden werken.

Best practice: gebruik een index-bestand

Het is handig om een _index.scss-bestand te maken dat alle benodigde bestanden laadt:

tools/_index.scss

@forward "variables";
@forward "mixins";
@forward "functions";

Hierdoor kun je alles in één keer importeren met:

@use "tools";

Wat doet @forward?

@forward stuurt de inhoud van het bestand door naar andere bestanden. Dit is handig voor het centraliseren van SCSS-inhoud.

Voorbeeld:

tools/_variables.scss

$button-color: green !default;

tools/_index.scss

@forward "variables";

style.scss

Doordat in tools.scss de variables geforward worden; is alles binnen deze forward beschikbaar in de tools namespace.

@use "tools" as *;
.button {
  color: $button-color;
}

Migreren van @import naar @use en @forward

Het migreren is helaas niet zo eenvoudig als alle @import vervangen met @forward of @use. Zo eenvoudig zou het wel kunnen zijn als je helemaal geen gebruik maakt van variabelen, mixins of functies; maar als dat het geval is dan heb je überhaupt geen SCSS nodig ;-)

Maar hoe migreer je dan wel? Ik ga even uit van de volgende structuur:

SCSS map

scss (hoofd folder)
  molecules
    _button.scss
    _card.scss
    _icon.scss
  tools
    functions
      _base.scss
    mixins
      _base.scss
    variables
      _base.scss
      _color.scss
    _tools.scss
  style.scss

Voorbeeld situatie bij gebruik van @import

style.scss

// Tools
@import "tools";
// Molecules
@import "molecules/button";
@import "molecules/card";
@import "molecules/icon";

tools/tools.scss

@import "variables/base";
@import "variables/color";
@import "functions/base";
@import "mixins/base";

Voorbeeld omzetten naar @use en @forward

Als je deze opzet zou willen omzetten dan kun je het volgende doen:

We voegen in de mappen molecules, functions, mixins en variables een _index.scss bestand toe waarin we de benodigde (alle overige) bestanden met @forward inladen. Het bestand tools/_tools.scss vernoemen we naar tools/_index.scss De opzet in de bestanden wordt dan als volgt:

style.scss

// Molecules
@forward "molecules/button";
@forward "molecules/card";
@forward "molecules/icon";

tools/tool.scss

@forward "variables/base";
@forward "variables/functions";
@forward "variables/mixins";

molecules/_button.scss en
molecules/_card.scss en
molecules/_icon.scss starten allemaal met de volgende regel:

@use "../tools" as *;

Je kan ook nog _index.scss bestanden toevoegen aan de overige folders.

Zo zou molecules/_index.scss er bijvoorbeeld uit kunnen zien:

@forward "button";
@forward "card";
@forward "icon";

En zo zou tools/variables/_index.scss er uit kunnen zien:

@forward "base";
@forward "color";

Zoals je ziet hebben we dus altijd minstens 1 @use nodig voor het gebruik van variabelen, mixins of functies. Door deze allemaal samen in 1 tools bestand te zetten kunnen we bestaande projecten redelijk eenvoudig omzetten naar @use en @forward. Je haalt de tools (variables, mixins en functies) uit het hoofdbestand en voegt deze met @use toe aan alle individuele bestanden die gebruik maken van een van deze tools.

In bovenstaand project is dat vrij eenvoudig omdat het hier om een klein voorbeeld project gaat. Als je grotere projecten hebt met ingewikkeldere structuren dan kan het zijn dat de compiler niet wil compilen omdat bestanden meerdere keren worden ingeladen, iets wat geen probleem was bij het gebruik van @import.

Mijn advies is om zoveel mogelijk code proberen te isoleren. Zet het in de globale tools als het globaal beschikbaar moet zijn. Is het niet globaal nodig? Zet het dan in het bestand waar het nodig is.

Als je de codebase 'plat kan slaan' zou ik dat ook zoveel mogelijk doen. De compiler waarschuwt je als je een bestand meerdere keren probeert te laden; maar de oplossing zoeken kan soms vrij complex zijn.

Migratie-uitdaging: Illusion

Binnen onze projecten maken wij veelvuldig gebruik van het Illusion framework. Illusion geeft ons een aantal krachtige tools waarmee we sneller en efficiënter kunnen werken; maar waar ook de gecompileerde CSS beter van wordt. Het voegt ook een aantal basis CSS-regels toe waardoor we die als ontwikkelaars niet kunnen vergeten en de gebruikerservaring voor iedereen beter wordt.

Bij het gebruik van Illusion; in ieder geval op de manier waarop wij het inzetten; kwam wel een uitdaging om de hoek kijken. Wanneer je Illusion gebruikt dan doet dat in principe nog helemaal niets. Je hebt een aantal variabelen, mixins en functies tot je beschikking. Vooral die mixins en functies zijn erg krachtige tools. Maar door bepaalde variabelen te overschrijven kun je ervoor zorgen dat Illusion wel CSS compiled. Zo zitten er bepaalde styling regels in voor standaard elementen die voortborduren op normalize CSS genoemd extendalize. Als je deze variable op 'true' zet dan zullen er een aantal regels worden gecompileerd.

De uitdaging zat hem vooral in het feit dat we naast het hoofd CSS-bestand ook diverse 'thema' bestanden compilen. Dus naast een hoofdbestand style.css hebben we bijvoorbeeld ook thema1.css of componentX.scss. Deze bestanden kunnen we dan alleen inladen op de pagina's waar die bestanden nodig zijn. Binnen deze bestanden gebruiken we ook graag Illusion. Ook is het superhandig om alle overschreven Illusion variabelen hiervoor te gebruiken. Ook de aangemaakte custom mixins en functies zijn handig. Kortom; we willen de tools van het hoofdproject inladen. Maar als we dat zomaar doen dan worden binnen deze thema bestanden ook o.a. alle extendalize CSS gecompileerd met als resultaat dubbele CSS-code die nergens voor nodig is.

Onze oplossing met @import was altijd het opslitsen van de Illusion variabelen overrides in twee bestanden namelijk illusion-settings.scss en illusion-includes.scss . Het includes bestand zag er dan bijvoorbeeld zo uit:

$illusion-extendalize: true !default;
$illusion-form: true !default;
$illusion-custom-select: true !default;
$illusion-multiple-choice: true !default;

En style.scss bevatte dan bijvoorbeeld:

@import "tools/variables/illusion-includes";
@import "tools/tools";

Terwijl tools.css het volgende bevatte:

@import "tools/variables/illusion-variables;
// Hier wordt Illusion daadwerkelijk geladen
@import "../../../../../../node_modules/illusion/scss/illusion";

Binnen een thema konden we dan gemakkelijk het volgende doen:

themas/thema1/tools/illusion-includes.scss

$illusion-extendalize: false !default;
$illusion-form: false !default;
$illusion-custom-select: false !default;
$illusion-multiple-choice: false !default;

En themas/thema1/thema1.scss bevatte dan bijvoorbeeld

@import "tools/variables/illusion-includes";
// Hier laden we de tools van het "hoofdbestand"
@import "../../tools/tools";

Op deze manier gebruikte we dezelfde tools, maar werden er andere variables mee gestuurd.

Helaas is dit niet meer mogelijk bij het gebruik van @use. Illusion twee maal gebruiken binnen 1 folder structuur met andere overschreven variabelen geeft een compileerfout. Gelukkig is ook daarvoor een oplossing. Je kunt een with () meegeven aan een @forward waardoor je alsnog andere variabelen kunt meesturen.

Oplossing:

tools/_index.scss

// Settings
$use-extendalize: true !default;
$use-custom-select: true !default;
$use-multiple-choice: true !default;
$use-form: true !default;
// Illusion
@forward "illusion" with (
  $extendalize: $use-extendalize,
  $custom-select: $use-custom-select,
  $multiple-choice: $use-multiple-choice,
  $form: $use-form
);

In alle bestanden waar je Illusion nodig hebt; bijvoorbeeld molecules/button.scss

@use "../tools" as *;

Binnen je thema's kun je in themas/thema1/tools/index.scss het volgende zetten:

// Hiermee laden we de tools uit het hoofdbestand; maar dan met overschreven waarden
@forward "../../../tools" with (
  $use-extendalize: false,
  $use-custom-select: false,
  $use-multiple-choice: false,
  $use-form: false
);

Vervolgens kun je in alle bestanden binnen een thema ook gewoon het volgende doen:

@use "../tools" as *;

Wat we hier effectief doen is vanuit het thema zeggen dat we de variabelen die in /tools/_index.scss staan overschrijven met with (); Aangezien deze variabelen binnen dit bestand een !default flag hebben worden deze overschreven. Als we vervolgens Illusion inladen dan doen we dat met de overschreven waarden vanuit het thema. Het resultaat is alle tools beschikbaar maar geen gecompileerde CSS.

Opletten tijdens migreren

  • @use en @forward regels moeten altijd als eerste worden geschreven. Je kan deze dus niet na een @import plaatsen in hetzelfde bestand.
  • De compiler kan ook waarschuwingen geven over o.a. slashes, maps, meta en lists Je kunt het beste meteen updaten naar de nieuwe sass stijl. Bijvoorbeeld @use "sass:math" met math.div(6, 2) om te delen, @use "sass:map" met map.get(), @use "sass:meta" met meta.type-of(), @use "sass:list" met list.nth() etc.
  • Een tools bestand (waar dus variabelen, mixins en functies in staan en verder niets) hoef je niet in je style.scss bestand te zetten; dit levert alleen maar dubbele include problemen op. @use je tools in de bestanden die gebruik maken van deze tools.

Conclusie

Bovenstaande werkwijze is wellicht niet optimaal voor nieuwe projecten. Bij het opzetten van een nieuw project kan het veel waarde hebben om variabelen, mixins en functies geïsoleerd te hebben van elkaar. Zo kun je een betere structuur aanbrengen zonder dat alles globaal beschikbaar is.

Maar in het echte leven hebben we vaak te maken met reeds opgezette en ingewikkelde projecten. Als het project al goed draait en er geen issues zijn met dubbele variabelen, mixins of functies; dan is het waarschijnlijk eenvoudiger om alles in de globale namespace te zetten.

Zo kunnen alle developers die een bepaalde werkwijze zijn gewend direct met het project verder. De enige aanpassing is de @use "tools" as *; regel bovenaan nieuwe bestanden toevoegen. Die kun je overigens niet vergeten; dat laat de compiler je dan wel weten ;-)