El Pipeline de Activos

Esta guía cubre el pipeline de activos.

Después de leer esta guía, sabrás:


1 ¿Qué es el Pipeline de Activos?

El pipeline de activos proporciona un marco para manejar la entrega de activos JavaScript y CSS. Esto se hace aprovechando tecnologías como HTTP/2 y técnicas como concatenación y minificación. Finalmente, permite que tu aplicación se combine automáticamente con activos de otras gemas.

El pipeline de activos está implementado por las gemas importmap-rails, sprockets y sprockets-rails, y está habilitado por defecto. Puedes desactivarlo al crear una nueva aplicación pasando la opción --skip-asset-pipeline.

$ rails new appname --skip-asset-pipeline

NOTA: Esta guía se centra en el pipeline de activos predeterminado utilizando solo sprockets para CSS y importmap-rails para el procesamiento de JavaScript. La principal limitación de estos dos es que no hay soporte para transpilar, por lo que no puedes usar cosas como Babel, TypeScript, formato React JSX o Tailwind CSS. Te animamos a leer la sección de Bibliotecas Alternativas si necesitas transpilar tu JavaScript/CSS.

2 Características Principales

La primera característica del pipeline de activos es insertar una huella digital SHA256 en cada nombre de archivo para que el archivo sea cacheado por el navegador web y el CDN. Esta huella digital se actualiza automáticamente cuando cambias el contenido del archivo, lo que invalida el caché.

La segunda característica del pipeline de activos es usar import maps al servir archivos JavaScript. Esto te permite construir aplicaciones modernas utilizando bibliotecas JavaScript hechas para módulos ES (ESM) sin la necesidad de transpilar y empaquetar. A su vez, esto elimina la necesidad de Webpack, yarn, node o cualquier otra parte de la cadena de herramientas JavaScript.

La tercera característica del pipeline de activos es concatenar todos los archivos CSS en un archivo principal .css, que luego se minifica o comprime. Como aprenderás más adelante en esta guía, puedes personalizar esta estrategia para agrupar archivos de la manera que desees. En producción, Rails inserta una huella digital SHA256 en cada nombre de archivo para que el archivo sea cacheado por el navegador web. Puedes invalidar el caché alterando esta huella digital, lo que ocurre automáticamente cada vez que cambias el contenido del archivo.

La cuarta característica del pipeline de activos es que permite codificar activos a través de un lenguaje de nivel superior para CSS.

2.1 ¿Qué es la Huella Digital y Por Qué Debería Importarme?

La huella digital es una técnica que hace que el nombre de un archivo dependa del contenido del archivo. Cuando el contenido del archivo cambia, el nombre del archivo también cambia. Para contenido estático o que cambia con poca frecuencia, esto proporciona una manera fácil de saber si dos versiones de un archivo son idénticas, incluso a través de diferentes servidores o fechas de implementación.

Cuando un nombre de archivo es único y se basa en su contenido, se pueden establecer encabezados HTTP para alentar a los cachés en todas partes (ya sea en CDNs, ISPs, equipos de red o en navegadores web) a mantener su propia copia del contenido. Cuando el contenido se actualiza, la huella digital cambiará. Esto hará que los clientes remotos soliciten una nueva copia del contenido. Esto se conoce generalmente como cache busting.

La técnica que Sprockets utiliza para la huella digital es insertar un hash del contenido en el nombre, generalmente al final. Por ejemplo, un archivo CSS global.css

global-908e25f4bf641868d8683022a5b62f54.css

Esta es la estrategia adoptada por el pipeline de activos de Rails.

La huella digital está habilitada por defecto tanto para los entornos de desarrollo como de producción. Puedes habilitarla o deshabilitarla en tu configuración a través de la opción config.assets.digest.

2.2 ¿Qué son los Import Maps y Por Qué Debería Importarme?

Los import maps te permiten importar módulos JavaScript utilizando nombres lógicos que se mapean a archivos versionados/digeridos, directamente desde el navegador. Así puedes construir aplicaciones JavaScript modernas utilizando bibliotecas JavaScript hechas para módulos ES (ESM) sin la necesidad de transpilar o empaquetar.

Con este enfoque, enviarás muchos archivos JavaScript pequeños en lugar de un archivo JavaScript grande. Gracias a HTTP/2, eso ya no conlleva una penalización de rendimiento material durante el transporte inicial, y de hecho ofrece beneficios sustanciales a largo plazo debido a mejores dinámicas de caché.

3 Cómo usar Import Maps como Pipeline de Activos de JavaScript

Los Import Maps son el procesador JavaScript predeterminado, la lógica de generación de import maps es manejada por la gema importmap-rails.

ADVERTENCIA: Los import maps se utilizan solo para archivos JavaScript y no se pueden usar para la entrega de CSS. Consulta la sección de Sprockets para aprender sobre CSS.

Puedes encontrar instrucciones detalladas de uso en la página de inicio de la gema, pero es importante entender los conceptos básicos de importmap-rails.

3.1 Cómo funciona

Los import maps son esencialmente una sustitución de cadenas para lo que se conoce como "especificadores de módulo descalzos". Te permiten estandarizar los nombres de las importaciones de módulos JavaScript.

Toma por ejemplo tal definición de importación, no funcionará sin un import map:

import React from "react"

Tendrías que definirlo así para que funcione:

import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"

Aquí es donde entra el import map, definimos el nombre react para que esté fijado en la dirección https://ga.jspm.io/npm:react@17.0.2/index.js. Con tal información, nuestro navegador acepta la definición simplificada import React from "react". Piensa en el import map como un alias para la dirección fuente de la biblioteca.

3.2 Uso

Con importmap-rails creas el archivo de configuración del import map fijando la ruta de la biblioteca a un nombre:

