Se está por cumplir un año ya desde el lanzamiento de la versión que quizás introdujo los cambios más significativos en Sass como lenguaje desde la migración de su compilador de Ruby a Dart, y he notado que, sin embargo, este suceso ha pasado bastante desapercibido. Fuera del sitio oficial de Sass, los posteos en internet hablando acerca de esto, así como los videos en YouTube, son escasos por no decir nulos, y ni hablemos de encontrar algo en español. Es por eso que decidí ordenar de la forma más prolija y detallada posible pero sin agobiar al lector, las features más trascendentales que introduce Sass 1.23, y que, según sus desarrolladores, llegaron para quedarse.
Démosle la bienvenida al Module System
Sin lugar a dudas, el salto más importante que ha dado esta versión, y por el cual se la considera una versión bisagra es debido a la deprecación completa (aunque gradual) de la regla @import
para la carga de archivos, cediendo el paso al nuevo Module System, cuya implementación gira en torno a la nueva at-rule @use
.
Previo a esta versión, cuando usábamos @import
, era imposible saber dónde habían sido definidas las funciones, mixins y variables (llamados "miembros" por convención, y así nos referiremos de ahora en adelante) que estábamos invocando dentro de un archivo (a los cuales a partir de ahora es correcto referirse como "namespaces"), ya que el solo hecho de hacer un @import
los volvía disponibles dentro de todos los archivos que fueran llamados a continuación.
Esto era un dolor de cabeza para muchos desarrolladores, y un potencial problema en proyectos grandes donde podía llegar a ocurrir que se definieran dos miembros con el mismo nombre. Además, nos obligaba a prestar particular atención al orden de importación, cuando existían múltiples imports, si queríamos que determinados miembros estuvieran disponibles cuando los necesitábamos.
Con esto, de paso, Sass se despega por completo de la colisión confusa con el @import
de CSS.
Cambios de la versión
Estos son algunos de los pricipales cambios y mejoras que introdujo la versión 1.23:
- Importación de archivos como módulos mediante la regla
@use
. Mejora el encapsulamiento ya que los miembros de éste solo estarán disponibles dentro del archivo que los está usando, a diferencia de@import
que tenía un scope global.
// some-file.scss
@import "some-module"; // ❌ disponible dentro de este archivo
// y de todos los que se importen después
// some-file.scss
@use "some-module"; // ✅ disponible solo dentro de este archivo
- Con la regla
@forward
, se podrá hacer que los miembros de otro archivo estén disponibles cuando el archivo actual sea importado con@use
, creando de esta manera una API extendida. Sin embargo, el archivo que realiza el@forward
no podrá acceder a los miembros de éste, a diferencia de@use
, si no que actuará como un simple intermediario. Es decir que podemos hacer lo siguiente:
// mixins.scss
@mixin expand {
display: block;
width: 100%;
}
// foo.scss
@forward "mixins";
.foo {
align-items: center;
background-color: peru;
display: flex;
flex-direction: column-reverse;
}
// some-other-file.scss
@use "foo";
.expanded {
@include foo.expand;
}
Mientras que esto nos daría un error:
// palette.scss
$colors: (
primary: #007BFF,
secondary: #6C757D,
success: #DC3545
);
// functions.scss
@use "sass:map";
@forward "palette";
@function color($prop) {
@return map.get(palette.$colors, $prop);
// => Error: There is no module
// with the namespace "palette".
}
Es posible, de ser necesario, decidir de manera selectiva qué variables, mixins o funciones de determinado namespace estarán disponibles, de la siguiente manera:
// src/_list.scss
$horizontal-list-gap: 2em;
@mixin list-reset {
margin: 0;
padding: 0;
list-style: none;
}
@mixin list-horizontal {
@include reset;
li {
display: inline-block;
margin: {
left: -2px;
right: $horizontal-list-gap;
}
}
}
// bootstrap.scss
@forward "src/list" hide list-reset, $horizontal-list-gap;
...o de forma inversa con show
:
@forward "src/list" show list-horizontal;
Y también algo que es muy interesante es que podemos agregar un prefijo para todos los miembros del namespace:
// src/_list.scss
@mixin reset {
margin: 0;
padding: 0;
list-style: none;
}
// bootstrap.scss
@forward "src/list" as list-*;
// styles.scss
@use "bootstrap";
li {
@include bootstrap.list-reset;
}
La at-rule
@extend
se limita al scope del archivo actual cuando se utiliza@use
. A diferencia de antes, que al usar@import
el@extend
quedaba disponible globalmente, en ocasiones haciendo difícil predecir qué reglas estábamos extendiendo.Usando los modificadores de acceso
-
ó_
podemos hacer que un miembro sea privado, lo que significa que solo podrá ser utilizado dentro del propio archivo que lo define:$_private-var: value
ó$-private-var: value
.Namespaces personalizados mediante el uso de
as
. Por ejemplo:
@use "functions" as fn;
.button {
background-color: fn.color("success");
}
...o como top-level module (usar con precaución):
@use "functions" as *;
.button {
background-color: color("success");
}
Esta última forma tiene la limitación de que, si usamos otro módulo como "top-level module" y este contiene, también, una función llamada color()
, el compilador arrojará un error.
- También podemos importar un módulo sobreescribiendo el valor de una o más de sus variables. Para ello, primero tenemos que asignarle un valor por defecto al momento de definirla:
// bootstrap.scss
$paragraph-margin-bottom: 1rem !default;
p {
margin-top: 0;
margin-bottom: $paragraph-margin-bottom;
}
Y luego llamar el archivo de esta manera, utilizando la cláusula with
que recibirá un mapa con las variables que queramos pisar:
@use "bootstrap" with (
$paragraph-margin-bottom: 1.2rem
);
Las funciones del core de Sass ahora son accedidas a través de módulos nativos:
sass:color
,sass:list
,sass:map
,sass:math
,sass:meta
,sass:selector
, ysass:string
. Las cargaremos en nuestra hoja de estilos con@use
(por ej:@use "sass:map"
) y las usaremos como cualquier otro módulo, con la sintáxisnamespace.function()
. Esto se hizo para evitar la colisión con funciones nativas de CSS, lo que les permitirá, en el futuro, agregar nuevas de manera segura.Se incorporaron, además, algunos mixins al core, que vienen a solucionar algunas malas prácticas del pasado, como los
@import
anidados, tal es el caso demeta.load-css($url, $with: ())
que sirve para cargar una hoja de estilos de manera dinámica, recibiendo dos parámetros, el primero$url
es un string con la url de dicho CSS, y el segundo$with: ()
es opcional y deberá ser un mapa con la configuración, igual al del ejemplo anterior solo que en este caso las variables no llevan $ adelante. También tenemosmeta.module-variables()
ymeta.module-functions()
para acceder a las variables o funciones respectivamente de un determinado módulo. Están disponibles de la misma manera que las built-in functions:@use "sass:meta"
.
¿Es seguro de usar?
Desde el lanzamiento de esta versión, ya ha pasado casi un año y han salido 3 versiones menores posteriormente (actualmente se encuentra en la 1.26), por lo que es 100% seguro de utilizar en cualquier proyecto nuevo. Sin embargo, hemos de esperar que no nos funcionen las nuevas features en proyectos de más de un año de antigüedad.
Si bien esta versión es completamente retro-compatible, los desarrolladores avisan que @import
será eventualmente removido por completo de Sass, pero que dejarán transcurrir un tiempo prudencial para que tengamos chance de migrar.
Por suerte, Sass pone a nuestra disposición una herramienta de migración automatizada, a través de línea de comandos: sass-migrator. Existen varias formas de instalarla, las cuales se detallan aquí.
Migrando nuestro código
Seguiremos las instrucciones para Sass: Migrator de la página oficial, y una vez que tengamos instalada la CLI, simplemente navegamos a la carpeta de nuestro proyecto y hacemos:
sass-migrator module --migrate-deps <path/to/style.scss>
El flag --migrate-deps
lo que hace es decirle que migre no solo el archivo específico que le estamos indicando si no además todos los archivos que pudiera estar importando.
Podemos pasarle el flag --verbose
para que la consola nos de output sobre los cambios que va realizando sobre los archivos, y el flag --dry-run
si queremos que corra el comando de manera "simulada" sin ejecutar realmente los cambios, lo cual tiene sentido usándolo en combinación con el anterior.
Si bien lo anterior bastará para la mayoría de los casos, hay un último flag que puede llegar a ser muy útil para quienes desarrollen alguna biblioteca: --forward
, que acepta los valores none
(valor por defecto), all
y prefixed
. Si elegimos --forward=all
permitiremos que el usuario de nuestra biblioteca pueda acceder a toda la API (excepto miembros privados) con un solo @use
, ya que lo que hará es un @forward
de todos los archivos. En el caso de prefixed
, básicamente hace un @forward
únicamente de los miembros a los que hayamos puesto un prefijo, el cual debemos explicitar de manera obligatoria con el flag adicional --remove-prefix
; la herramienta eliminará automáticamente este prefijo. Antes de los módulos, se solía agregar estos prefijos para evitar colisiones de nombres. El ejemplo del sitio de Sass, lo grafica a la perfección:
$ cat style.scss
@import "theme";
@mixin app-inverted {
color: $app-bg-color;
background-color: $app-color;
}
$ sass-migrator --migrate-deps module --remove-prefix=app- style.scss
$ cat style.scss
@use "theme";
@mixin inverted {
color: theme.$bg-color;
background-color: theme.$color;
}
Debo decir que he realizado unas cuantas migraciones y, en todos los casos, la herramienta es bastante efectiva e inteligente al realizar su labor. En algúna ocasión he tenido que refactorizar un poco a mano pero han sido cosas mínimas que no me han llevado más de unos minutos. Aun así, es recomendable primero probar siempre con --dry-run --verbose
, para nuestra tranquilidad.
Referencias:
Top comments (2)
No estaba enterado de estas features, muy bueno leer todo y en español, gracias por el post!
Gracias por compartir esta información, había leído lo de @use pero simplemente me pareció una forma más fina de un @import sin más.
Como dices en la introducción esto a pasado por debajo de mesa sin mucha repercusión pero creo que es un problema de "marketing" un cambio así debería haber llevado a una versión 2 por ejemplo, algo que sólo por ver el cambio de versión te pongas a investigar.