Compatibilidad con CSS-in-JS en Herramientas para desarrolladores

Alex Rudenko
Alex Rudenko

En este artículo, se habla de la compatibilidad con CSS en JS en DevTools que se implementó desde Chrome 85 y, en general, qué entendemos por CSS en JS y en qué se diferencia del CSS normal que DevTools admite desde hace mucho tiempo.

¿Qué es CSS en JS?

La definición de CSS en JS es bastante vaga. En un sentido amplio, es un enfoque para administrar el código CSS con JavaScript. Por ejemplo, podría significar que el contenido CSS se define con JavaScript y que la app genera el resultado CSS final sobre la marcha.

En el contexto de DevTools, CSS en JS significa que el contenido de CSS se inserta en la página con las APIs de CSSOM. El CSS normal se inserta con elementos <style> o <link> y tiene una fuente estática (p.ej., un nodo DOM o un recurso de red). Por el contrario, CSS en JS a menudo no tiene una fuente estática. Un caso especial aquí es que el contenido de un elemento <style> se puede actualizar con la API de CSSOM, lo que hace que la fuente deje de estar sincronizada con la hoja de estilo CSS real.

Si usas alguna biblioteca de CSS en JS (p.ej., styled-component, Emotion, JSS), la biblioteca podría insertar estilos con las APIs de CSSOM en función del modo de desarrollo y el navegador.

Veamos algunos ejemplos sobre cómo puedes insertar una hoja de estilo usando la API de CSSOM de manera similar a como lo hacen las bibliotecas CSS-in-JS.

// Insert new rule to an existing CSS stylesheet
const element = document.querySelector('style');
const stylesheet = element.sheet;
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

También puedes crear un nuevo diseño de página:

// Create a completely new stylesheet
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

// Apply constructed stylesheet to the document
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

Compatibilidad con CSS en DevTools

En DevTools, la función más utilizada cuando se trabaja con CSS es el panel Styles. En el panel Styles, puedes ver qué reglas se aplican a un elemento en particular, editarlas y ver los cambios en la página en tiempo real.

Antes del año pasado, la compatibilidad con las reglas de CSS modificadas con las APIs de CSSOM era bastante limitada: solo podías ver las reglas aplicadas, pero no editarlas. El objetivo principal que teníamos el año pasado era permitir la edición de reglas de CSS en JS con el panel de estilos. En ocasiones, también llamamos a los estilos de CSS en JS "constructed" para indicar que se construyeron con APIs web.

Veamos en detalle los trabajos de edición de Styles en Herramientas para desarrolladores.

Mecanismo de edición de estilo en DevTools

Mecanismo de edición de estilo en DevTools

Cuando seleccionas un elemento en DevTools, se muestra el panel Styles. El panel Styles emite un comando de CDP llamado CSS.getMatchedStylesForNode para obtener las reglas de CSS que se aplican al elemento. CDP significa Protocolo de herramientas para desarrolladores de Chrome y es una API que permite que el frontend de DevTools obtenga información adicional sobre la página inspeccionada.

Cuando se invoca, CSS.getMatchedStylesForNode identifica todos los diseños de página del documento y los analiza con el analizador de CSS del navegador. Luego, crea un índice que asocia cada regla de CSS con una posición en la fuente de la hoja de estilo.

Te preguntarás por qué es necesario analizar el CSS de nuevo. El problema aquí es que, por motivos de rendimiento, al navegador no le interesan las posiciones de origen de las reglas de CSS y, por lo tanto, no las almacena. Sin embargo, DevTools necesita las posiciones de origen para admitir la edición de CSS. No queremos que los usuarios normales de Chrome paguen la penalización de rendimiento, pero sí queremos que los usuarios de Herramientas para desarrolladores tengan acceso a las posiciones de origen. Este enfoque de análisis nuevamente aborda ambos casos de uso con desventajas mínimas.

A continuación, la implementación de CSS.getMatchedStylesForNode le solicita al motor de estilo del navegador que proporcione reglas de CSS que coincidan con el elemento determinado. Por último, el método asocia las reglas que muestra el motor de diseño con el código fuente y proporciona una respuesta estructurada sobre las reglas de CSS para que DevTools sepa qué parte de la regla es el selector o las propiedades. Permite que las Herramientas para desarrolladores editen el selector y las propiedades de forma independiente.

Ahora, veamos la edición. ¿Recuerdas que CSS.getMatchedStylesForNode devuelve las posiciones de origen para cada regla? Eso es fundamental para la edición. Cuando cambias una regla, DevTools emite otro comando de CDP que realmente actualiza la página. El comando incluye la posición original del fragmento de la regla que se actualiza y el texto nuevo con el que se debe actualizar el fragmento.

En el backend, cuando se controla la llamada de edición, DevTools actualiza el diseño de la hoja de estilo de destino. También actualiza la copia de la fuente de la hoja de estilo que mantiene y actualiza las posiciones de la fuente para la regla actualizada. En respuesta a la llamada de edición, el frontend de DevTools recupera las posiciones actualizadas del fragmento de texto que se acaba de actualizar.