# config/importmap.rb
pin "application"
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"

Todos los import maps configurados deben adjuntarse en el elemento <head> de tu aplicación agregando <%= javascript_importmap_tags %>. El javascript_importmap_tags genera un montón de scripts en el elemento head:

  • JSON con todos los import maps configurados:
<script type="importmap">
{
  "imports": {
    "application": "/assets/application-39f16dc3f3....js"
    "react": "https://ga.jspm.io/npm:react@17.0.2/index.js"
  }
}
</script>
  • Punto de entrada para cargar JavaScript desde app/javascript/application.js:
<script type="module">import "application"</script>

NOTA: Antes de la versión 2.0.0, importmap-rails incluía Es-module-shims en la salida de javascript_importmap_tags como un polyfill para garantizar el soporte de import maps en navegadores antiguos. Sin embargo, con el soporte nativo para import maps en todos los navegadores principales, la versión 2.0.0 ha eliminado el shim incluido. Si deseas soportar navegadores heredados que carecen de soporte para import maps, inserta manualmente Es-module-shims antes de javascript_importmap_tags. Para más información, consulta el README de importmap-rails.

3.3 Usar paquetes npm a través de CDNs de JavaScript

Puedes usar el comando bin/importmap que se agrega como parte de la instalación de importmap-rails para fijar, desfijar o actualizar paquetes npm en tu import map. El binstub utiliza JSPM.org.

Funciona así:

$ bin/importmap pin react react-dom
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/index.js
Pinning "react-dom" to https://ga.jspm.io/npm:react-dom@17.0.2/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js
Pinning "scheduler" to https://ga.jspm.io/npm:scheduler@0.20.2/index.js

bin/importmap json

{
  "imports": {
    "application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
    "react": "https://ga.jspm.io/npm:react@17.0.2/index.js",
    "react-dom": "https://ga.jspm.io/npm:react-dom@17.0.2/index.js",
    "object-assign": "https://ga.jspm.io/npm:object-assign@4.1.1/index.js",
    "scheduler": "https://ga.jspm.io/npm:scheduler@0.20.2/index.js"
  }
}

Como puedes ver, los dos paquetes react y react-dom se resuelven en un total de cuatro dependencias, cuando se resuelven a través del jspm por defecto.

Ahora puedes usar estos en tu punto de entrada application.js como lo harías con cualquier otro módulo:

import React from "react"
import ReactDOM from "react-dom"

También puedes designar una versión específica para fijar:

$ bin/importmap pin react@17.0.1
Pinning "react" to https://ga.jspm.io/npm:react@17.0.1/index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js

O incluso eliminar fijaciones:

$ bin/importmap unpin react
Unpinning "react"
Unpinning "object-assign"

Puedes controlar el entorno del paquete para paquetes con compilaciones separadas de "producción" (el valor predeterminado) y "desarrollo":

$ bin/importmap pin react --env development
Pinning "react" to https://ga.jspm.io/npm:react@17.0.2/dev.index.js
Pinning "object-assign" to https://ga.jspm.io/npm:object-assign@4.1.1/index.js

También puedes elegir un proveedor de CDN alternativo y compatible al fijar, como unpkg o jsdelivr (jspm es el predeterminado):

$ bin/importmap pin react --from jsdelivr
Pinning "react" to https://cdn.jsdelivr.net/npm/react@17.0.2/index.js

Recuerda, sin embargo, que si cambias una fijación de un proveedor a otro, es posible que debas limpiar las dependencias agregadas por el primer proveedor que no se utilizan por el segundo proveedor.

Ejecuta bin/importmap para ver todas las opciones.

Ten en cuenta que este comando es simplemente un envoltorio de conveniencia para resolver nombres de paquetes lógicos a URL de CDN. También puedes buscar las URL de CDN tú mismo y luego fijarlas. Por ejemplo, si quisieras usar Skypack para React, podrías simplemente agregar lo siguiente a config/importmap.rb:

pin "react", to: "https://cdn.skypack.dev/react"

3.4 Pre-carga de módulos fijados

Para evitar el efecto cascada donde el navegador tiene que cargar un archivo tras otro antes de poder llegar a la importación más anidada, importmap-rails soporta enlaces modulepreload. Los módulos fijados pueden ser precargados añadiendo preload: true a la fijación.

Es una buena idea precargar bibliotecas o frameworks que se utilizan en toda tu aplicación, ya que esto le dirá al navegador que los descargue antes.

Ejemplo:

# config/importmap.rb
pin "@github/hotkey", to: "https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js", preload: true
pin "md5", to: "https://cdn.jsdelivr.net/npm/md5@2.3.0/md5.js"
<%# app/views/layouts/application.html.erb %>
<%= javascript_importmap_tags %>

<%# incluirá el siguiente enlace antes de que se configure el importmap: %>
<link rel="modulepreload" href="https://ga.jspm.io/npm:@github/hotkey@1.4.4/dist/index.js">
...

NOTA: Consulta el repositorio de importmap-rails para la documentación más actualizada.

4 Cómo Usar Sprockets

El enfoque ingenuo para exponer los activos de tu aplicación a la web sería almacenarlos en subdirectorios de la carpeta public como images y stylesheets. Hacerlo manualmente sería difícil, ya que la mayoría de las aplicaciones web modernas requieren que los activos se procesen de una manera específica, por ejemplo, comprimiendo y agregando huellas digitales a los activos.

Sprockets está diseñado para preprocesar automáticamente tus activos almacenados en los directorios configurados y, después del procesamiento, exponerlos en la carpeta public/assets con huellas digitales, compresión, generación de mapas de fuentes y otras características configurables.

