Skip to content

2022_01 Actualización a rails 7

Vladimir Támara Patiño edited this page Jun 4, 2022 · 22 revisions

1. Experimentos para actualizar a rails7

Rails 7 da nuevas posibilidades para integración con Javascript al lado del cliente, sin embargo son muchas opciones mutuamente excluyentes por lo que experimentamos con:

  • Alejarnos de webpacker porque requiere bastantes recursos computacionales, su configuración es compleja y emplea infraestructura de node.js con problemas (ver https://www.youtube.com/watch?v=M3BM9TB-8yA) y en su lugar probar con:
    • Importmaps
    • jsbundling-rails y esbuild
  • mrujs como remplazo a @rails/ujs (a futuro sería interesante experimentar con StimulusReflex)
  • Módulos y más ES6 tanto en lo manejado por jsbundling-rails como en lo manejado por sprockets
  • Turbo y Stimulus
  • Seguir distribuyendo javascript de motores a aplicaciones mediante sprockets, pues consideramos simplifica respecto a publicar paquetes npm. Publicar paquetes npm sigue siendo ruta interesante cuando refactoricemos librerías javascript con más utilidad que la requerida sólo por nuestros motores y aplicaciones.
  • Pruebas al sistema con cuprite

2. Conclusiones de los experimentos:

  • Importmaps: es un objetivo a largo plazo interesante, pero requiere bastante esfuerzo en cambios, pues no soporta coffeescript y la mayoría de motores usan este lenguaje. Ver https://github.com/pasosdeJesus/sivel2_gen/issues/573
  • En lugar de webpacker, encontramos viable el transito y uso de jsbundling-rails con esbuild --posteriormente encontramos esa recomendación en https://noelrappin.com/blog/2021/09/rails-7-and-javascript/
  • mrujs: aunque es interesante y se avanzó por el momento descartamos su uso (ver https://github.com/pasosdeJesus/sivel2_gen/issues/647)
  • Turbo: Nos parece viable y puede habilitarse en todo motor y aplicación de manera cauta (mayormente deshabilitado) e ir adoptando más paulatinamente
  • stimulus: viable incluirlo de manera pre-determinada e ir migrando gradualmente.
  • Módulos y ES6: resultó posible usarlos con sprockets 4, babel-transpiler y emulando minimamente CommonJS, junto con configuraciones para generar mapas usables por navegador. Como se compilan primero los módulos de app/javascript y despues los de app/assets/javascripts, es posible en app/assets/javascripts usar lo de app/javascript empleando el objeto global window (por eso en app/javascript/application.rb se hace por ejemplo windows.Rails = Rails).
  • Continuar usando sprockets unificando en app/assets/javascripts/recursos_sprockets.js, es importante para:
    • Migrar paulatinamente. Es decir seguir usando tanto como sea posible lo que se ha legado en Javascript (incluyendo lo que hay en CoffeeScript y jQuery).
    • Ir organizando paulatinamente como módulos y cuando se requiera refactorizando en paquete npm
    • Debe cambiarse la inicialización en javascript para centralizarla en app/javascript/application.rb sin depender de eventos como DOMContentLoaded, load (que son alterados por Turbo) ni turbo:load como recomienda la documentación de Turbo, pues encontramos que puede dispararse varias veces.
  • cuprite: vuelve a hacer viables pruebas al sistema con minitest y capybara.

Es un transito difícil y puede toparse con fallas en rails7 o en gemas que se usen. Por ejemplo encontramos de la manera dura que la gema meta-request (al menos hasta su versión 0.7.3) es incompatible con rails7 (ver https://github.com/rails/rails/issues/44157). El soporte de la comunidad rails ha sido efectivo.

3. Ruta de acción recomendada

  1. En lo existente pasar a rails7 con las opciones elegidas tras experimentación
  • Cambiar webpacker por jsbundling-rails + esbuild (que es efectivamente rápido y liviano)
  • Agregar gema y paquete turbo-rails y quitar turbolinks pasando eventos y atributos de turbolinks a turbo
  • Agregar gema stimulus-rails
  • Usar gridstack como módulo (y no mediante sprockets)
  • Reconfigurar javascript centralizando lo manejado por esbuild en app/javascript/application.js y lo manejado por sprockets en app/assets/javascripts/recursos_sprockets.js
    • El envío automático de formularios con remote=true ya no soporta format.script en controlador. En controladores cambiar uso de format.js (que sólo funcionaría con jQuery) respondiendo con HTML y con javascript remplazar
  • Hemos notado que rails7 es más exigente con el token CSRF en las peticiones AJAX, es fácil de agregar como encabezado en peticiones jQuery $.ajax y como parte de los parámetros en $.post.
  • Cambiar inicialización. Los eventos DOMContentLoaded y loaded no operan como es típico debido a Turbo. Turbo provee el evento turbo:load pero la documentación sugiere no usarlo. Hemos notado que suele dispararse varias veces. Lo que deba cargarse una sola vez para toda la aplicación dejarlo en app/javascript/application.js si es el caso dentro del bloque que se ejecuta tras la carga completa de los recursos sprockets y la presentación del documento. Lo que deba ejecutarse cada vez que turbo cambie página asegurar que no falla al ejecutarse varias veces consecutivas y dejarlo en un escuchador de turbo:load preferiblemente el de app/javascript/application.js
  • Agregar gema cuprite y al menos una prueba al sistema de ingreso
  1. En lo nuevo que se haga:
  • Evitar jQuery y coffescript
  • En el caso de aplicaciones agregar funcionalidad en app/javascript de una vez como módulo.
  • En el caso de motores agregar nuevas funcionalidad como módulo y con sintaxis y opciones moderna (e.g promesas) en app/javascript/*.es6 cargando e inicializando en función *eventos_comunes* de app/javascript/mimotor/motor.js para facilitar futuro paso a app/javascript o empaquetamiento npm (como se ha hecho con autocompleta_ajax ).
  1. Quitar la dependencia de jQuery que tiene como pre-requisitos cambiar los plugins y las funcionalidades de jQuery:

    • 2.1 Cambiar control de autocompletación y quitar jquery-ui. No encontramos un remplazo, aunque se ha experimentando con éxito con un control implementado desde ceros con el elemento datalist en HTML5. Ver autocompleta_ajax.
    • 2.2 Cambiar uso de cocoon que depende de jquery. Para esto se ha empezado a experimentar con stimulus+turbo
    • 2.3 Cambiar uso de chosen en diversos cuadros de selección desde sip. No hemos encontrado un reemplazo equivalente en Javascript puro, chosen no ofrece un camino de cambio, ver https://github.com/harvesthq/chosen/issues/3118 y por el contrario hay paquetes npm para react y vue que lo usan (con todo y jquery). Se trata de un paquete de 208K con fuentes Javascript para jQuery de 46K sin más dependencias.
    • 2.4 Cambiar uso de gridstack desde mr519_gen. Encontramos que las versiones recientes posteriores a 1.0.0 no requieren jQuery por lo que se propone actualizar a la versión más reciente.
    • 2.5 Cambiar $.ajax, $.post a corto plazo por Rails.ajax pero preferible por window.fetch --que es más complejo de lo esperado, pudiendo requerir cambios en controladores porque fetch no soporta el método js (que permitía al controlador enviar un javascript que era ejecutado en el cliente), llamando más al envio de HTML.
  2. Convertir coffeescript a Javascript

  3. A mediano o largo plazo emplear importmaps y componentes Web (estos componentes son parte del estándar HTML reciente y buscan suplir lo que hacen librerías como react, que posiblemente hará obsoleto react así como previas novedades en HTML han hecho obsoleto jquery).

4. Conceptos útiles

Para rediseñar la inicialización nos ha servido repasar conceptos de ES6, que refraseamos de Javascript Definitive Guide edición 7: En los módulos, el alcance de las declaraciones de nivel superior (top level o que están fuera de funciones o clases en el archivo) es el módulo mismo, lo que se requiera fuera del módulo debe exportarse explicitamente. Sin embargo, en los scripts que no son módulos, el alcance de las declaraciones de nivel superior es el documento completo, y las declaraciones son compartidas por todos los scripts en el documento. Las declaraciones hechas con function y var (al estilo antiguo) se comparten a través de propiedades del objeto global (window). Las declaraciones hechas con const, let y class también se comparten y tienen el mismo alcance del documento, pero no existen como propiedades del objeto global ni de objeto alguno al cual el código Javascript tenga acceso.

La inicialización de lo escrito en Javascript comienza en app/javascript/application.rb donde preferimos usar la sintaxis moderna y modular de javascript con el espacio de nombres global. Los recursos manejados por sprockets tipicamente estarán en el objeto global y los cargamos después de cargar lo de app/javascript/application.rb (difiriendo la ejecución de ambos tras la carga del HTML completo). Ver el detalle de la inicialización propuesta, más adelante, con las modificaciones propuestas a app/javascript/application.rb

5. Detalle de actualización a rails7 con opciones elegidas tras experimentación

Nos ha resultado útil dividir el transito en dos grandes partes que llamamos al lado del servidor y al lado del cliente.

5.1. Actualización de lo que corre al lado del servidor

  • Emplear rama rails7esjs

  • Gemas: En Gemfile actualizar rails a >~ 7.0 y actualizar gemas dependientes de sip a rama rails7esjs. Cambiar #gem 'byebug' por gem 'debug'. Ejecutar bundle update; bundle

  • Cambiar versión en lib/motor/version.rb

  • Cambiar en fuentes lo que requiere cambios en rails 7 al lado del servidor

    • En bin/gc.sh actualizar db:structure:dump por db:schema:dump
    • En modelos en relaciones belongs_to que no tienen optional agregar optional: false. Ayuda: find . -name "*rb" -exec grep -l "belongs_to" {} ';'
  • Ejecutar bin/rails app:update. Tras esto, si es un motor, en test/dummy/config/boot.rb dejar:

      ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
      require "bundler/setup" # Configurar gemas listadas en Gemfile                                  
      require "bootsnap/setup" # Acelerar tiempo de arranque dejando en colchón operaciones costosas                                                   
      $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)  
    
  • En directorio de aplicación config/application.rb cambiar config.load_defaults por 7.0 y ejecutar git add config/initializers/new_framework_defaults_7_0.rb

  • Verificar que pasan pruebas de regresión con minitest

5.2. Actualización de lo que corre al lado del cliente

  • En Gemfile quitar webpacker y turbolinks y agregar babel-transpiler, jsbundling-rails, sprockets-rails y turbo-rails. Ejecutar bundle update; bundle
  • En directorio de aplicación package.json quitar toda referencia a webpacker, webpack y expose-loader y agregar
"scripts": {
  "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds"
}
  • Correr yarn add esbuild @hotwired/turbo-rails @rails/ujs
  • Reorganizar directorios
    • git rm -rf config/webpack* public/packs
    • mkdir app/assets/builds/; touch app/assets/builds/.mantiene; git add app/assets/builds/.mantiene
    • Agregar app/assets/builds/ a .gitignore
    • git mv app/assets/javascripts/application.js app/assets/javascripts/recursos_globales.js
    • git mv app/packs/entrypoints app/javascript
  • Reconfigurar sprockets cambiando app/assets/config/manifest.js para que sea al menos el siguiente pero agregar otros que se requieran globalmente y preparados por sprockets:
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
//= link_directory ../../../node_modules/chosen-js .png
//= link recursos_sprockets.js
//= link recursos_sprockets.js.map
//= link application.css
//= link_tree ../builds
  • Reconfigurar lo de app/javascript
    • Editar app/javascript/application.js para quitar import expose_loader..., quitar import de jquery-ui, quitar referencias a Turbolinks y asegurar que tiene al comienzo:
import Rails from "@rails/ujs";
import "@hotwired/turbo-rails";
Rails.start();
window.Rails = Rails

import './jquery'
import '../../vendedor/recursos/javascripts/jquery-ui'   

import 'gridstack' //Solo en caso de que el motor o aplicación incluya a mr519_gen

// Al final agregar:
let esperarRecursosSprocketsYDocumento = function (resolver) {                             
  if (typeof window.puntomontaje == 'undefined') {                               
    setTimeout(esperarRecursosSprocketsYDocumento, 100, resolver)                          
    return false                                                                 
  }                                                                              
  if (document.readyState !== 'complete') { 
    setTimeout(esperarRecursosSprocketsYDocumento, 100, resolver)                          
    return false                                                                 
  }                                                                              

  resolver("Recursos sprockets cargados y documento presentado en navegador")                             
  return true                                                                    
}                                                                                
                                                                                 
let promesaRecursosSprocketsYDocumento = new Promise((resolver, rechazar) => {             
  esperarRecursosSprocketsYDocumento(resolver)             
})                                                                               
                                                                                 
promesaRecursosSprocketsYDocumento.then((mensaje) => {                                     
  console.log(mensaje)                                     
  var root;                                                                      
  root = window;                                                                 
  sip_prepara_eventos_comunes(root);                                             
})

document.addEventListener('turbo:load', (e) => {                                 
 /* Lo que debe ejecutarse cada vez que turbo cargue una página,                 
 * tener cuidado porque puede dispararse el evento turbo varias                  
 * veces consecutivas al cargar una página.                              
 */                                                                              
··                                                                               
  console.log('Escuchador turbo:load')                 
  sip_ejecutarAlCargarPagina(window) 
})
* Crear `app/javascript/jquery.js` para que sea:
import jquery from 'jquery';                                                     
window.jQuery = jquery;                                                          
window.$ = jquery;     
* De la distribución de `jquery-ui` copiar `jquery-ui.js` en `vendedor/recursos/javascripts/jquery-ui.js`
  • Para asegurar que se generan mapas útiles para depuración en navegador:
    • En config/environments/development.rb agregar:

      config.assets.debug = true
      config.assets.resolv_with = %i[manifest]
      
    • En app/assets/config/manifest.js agregar:

      //= link recursos_globales.js
      //= link recursos_globales.js.map
      
  • Buscar usos de turbolinks y cambiar por turbo. Ayudas: find app -exec grep -li turbolinks {} ';' y find test -exec grep -li turbolinks {} ';'
  • Buscar posibles root = exports para cambiar por root = window. Ayuda: find app -exec grep -li "root.*=.*exports" {} ';' y find test -exec grep -li "root.*=.*exports" {} ';'
  • Buscar botones submit sin data-turbo=false y agregarlo. Ayudas: find app -name "*html*" -exec grep -l "submit" {} '; y find test -name "*html*" -exec grep -l "submit" {} ';'
  • Mover inicializaciones manejadas por sprockets en escuchadores de evento turbo:load a la nueva sección para esto en app/javascript/applicaiton.js.
  • Ejecutar en modo desarrollo, realizar pruebas de usuarios, asegurar que corren pruebas con sideex
  • Si es el caso ejecutar en modo de ensayo/producción

5.3 Pruebas al sistema con cuprite

  • En grupo de gemas :test agregar

      gem 'cuprite'
    
  • Crear directorio test/system

  • Crear archivo de configuración test/application_system_test_case con:

require "test_helper"                                                            
require "capybara/cuprite"                                                       
                                                                                 
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase                 
  Capybara.javascript_driver = :cuprite                                          
  Capybara.register_driver(:cuprite) do |app|                                    
    Capybara::Cuprite::Driver.new(app, window_size: [1200, 800])                 
  end                                                                            
                                                                                 
  driven_by :cuprite                                                             
end   
  • Crear una primera prueba por ejemplo de ingreso al sistema en test/system/iniciar_sesion_test.rb con:
require "application_system_test_case"                                           
                                                                                 
class IniciarSesionTest < ApplicationSystemTestCase                              
                                                                                 
  test "iniciar sesión" do                                                       
    Sip::CapybaraHelper.iniciar_sesion(                                          
      self, Rails.configuration.relative_url_root, 'usuario', 'clave')        
  end                                                                            
                                                                                 
end         
Clone this wiki locally