Esto explica por qué la edición de CSS en JS en DevTools no funcionó de forma predeterminada: CSS en JS no tiene una fuente real almacenada en ningún lugar y las reglas de CSS se encuentran en la memoria del navegador en estructuras de datos de CSSOM.

Cómo agregamos la compatibilidad con CSS en JS

Por lo tanto, para admitir la edición de reglas de CSS en JS, decidimos que la mejor solución sería crear una fuente para los diseños de página creados que se puedan editar con el mecanismo existente que se describió anteriormente.

El primer paso es compilar el texto fuente. El motor de diseño del navegador almacena las reglas de CSS en la clase CSSStyleSheet. Esa clase es la que puedes crear instancias desde JavaScript, como se mencionó anteriormente. El código para compilar el texto fuente es el siguiente:

String InspectorStyleSheet::CollectStyleSheetRules() {
  StringBuilder builder;
  for (unsigned i = 0; i < page_style_sheet_->length(); i++) {
    builder.Append(page_style_sheet_->item(i)->cssText());
    builder.Append('\n');
  }
  return builder.ToString();
}

Itera sobre las reglas que se encuentran en una instancia de CSSStyleSheet y crea una sola cadena a partir de ella. Este método se invoca cuando se crea una instancia de la clase InspectorStyleSheet. La clase InspectorStyleSheet une una instancia de CSSStyleSheet y extrae metadatos adicionales que requiere DevTools:

void InspectorStyleSheet::UpdateText() {
  String text;
  bool success = InspectorStyleSheetText(&text);
  if (!success)
    success = InlineStyleSheetText(&text);
  if (!success)
    success = ResourceStyleSheetText(&text);
  if (!success)
    success = CSSOMStyleSheetText(&text);
  if (success)
    InnerSetText(text, false);
}

En este fragmento, vemos CSSOMStyleSheetText que llama a CollectStyleSheetRules de forma interna. Se invoca CSSOMStyleSheetText si el diseño de la página no está intercalado o no es un diseño de la página de recursos. Básicamente, estos dos fragmentos ya permiten la edición básica de los diseños de página que se crean con el constructor new CSSStyleSheet().

Un caso especial son las hojas de estilo asociadas con una etiqueta <style> que se mutaron con la API de CSSOM. En este caso, la hoja de estilo contiene el texto fuente y reglas adicionales que no están presentes en la fuente. Para controlar este caso, presentamos un método para combinar esas reglas adicionales en el texto fuente. Aquí, el orden es importante porque las reglas de CSS se pueden insertar en medio del texto fuente original. Por ejemplo, imagina que el elemento <style> original contenía el siguiente texto:

/* comment */
.rule1 {}
.rule3 {}

Luego, la página insertó algunas reglas nuevas con la API de JS, lo que produjo el siguiente orden de reglas: .rule0, .rule1, .rule2, .rule3, .rule4. El texto de origen resultante después de la operación de combinación debería ser el siguiente:

.rule0 {}
/* comment */
.rule1 {}
.rule2 {}
.rule3 {}
.rule4 {}

La preservación de los comentarios y la sangría originales es importante para el proceso de edición, ya que las posiciones del texto fuente de las reglas deben ser precisas.

Otro aspecto especial de los editores de estilo CSS en JS es que la página puede cambiarlos en cualquier momento. Si las reglas reales de CSSOM se desincronizaran con la versión de texto, la edición no funcionaría. Para ello, presentamos una llamada sonda, que permite que el navegador notifique a la parte del backend de DevTools cuando se está mutando una hoja de estilo. Luego, los diseños de página mutados se sincronizan durante la próxima llamada a CSS.getMatchedStylesForNode.

Con todas estas piezas en su lugar, la edición de CSS en JS ya funciona, pero queríamos mejorar la IU para indicar si se construyó una hoja de estilo. Agregamos un nuevo atributo llamado isConstructed a CSS.CSSStyleSheetHeader de CDP que el frontend usa para mostrar correctamente la fuente de una regla de CSS:

Hoja de estilo construible

Conclusiones

Para recapitular, analizamos los casos de uso relevantes relacionados con CSS en JS que DevTools no admitía y explicamos la solución para admitir esos casos de uso. La parte interesante de esta implementación es que pudimos aprovechar la funcionalidad existente haciendo que las reglas de CSS del CSSOM tengan un texto de origen normal, lo que evita la necesidad de volver a crear la arquitectura de edición de estilos en DevTools.

Para obtener más información, consulta nuestra propuesta de diseño o el error de seguimiento de Chromium, que hace referencia a todos los parches relacionados.

Descarga los canales de vista previa

Considera usar Chrome Canary, Dev o Beta como navegadores de desarrollo predeterminados. Estos canales de versión preliminar te brindan acceso a las funciones más recientes de DevTools, te permiten probar las APIs de plataformas web de vanguardia y te ayudan a encontrar problemas en tu sitio antes que tus usuarios.

Comunícate con el equipo de Chrome DevTools

Usa las siguientes opciones para hablar sobre las funciones nuevas, las actualizaciones o cualquier otro tema relacionado con DevTools.