Los activos aún pueden colocarse en la jerarquía public. Cualquier activo bajo public será servido como archivos estáticos por la aplicación o el servidor web cuando config.public_file_server.enabled esté configurado en true. Debes definir directivas manifest.js para archivos que deben someterse a algún preprocesamiento antes de ser servidos.

En producción, Rails precompila estos archivos en public/assets por defecto. Las copias precompiladas son luego servidas como activos estáticos por el servidor web. Los archivos en app/assets nunca se sirven directamente en producción.

4.1 Archivos de Manifiesto y Directivas

Al compilar activos con Sprockets, Sprockets necesita decidir qué objetivos de nivel superior compilar, generalmente application.css e imágenes. Los objetivos de nivel superior se definen en el archivo manifest.js de Sprockets, por defecto se ve así:

//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js

Contiene directivas - instrucciones que le dicen a Sprockets qué archivos requerir para construir un solo archivo CSS o JavaScript.

Esto está destinado a incluir el contenido de todos los archivos encontrados en el directorio ./app/assets/images o cualquier subdirectorio, así como cualquier archivo reconocido como JS directamente en ./app/javascript o ./vendor/javascript.

Cargará cualquier CSS del directorio ./app/assets/stylesheets (sin incluir subdirectorios). Suponiendo que tengas archivos application.css y marketing.css en la carpeta ./app/assets/stylesheets, te permitirá cargar esas hojas de estilo con <%= stylesheet_link_tag "application" %> o <%= stylesheet_link_tag "marketing" %> desde tus vistas.

Podrías notar que nuestros archivos JavaScript no se cargan desde el directorio assets por defecto, es porque ./app/javascript es el punto de entrada predeterminado para la gema importmap-rails y la carpeta vendor es el lugar donde se almacenarían los paquetes JS descargados.

En el manifest.js también podrías especificar la directiva link para cargar un archivo específico en lugar de todo el directorio. La directiva link requiere proporcionar una extensión de archivo explícita.

Sprockets carga los archivos especificados, los procesa si es necesario, los concatena en un solo archivo y luego los comprime (basado en el valor de config.assets.css_compressor o config.assets.js_compressor). La compresión reduce el tamaño del archivo, permitiendo que el navegador descargue los archivos más rápido.

4.2 Activos Específicos del Controlador

Cuando generas un scaffold o un controlador, Rails también genera un archivo de hoja de estilo en cascada para ese controlador. Además, al generar un scaffold, Rails genera el archivo scaffolds.css.

Por ejemplo, si generas un ProjectsController, Rails también agregará un nuevo archivo en app/assets/stylesheets/projects.css. Por defecto, estos archivos estarán listos para ser usados por tu aplicación inmediatamente usando la directiva link_directory en el archivo manifest.js.

También puedes optar por incluir archivos de hojas de estilo específicos del controlador solo en sus respectivos controladores usando lo siguiente:

<%= stylesheet_link_tag params[:controller] %>

Al hacer esto, asegúrate de no estar usando la directiva require_tree en tu application.css, ya que eso podría resultar en que tus activos específicos del controlador se incluyan más de una vez.

4.3 Organización de Activos

Los activos del pipeline pueden colocarse dentro de una aplicación en una de tres ubicaciones: app/assets, lib/assets o vendor/assets.

  • app/assets es para activos que son propiedad de la aplicación, como imágenes o hojas de estilo personalizadas.

  • app/javascript es para tu código JavaScript

  • vendor/[assets|javascript] es para activos que son propiedad de entidades externas, como frameworks CSS o bibliotecas JavaScript. Ten en cuenta que el código de terceros con referencias a otros archivos también procesados por el pipeline de activos (imágenes, hojas de estilo, etc.), necesitará ser reescrito para usar ayudantes como asset_path.

Otras ubicaciones podrían configurarse en el archivo manifest.js, consulta la sección de Archivos de Manifiesto y Directivas.

4.3.1 Rutas de Búsqueda

Cuando se hace referencia a un archivo desde un manifiesto o un ayudante, Sprockets busca en todas las ubicaciones especificadas en manifest.js. Puedes ver la ruta de búsqueda inspeccionando Rails.application.config.assets.paths en la consola de Rails.

4.3.2 Usar Archivos Índice como Proxies para Carpetas

Sprockets utiliza archivos llamados index (con las extensiones relevantes) para un propósito especial.

Por ejemplo, si tienes una biblioteca CSS con muchos módulos, que se almacena en lib/assets/stylesheets/library_name, el archivo lib/assets/stylesheets/library_name/index.css sirve como el manifiesto para todos los archivos en esta biblioteca. Este archivo podría incluir una lista de todos los archivos requeridos en orden, o una simple directiva require_tree.

También es algo similar a la forma en que un archivo en public/library_name/index.html puede ser alcanzado por una solicitud a /library_name. Esto significa que no puedes usar directamente un archivo índice.

La biblioteca como un todo puede ser accedida en los archivos .css así:

/* ...
*= require library_name
*/

Esto simplifica el mantenimiento y mantiene las cosas limpias al permitir que el código relacionado se agrupe antes de incluirse en otros lugares.

4.4 Codificación de Enlaces a Activos

Sprockets no agrega ningún método nuevo para acceder a tus activos: todavía usas el conocido stylesheet_link_tag:

<%= stylesheet_link_tag "application", media: "all" %>

Si usas la gema turbo-rails, que está incluida por defecto en Rails, entonces incluye la opción data-turbo-track que hace que Turbo verifique si un activo ha sido actualizado y, si es así, lo carga en la página:

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>

En vistas regulares puedes acceder a imágenes en el directorio app/assets/images así:

<%= image_tag "rails.png" %>

Siempre que el pipeline esté habilitado dentro de tu aplicación (y no deshabilitado en el contexto del entorno actual), este archivo es servido por Sprockets. Si existe un archivo en public/assets/rails.png, es servido por el servidor web.

Alternativamente, una solicitud para un archivo con un hash SHA256 como public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png se trata de la misma manera. Cómo se generan estos hashes se cubre en la sección En Producción más adelante en esta guía.

Las imágenes también pueden organizarse en subdirectorios si es necesario, y luego pueden accederse especificando el nombre del directorio en la etiqueta:

<%= image_tag "icons/rails.png" %>

ADVERTENCIA: Si estás precompilando tus activos (ver En Producción a continuación), enlazar a un activo que no existe generará una excepción en la página que llama. Esto incluye enlazar a una cadena en blanco. Por lo tanto, ten cuidado al usar image_tag y los otros ayudantes con datos proporcionados por el usuario.

4.4.1 CSS y ERB

El pipeline de activos evalúa automáticamente ERB. Esto significa que si agregas una extensión erb a un activo CSS (por ejemplo, application.css.erb), entonces los ayudantes como asset_path están disponibles en tus reglas CSS:

.class { background-image: url(<%= asset_path 'image.png' %>) }

Esto escribe la ruta al activo particular que se está haciendo referencia. En este ejemplo, tendría sentido tener una imagen en una de las rutas de carga de activos, como app/assets/images/image.png, que se referenciaría aquí. Si esta imagen ya está disponible en public/assets como un archivo con huella digital, entonces se hace referencia a esa ruta.

Si deseas usar un URI de datos, un método para incrustar los datos de la imagen directamente en el archivo CSS, puedes usar el ayudante asset_data_uri.

#logo { background: url(<%= asset_data_uri 'logo.png' %>) }

Esto inserta un URI de datos correctamente formateado en la fuente CSS.

Ten en cuenta que la etiqueta de cierre no puede ser del estilo -%>.

4.5 Generar un Error Cuando un Activo No se Encuentra

Si estás usando sprockets-rails >= 3.2.0, puedes configurar lo que sucede cuando se realiza una búsqueda de activos y no se encuentra nada. Si desactivas el "fallback de activos", se generará un error cuando no se pueda encontrar un activo.

config.assets.unknown_asset_fallback = false

Si el "fallback de activos" está habilitado, entonces cuando no se pueda encontrar un activo, se imprimirá la ruta en su lugar y no se generará ningún error. El comportamiento del fallback de activos está deshabilitado por defecto.

4.6 Desactivar Huellas Digitales

Puedes desactivar las huellas digitales actualizando config/environments/development.rb para incluir:

config.assets.digest = false

Cuando esta opción es verdadera, se generarán huellas digitales para las URLs de los activos.

4.7 Activar Mapas de Fuentes

Puedes activar los mapas de fuentes actualizando config/environments/development.rb para incluir:

config.assets.debug = true

Cuando el modo de depuración está activado, Sprockets generará un Mapa de Fuentes para cada activo. Esto te permite depurar cada archivo individualmente en las herramientas de desarrollo de tu navegador.

Los activos se compilan y almacenan en caché en la primera solicitud después de que el servidor se inicia. Sprockets establece un encabezado HTTP must-revalidate para reducir la sobrecarga de solicitudes en solicitudes posteriores: en estas, el navegador obtiene una respuesta 304 (No Modificado).

Si alguno de los archivos en el manifiesto cambia entre solicitudes, el servidor responde con un nuevo archivo compilado.

5 En Producción

En el entorno de producción, Sprockets utiliza el esquema de huellas digitales descrito anteriormente. Por defecto, Rails asume que los activos han sido precompilados y serán servidos como activos estáticos por tu servidor web.

Durante la fase de precompilación, se genera un SHA256 a partir del contenido de los archivos compilados y se inserta en los nombres de archivo a medida que se escriben en el disco. Estos nombres con huellas digitales son utilizados por los ayudantes de Rails en lugar del nombre del manifiesto.

Por ejemplo, esto:

<%= stylesheet_link_tag "application" %>

genera algo como esto:

<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" rel="stylesheet" />

El comportamiento de las huellas digitales está controlado por la opción de inicialización config.assets.digest (que por defecto es true).

NOTA: En circunstancias normales, la opción predeterminada config.assets.digest no debería cambiarse. Si no hay huellas digitales en los nombres de archivo, y se establecen encabezados de futuro lejano, los clientes remotos nunca sabrán que deben volver a buscar los archivos cuando su contenido cambie.

5.1 Precompilación de Activos

Rails viene con un comando para compilar los manifiestos de activos y otros archivos en el pipeline.

Los activos compilados se escriben en la ubicación especificada en config.assets.prefix. Por defecto, este es el directorio /assets.

Puedes llamar a este comando en el servidor durante la implementación para crear versiones compiladas de tus activos directamente en el servidor. Consulta la siguiente sección para obtener información sobre la compilación local.

El comando es:

$ RAILS_ENV=production rails assets:precompile

Esto enlaza la carpeta especificada en config.assets.prefix a shared/assets. Si ya usas esta carpeta compartida, necesitarás escribir tu propio comando de implementación.

Es importante que esta carpeta sea compartida entre implementaciones para que las páginas en caché remotamente que hacen referencia a los activos compilados antiguos aún funcionen durante la vida útil de la página en caché.

NOTA: Siempre especifica un nombre de archivo compilado esperado que termine con .js o .css.

El comando también genera un .sprockets-manifest-randomhex.json (donde randomhex es una cadena hexadecimal aleatoria de 16 bytes) que contiene una lista con todos tus activos y sus respectivas huellas digitales. Esto es utilizado por los métodos de ayuda de Rails para evitar devolver las solicitudes de mapeo a Sprockets. Un archivo de manifiesto típico se ve así:

{"files":{"application-<fingerprint>.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383,
"digest":"<fingerprint>","integrity":"sha256-<random-string>"}},
"assets":{"application.js":"application-<fingerprint>.js"}}

En tu aplicación, habrá más archivos y activos listados en el manifiesto, <fingerprint> y <random-string> también se generarán.

La ubicación predeterminada para el manifiesto es la raíz de la ubicación especificada en config.assets.prefix ('/assets' por defecto).

NOTA: Si faltan archivos precompilados en producción, recibirás una excepción Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError que indica el nombre del archivo(s) faltante(s).

5.1.1 Encabezado Expires de Futuro Lejano

Los activos precompilados existen en el sistema de archivos y son servidos directamente por tu servidor web. No tienen encabezados de futuro lejano por defecto, por lo que para obtener el beneficio de las huellas digitales, tendrás que actualizar la configuración de tu servidor para agregar esos encabezados.

Para Apache:

# Las directivas Expires* requieren que el módulo Apache
# `mod_expires` esté habilitado.
<Location /assets/>
  # El uso de ETag no se recomienda cuando Last-Modified está presente
  Header unset ETag
  FileETag None
  # RFC dice que solo se debe almacenar en caché durante 1 año
  ExpiresActive On
  ExpiresDefault "access plus 1 year"
</Location>

Para NGINX:

location ~ ^/assets/ {
  expires 1y;
  add_header Cache-Control public;

  add_header ETag "";
}

5.2 Precompilación Local

A veces, es posible que no desees o no puedas compilar activos en el servidor de producción. Por ejemplo, es posible que tengas acceso limitado de escritura a tu sistema de archivos de producción, o que planees implementar con frecuencia sin hacer ningún cambio en tus activos.

En tales casos, puedes precompilar activos localmente, es decir, agregar un conjunto final de activos compilados y listos para producción a tu repositorio de código fuente antes de enviarlos a producción. De esta manera, no necesitan ser precompilados por separado en el servidor de producción en cada implementación.

Como se mencionó anteriormente, puedes realizar este paso usando

$ RAILS_ENV=production rails assets:precompile

Ten en cuenta las siguientes advertencias:

  • Si los activos precompilados están disponibles, se servirán, incluso si ya no coinciden con los activos originales (no compilados), incluso en el servidor de desarrollo.

    Para asegurarte de que el servidor de desarrollo siempre compile activos sobre la marcha (y por lo tanto siempre refleje el estado más reciente del código), el entorno de desarrollo debe configurarse para mantener los activos precompilados en una ubicación diferente a la de producción. De lo contrario, cualquier activo precompilado para su uso en producción sobrescribirá las solicitudes de los mismos en desarrollo (es decir, los cambios que realices en los activos no se reflejarán en el navegador).

    Puedes hacer esto agregando la siguiente línea a config/environments/development.rb:

    config.assets.prefix = "/dev-assets"
    
  • La tarea de precompilación de activos en tu herramienta de implementación (por ejemplo, Capistrano) debe deshabilitarse.

  • Cualquier compresor o minificador necesario debe estar disponible en tu sistema de desarrollo.

También puedes establecer ENV["SECRET_KEY_BASE_DUMMY"] para activar el uso de una secret_key_base generada aleatoriamente que se almacena en un archivo temporal. Esto es útil cuando se precompilan activos para producción como parte de un paso de construcción que de otro modo no necesitaría acceso a los secretos de producción.

$ SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile

5.3 Compilación en Vivo

En algunas circunstancias, es posible que desees usar la compilación en vivo. En este modo, todas las solicitudes de activos en el pipeline son manejadas directamente por Sprockets.

Para habilitar esta opción, establece:

config.assets.compile = true

En la primera solicitud, los activos se compilan y almacenan en caché como se describe en Almacenamiento en Caché de Activos, y los nombres de manifiesto utilizados en los ayudantes se alteran para incluir el hash SHA256.

Sprockets también establece el encabezado HTTP Cache-Control en max-age=31536000. Esto señala a todos los cachés entre tu servidor y el navegador del cliente que este contenido (el archivo servido) puede almacenarse en caché durante 1 año. El efecto de esto es reducir el número de solicitudes para este activo desde tu servidor; el activo tiene una buena oportunidad de estar en el caché local del navegador o en algún caché intermedio.

Este modo utiliza más memoria, funciona peor que el predeterminado y no se recomienda.

5.4 CDNs

CDN significa Red de Entrega de Contenidos, están diseñadas principalmente para almacenar en caché activos en todo el mundo para que cuando un navegador solicite el activo, haya una copia en caché geográficamente cercana a ese navegador. Si estás sirviendo activos directamente desde tu servidor Rails en producción, la mejor práctica es usar un CDN frente a tu aplicación.

Un patrón común para usar un CDN es establecer tu aplicación de producción como el servidor de "origen". Esto significa que cuando un navegador solicita un activo del CDN y hay un fallo de caché, lo obtendrá de tu servidor sobre la marcha y luego lo almacenará en caché. Por ejemplo, si estás ejecutando una aplicación Rails en example.com y tienes un CDN configurado en mi-subdominio-cdn.fictional-cdn.com, entonces cuando se realiza una solicitud a mi-subdominio-cdn.fictional-cdn.com/assets/smile.png, el CDN consultará tu servidor una vez en example.com/assets/smile.png y almacenará la solicitud en caché. La próxima solicitud al CDN que llegue a la misma URL utilizará la copia en caché. Cuando el CDN puede servir un activo directamente, la solicitud nunca toca tu servidor Rails. Dado que los activos de un CDN están geográficamente más cerca del navegador, la solicitud es más rápida, y dado que tu servidor no necesita dedicar tiempo a servir activos, puede centrarse en servir el código de la aplicación lo más rápido posible.

5.4.1 Configurar un CDN para Servir Activos Estáticos

Para configurar tu CDN, debes tener tu aplicación ejecutándose en producción en Internet en una URL públicamente disponible, por ejemplo, example.com. Luego, deberás registrarte en un servicio CDN de un proveedor de alojamiento en la nube. Cuando hagas esto, debes configurar el "origen" del CDN para que apunte de regreso a tu sitio web example.com. Consulta la documentación de tu proveedor para obtener información sobre cómo configurar el servidor de origen.

El CDN que aprovisionaste debería darte un subdominio personalizado para tu aplicación, como mi-subdominio-cdn.fictional-cdn.com (ten en cuenta que fictional-cdn.com no es un proveedor de CDN válido en el momento de escribir esto). Ahora que has configurado tu servidor CDN, necesitas decirle a los navegadores que usen tu CDN para obtener activos en lugar de tu servidor Rails directamente. Puedes hacer esto configurando Rails para establecer tu CDN como el host de activos en lugar de usar una ruta relativa. Para establecer tu host de activos en Rails, necesitas configurar config.asset_host en config/environments/production.rb:

config.asset_host = 'mi-subdominio-cdn.fictional-cdn.com'

NOTA: Solo necesitas proporcionar el "host", este es el subdominio y el dominio raíz, no necesitas especificar un protocolo o "esquema" como http:// o https://. Cuando se solicita una página web, el protocolo en el enlace a tu activo que se genera coincidirá con cómo se accede a la página web por defecto.

También puedes establecer este valor a través de una variable de entorno para facilitar la ejecución de una copia de ensayo de tu sitio:

config.asset_host = ENV['CDN_HOST']

NOTA: Necesitarías establecer CDN_HOST en tu servidor a mi-subdominio-cdn.fictional-cdn.com para que esto funcione.

Una vez que hayas configurado tu servidor y tu CDN, las rutas de los activos desde los ayudantes como:

<%= asset_path('smile.png') %>

Se representarán como URL completas de CDN como http://mi-subdominio-cdn.fictional-cdn.com/assets/smile.png (huella digital omitida para mayor claridad).

Si el CDN tiene una copia de smile.png, la servirá al navegador, y tu servidor ni siquiera sabrá que se solicitó. Si el CDN no tiene una copia, intentará encontrarla en el "origen" example.com/assets/smile.png, y luego la almacenará para uso futuro.

Si deseas servir solo algunos activos desde tu CDN, puedes usar la opción personalizada :host en tu ayudante de activos, que sobrescribe el valor establecido en config.action_controller.asset_host.

<%= asset_path 'image.png', host: 'mi-subdominio-cdn.fictional-cdn.com' %>

5.4.2 Personalizar el Comportamiento de Caché del CDN

Un CDN funciona almacenando contenido en caché. Si el CDN tiene contenido obsoleto o incorrecto, entonces está perjudicando en lugar de ayudar a tu aplicación. El propósito de esta sección es describir el comportamiento general de caché de la mayoría de los CDNs. Tu proveedor específico puede comportarse de manera ligeramente diferente.

5.4.2.1 Caché de Solicitudes del CDN

Mientras que un CDN se describe como bueno para almacenar activos en caché, en realidad almacena en caché toda la solicitud. Esto incluye el cuerpo del activo, así como cualquier encabezado. El más importante es Cache-Control, que le dice al CDN (y a los navegadores web) cómo almacenar en caché los contenidos. Esto significa que si alguien solicita un activo que no existe, como /assets/i-dont-exist.png, y tu aplicación Rails devuelve un 404, entonces tu CDN probablemente almacenará en caché la página 404 si hay un encabezado Cache-Control válido presente.

5.4.2.2 Depuración de Encabezados del CDN

Una forma de comprobar que los encabezados se almacenan correctamente en caché en tu CDN es utilizando curl. Puedes solicitar los encabezados tanto de tu servidor como de tu CDN para verificar que sean los mismos:

$ curl -I http://www.example/assets/application-
d0e099e021c95eb0de3615fd1d8c4d83.css
HTTP/1.1 200 OK
Server: Cowboy
Date: Sun, 24 Aug 2014 20:27:50 GMT
Connection: keep-alive
Last-Modified: Thu, 08 May 2014 01:24:14 GMT
Content-Type: text/css
Cache-Control: public, max-age=2592000
Content-Length: 126560
Via: 1.1 vegur

Versus la copia del CDN:

$ curl -I http://mi-subdominio-cdn.fictional-cdn.com/application-
d0e099e021c95eb0de3615fd1d8c4d83.css
HTTP/1.1 200 OK Server: Cowboy Last-
Modified: Thu, 08 May 2014 01:24:14 GMT Content-Type: text/css
Cache-Control:
public, max-age=2592000
Via: 1.1 vegur
Content-Length: 126560
Accept-Ranges:
bytes
Date: Sun, 24 Aug 2014 20:28:45 GMT
Via: 1.1 varnish
Age: 885814
Connection: keep-alive
X-Served-By: cache-dfw1828-DFW
X-Cache: HIT
X-Cache-Hits:
68
X-Timer: S1408912125.211638212,VS0,VE0

Consulta la documentación de tu CDN para obtener información adicional que puedan proporcionar, como X-Cache o para cualquier encabezado adicional que puedan agregar.

5.4.2.3 CDNs y el Encabezado Cache-Control

El encabezado Cache-Control describe cómo se puede almacenar en caché una solicitud. Cuando no se utiliza un CDN, un navegador utilizará esta información para almacenar contenidos en caché. Esto es muy útil para los activos que no se modifican, de modo que un navegador no necesita volver a descargar el CSS o JavaScript de un sitio web en cada solicitud. Generalmente queremos que nuestro servidor Rails le diga a nuestro CDN (y al navegador) que el activo es "public". Eso significa que cualquier caché puede almacenar la solicitud. También queremos comúnmente establecer max-age, que es cuánto tiempo el caché almacenará el objeto antes de invalidar el caché. El valor de max-age se establece en segundos con un valor máximo posible de 31536000, que es un año. Puedes hacer esto en tu aplicación Rails configurando

config.public_file_server.headers = {
  'Cache-Control' => 'public, max-age=31536000'
}

Ahora, cuando tu aplicación sirva un activo en producción, el CDN almacenará el activo hasta por un año. Dado que la mayoría de los CDNs también almacenan en caché los encabezados de la solicitud, este Cache-Control se pasará a todos los navegadores futuros que busquen este activo. El navegador luego sabe que puede almacenar este activo por un tiempo muy largo antes de necesitar volver a solicitarlo.

5.4.2.4 CDNs e Invalidación de Caché Basada en URL

La mayoría de los CDNs almacenarán en caché el contenido de un activo basado en la URL completa. Esto significa que una solicitud a

http://mi-subdominio-cdn.fictional-cdn.com/assets/smile-123.png

Será un caché completamente diferente de

http://mi-subdominio-cdn.fictional-cdn.com/assets/smile.png

Si deseas establecer un max-age de futuro lejano en tu Cache-Control (y lo deseas), entonces asegúrate de que cuando cambies tus activos, tu caché se invalide. Por ejemplo, al cambiar la cara sonriente en una imagen de amarilla a azul, deseas que todos los visitantes de tu sitio obtengan la nueva cara azul. Cuando usas un CDN con el pipeline de activos de Rails, config.assets.digest está configurado en true por defecto para que cada activo tenga un nombre de archivo diferente cuando se cambia. De esta manera, nunca tienes que invalidar manualmente ningún elemento en tu caché. Al usar un nombre de activo único diferente, tus usuarios obtienen el último activo.

6 Personalizando el Pipeline

6.1 Compresión de CSS

Una de las opciones para comprimir CSS es YUI. El compresor de CSS de YUI proporciona minificación.

La siguiente línea habilita la compresión de YUI y requiere la gema yui-compressor.

config.assets.css_compressor = :yui

6.2 Compresión de JavaScript

Las opciones posibles para la compresión de JavaScript son :terser, :closure y :yui. Estas requieren el uso de las gemas terser, closure-compiler o yui-compressor, respectivamente.

Toma la gema terser, por ejemplo. Esta gema envuelve Terser (escrito para Node.js) en Ruby. Comprime tu código eliminando espacios en blanco y comentarios, acortando nombres de variables locales y realizando otras micro-optimizaciones como cambiar declaraciones if y else a operadores ternarios donde sea posible.

La siguiente línea invoca terser para la compresión de JavaScript.

config.assets.js_compressor = :terser

NOTA: Necesitarás un runtime compatible con ExecJS para usar terser. Si estás usando macOS o Windows, tienes un runtime JavaScript instalado en tu sistema operativo.

NOTA: La compresión de JavaScript también funcionará para tus archivos JavaScript cuando estés cargando tus activos a través de las gemas importmap-rails o jsbundling-rails.

6.3 Comprimir tus activos con GZIP

Por defecto, se generará una versión comprimida con GZIP de los activos compilados, junto con la versión no comprimida de los activos. Los activos comprimidos con GZIP ayudan a reducir la transmisión de datos por la red. Puedes configurar esto estableciendo el indicador gzip.

config.assets.gzip = false # desactivar la generación de activos comprimidos con GZIP

Consulta la documentación de tu servidor web para obtener instrucciones sobre cómo servir activos comprimidos con GZIP.

6.4 Usar tu Propio Compresor

Las configuraciones de compresor para CSS y JavaScript también aceptan cualquier objeto. Este objeto debe tener un método compress que tome una cadena como único argumento y debe devolver una cadena.

class Transformer
  def compress(string)
    do_something_returning_a_string(string)
  end
end

Para habilitar esto, pasa un nuevo objeto a la opción de configuración en application.rb:

config.assets.css_compressor = Transformer.new

6.5 Cambiar la Ruta de assets

La ruta pública que Sprockets utiliza por defecto es /assets.

Esto puede cambiarse a otra cosa:

config.assets.prefix = "/some_other_path"

Esta es una opción útil si estás actualizando un proyecto antiguo que no usaba el pipeline de activos y ya utiliza esta ruta o deseas usar esta ruta para un nuevo recurso.

6.6 Encabezados X-Sendfile

El encabezado X-Sendfile es una directiva para el servidor web para ignorar la respuesta de la aplicación y en su lugar servir un archivo específico desde el disco. Esta opción está desactivada por defecto, pero puede habilitarse si tu servidor la admite. Cuando está habilitada, esto pasa la responsabilidad de servir el archivo al servidor web, lo cual es más rápido. Echa un vistazo a send_file para saber cómo usar esta función.

Apache y NGINX admiten esta opción, que puede habilitarse en config/environments/production.rb:

# config.action_dispatch.x_sendfile_header = "X-Sendfile" # para Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # para NGINX

ADVERTENCIA: Si estás actualizando una aplicación existente y planeas usar esta opción, ten cuidado de pegar esta opción de configuración solo en production.rb y en cualquier otro entorno que definas con comportamiento de producción (no en application.rb).

CONSEJO: Para obtener más detalles, consulta la documentación de tu servidor web de producción:

7 Almacenamiento en Caché de Activos

Por defecto, Sprockets almacena en caché los activos en tmp/cache/assets en los entornos de desarrollo y producción. Esto puede cambiarse de la siguiente manera:

config.assets.configure do |env|
  env.cache = ActiveSupport::Cache.lookup_store(:memory_store,
                                                { size: 32.megabytes })
end

Para desactivar el almacenamiento en caché de activos:

config.assets.configure do |env|
  env.cache = ActiveSupport::Cache.lookup_store(:null_store)
end

8 Agregar Activos a tus Gemas

Los activos también pueden provenir de fuentes externas en forma de gemas.

Un buen ejemplo de esto es la gema jquery-rails. Esta gema contiene una clase de motor que hereda de Rails::Engine. Al hacer esto, Rails es informado de que el directorio para esta gema puede contener activos y los directorios app/assets, lib/assets y vendor/assets de este motor se agregan a la ruta de búsqueda de Sprockets.

9 Hacer que tu Biblioteca o Gema sea un Preprocesador

Sprockets utiliza Procesadores, Transformadores, Compresores y Exportadores para extender la funcionalidad de Sprockets. Echa un vistazo a Extending Sprockets para aprender más. Aquí registramos un preprocesador para agregar un comentario al final de los archivos text/css (.css).

module AddComment
  def self.call(input)
    { data: input[:data] + "/* Hola desde mi extensión de sprockets */" }
  end
end

Ahora que tienes un módulo que modifica los datos de entrada, es hora de registrarlo como un preprocesador para tu tipo MIME.

Sprockets.register_preprocessor 'text/css', AddComment

10 Bibliotecas Alternativas

A lo largo de los años, ha habido múltiples enfoques predeterminados para manejar los activos. La web evolucionó y comenzamos a ver más y más aplicaciones pesadas en JavaScript. En The Rails Doctrine creemos que The Menu Is Omakase, por lo que nos centramos en la configuración predeterminada: Sprockets con Import Maps.

Somos conscientes de que no hay soluciones de talla única para todos los diversos frameworks/extensiones de JavaScript y CSS disponibles. Hay otras bibliotecas de empaquetado en el ecosistema de Rails que deberían empoderarte en los casos donde la configuración predeterminada no es suficiente.

10.1 jsbundling-rails

jsbundling-rails es una alternativa dependiente del tiempo de ejecución de JavaScript a la forma de importmap-rails de empaquetar JS con Bun, esbuild, rollup.js o Webpack.

La gema proporciona una tarea de construcción en package.json para observar cambios y generar automáticamente la salida en desarrollo. Para producción, engancha automáticamente la tarea javascript:build en la tarea assets:precompile para asegurarse de que todas tus dependencias de paquetes se hayan instalado y que el JavaScript se haya construido para todos los puntos de entrada.

¿Cuándo usar en lugar de importmap-rails? Si tu código JavaScript depende de la transpilation, por lo que si estás usando Babel, TypeScript o el formato React JSX, entonces jsbundling-rails es la forma correcta de proceder.

10.2 Webpacker/Shakapacker

Webpacker fue el preprocesador y empaquetador de JavaScript predeterminado para Rails 5 y 6. Ahora ha sido retirado. Un sucesor llamado shakapacker existe, pero no es mantenido por el equipo o proyecto de Rails.

A diferencia de otras bibliotecas en esta lista, webpacker/shakapacker es completamente independiente de Sprockets y podría procesar tanto archivos JavaScript como CSS.

NOTA: Lee el documento Comparison with Webpacker para entender las diferencias entre jsbundling-rails y webpacker/shakapacker.

10.3 cssbundling-rails

cssbundling-rails permite empaquetar y procesar tu CSS usando Tailwind CSS, Bootstrap, Bulma, PostCSS o Dart Sass, y luego entrega el CSS a través del pipeline de activos.

Funciona de manera similar a jsbundling-rails, por lo que agrega la dependencia de Node.js a tu aplicación con el proceso yarn build:css --watch para regenerar tus hojas de estilo en desarrollo y engancha en la tarea assets:precompile en producción.

¿Cuál es la diferencia con Sprockets? Sprockets por sí solo no puede transpilar el Sass en CSS, se requiere Node.js para generar los archivos .css a partir de tus archivos .sass. Una vez que los archivos .css se generan, entonces Sprockets puede entregarlos a tus clientes.

NOTA: cssbundling-rails depende de Node para procesar el CSS. Las gemas dartsass-rails y tailwindcss-rails usan versiones independientes de Tailwind CSS y Dart Sass, lo que significa que no hay dependencia de Node. Si estás usando importmap-rails para manejar tus JavaScripts y dartsass-rails o tailwindcss-rails para CSS, podrías evitar completamente la dependencia de Node, lo que resulta en una solución menos compleja.

10.4 dartsass-rails

Si deseas usar Sass en tu aplicación, dartsass-rails viene como un reemplazo para la gema sassc-rails heredada. dartsass-rails utiliza la implementación de Dart Sass en favor de LibSass que fue deprecada en 2020 y utilizada por sassc-rails.

A diferencia de sassc-rails, la nueva gema no está directamente integrada con Sprockets. Consulta la página de inicio de la gema para obtener instrucciones de instalación/migración.

ADVERTENCIA: La popular gema sassc-rails no se mantiene desde 2019.

10.5 tailwindcss-rails

tailwindcss-rails es una gema envolvente para la versión ejecutable independiente del framework Tailwind CSS v3. Se utiliza para nuevas aplicaciones cuando se proporciona --css tailwind al comando rails new. Proporciona un proceso watch para generar automáticamente la salida de Tailwind en desarrollo. Para producción, se engancha en la tarea assets:precompile.


Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.