-
-
Notifications
You must be signed in to change notification settings - Fork 8
2019_11 Pasando de sprockets a webpacker con Rails 6
La función de webpacker y sprockets es la misma: preparar y desplegar junto con cada página HTML de una aplicación los recursos javascript, CSS e imagenes que se requieran.
La necesidad de cambiar de sprockets (o tubería de recursos de Rails) a webpacker, en lo que a Javascript respecta, se debe a los módulos Javascript que no pueden manejarse con sprockets sino con webpack que viene convirtiendose en la solución estándar para implementar módulos en navegadores .
sprockets y la tubería de recursos es histórica de Rails, pero webpacker
es una gema que usa la herramienta Javascript webpack
, que es muy común en el mundo Javascript por optimizar en tiempo de despliegue (o compilación) las diversas fuentes y módulos Javascript que requiera una aplicación y que realiza las traducciones necesarias.
Para evitar por un tiempo la migración a módulos en Javascript es posible seguir usando sprockets, e incluso hay un servicio gratuito que convierten en línea algunos paquetes para bower a gemas: https://rails-assets.org/. Sin embargo notamos que el equipo de desarrollo de Rails y en general las aplicaciones sobre Rails están migrando buena parte de sus Javascript a módulos Javascript que se distribuyen vía npm y webpacker y están reservando sprockets más para distribución de imágenes y hojas de estilo (aunque eso también lo podría hacer webpack).
A continuación incluimos explicaciones que pueden ayudar en el proceso. Recomendmos tambień otros recursos como:
Es un poco caótico debido a que hubo varias convenciones no oficiales hasta el 2015 (CommonJS y AMD), varias implementaciones y finalmente con ES2016 hubo una especificación estándar que ha sido paulatinamente adoptada por los navegadores. Por lo mismo en npm (repositorio oficial de paquetes Javascript) se encuentran paquetes que usan las diversas convenciones para módulos, por lo que antes de usar alguno debe analizarse si podrá ser cargado con webpack.
Además de esas dificultades, los navegadores han implementado el sistema de módulos estándar separado del ambiente global de ejecución Javascript, por lo que (1) aunque el navegador soporte módulos estándar desde la consola no puede usar import, (2) pueden ser dificiles de depurar o usar. Finalmente mezclar diversas formas de usar módulos puede generar fallas con webpack (como sus desarrolladores reconocen e.g https://github.com/webpack/webpack.js.org/issues/552 ), y hemos evidenciado diversas fallas al mezclar módulos manejados con webpacker y librerías Javascript manejadas con sprockets.
Como se explica en https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules en la especificación y convención actual los módulos deberían estar en archivos con extensión .js
o .mjs
y deberían ser servidos por el servidor web con el tipo MIME application/javascript
. Deben cargarse desde el HTML con <script type='module' src='modulo.mjs'>
y mediante un servidor (no pueden cargarse directo de un archivo del computador donde corre el navegador). Se ejecutarán después de que esté desplegado el HTML (así que no se necesita $.ready()
).
Un módulo puede exportar funciones así como variables (let
y var
), constantes (const
) y clases (class
) bien cuando se definen o bien posteriormente con
export {mivar, mifun, miconst, miclase}
Un módulo puede importar de otro módulo mediante
import {mivar, mifun, miconst, miclase} from './ruta/a/modulo'
Un módulo también puede exportar lo que importa de otros módulos.
Opcionalmente un módulo puede renombrar lo que exporta o lo que importa.
Son ideales los módulos que exportan una sola clase que encapsula todo lo necesario y que sirve como espacio de nombres.
Aún más reciente es la posibilidad de cargar módulos dinamicamente mediante la función import
, que retorna una promesa, cuyo uso, tomado de la misma referencia de MDN, se ejemplifica a continuación:
import('./modules/square.mjs').then((Module) => {
let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();
})
});
Es el sistema empleado por node.js donde los módulos se cargan de forma sincrona.
Para poderlo usar en un navegador, se requiere una herramienta que agrupe los diversos módulos en uno sólo. Justamente webpacker puede hacerlo, un ejemplo mínimo puede verse en https://github.com/webpack/webpack/tree/master/examples/commonjs
Los módulos se cargan de manera asincrona como es más típico en navegadores. Un ejemplo de una definición de un módulo:
define([], function() {
return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});
Y de su uso:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});
Permite definir módulos que se usen como CommonJS o como AMD para operar en clientes y en servidores.
Un ejemplo
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['myModule', 'myOtherModule'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('myModule'), require('myOtherModule'));
} else {
// Browser globals (Note: root is window)
root.returnExports = factory(root.myModule, root.myOtherModule);
}
}(this, function (myModule, myOtherModule) {
// Methods
function notHelloOrGoodbye(){}; // A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)
// Exposed public methods
return {
hello: hello,
goodbye: goodbye
}
}));
Permiten definir cómo módulos en el sentido de que aislan el espacio de nombres empleando el alcance local de una función.
function () {
// We keep these variables private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return 'Your average grade is ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return 'You failed ' + failingGrades.length + ' times.';
}
console.log(failing());
}());
// ‘You failed 2 times.’
(function (globalVariable) {
// Keep this variables private inside this closure scope
var privateFunction = function() {
console.log('Shhhh, this is private!');
}
// Expose the below methods via the globalVariable interface while
// hiding the implementation of the method within the
// function() block
globalVariable.each = function(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};
globalVariable.filter = function(collection, test) {
var filtered = [];
globalVariable.each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};
globalVariable.map = function(collection, iterator) {
var mapped = [];
globalUtils.each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
};
globalVariable.reduce = function(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;
globalVariable.each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});
return accumulator;
};
}(globalVariable));
var myGradesCalculate = (function () {
// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
// Expose these functions via an interface while hiding
// the implementation of the module within the function() block
return {
average: function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
},
failing: function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
}
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
var myGradesCalculate = (function () {
// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
};
var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
};
// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly
return {
average: average,
failing: failing
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
webpack
es una aplicación que compila módulos Javascript con las traducciones previas que cada uno requiera (por ejemplo Coffescript o de versiones más recientes de Javascript a unas más clásicas y soportadas) y que genera un archivo con todo incluido apto para transmitir al navegador.
webpacker
es una gema que maneja la configuración de webpack en una aplicación rails. Como convención por omisión configura webpack
para leer módulos Javascript de app/javascript/packs
y de node_modules
, funciones que deben ejecutarse en el ambiente global de app/javascript
y que escribe el archivo o archivos resultantes en public/packs/js/
(dejando un archivo de resumen public/packs/manifest.js
).
Por omisión webpacker
puede manejar ambientes de desarrollo, pruebas y producción y el modo de ejecución lo lee de la variable NODE_ENV
.
Provee ayudadores para incluir recursos compilados con webpack
en las vistas como javascript_pack_tag
, stylesheet_pack_tag
, favicon_pack_tag
, image_pack_tag
, los cuales generaran elementos HTML que referencian rutas de la forma /packs/
webpacker
configura webpack
con babel y cargadores de CSS que pueden convertir de SASS (por si se quisieran transmitir CSS con webpack
en lugar de sprockets).
Los paquetes npm deben estar en el directorio node_modules
y allí los pondrá yarn.
El soporte para webpacker en motores es apenas incipiente y el camino que han seguido los desarrolladores de rails, ha sido publicar paquetes npm como @rails/actioncable
, @rails/activestorage
, @rails/ujs
y @rails/webpacker
, que proceden de las gemas actioncable, activestorage, rails y webpacker respectivamente y por lo mismo cambian de versión de forma simultanea gemas y paquetes npm.
En Rails 6 la forma por omisión de emplear el editor enriquecido trix
es mediante webpacker. A continuación pasos para lograr su operación:
-
Asegure que webpacker y yarn operan. Como se describe en https://github.com/pasosdeJesus/sip/wiki/Actualizaci%C3%B3n-a-Rails-6-en-6-pasos:
doas pkg_add install bash ftp -o- https://yarnpkg.com/install.sh | bash . $HOME/.profile CXX=c++ bin/rails webpacker:install
Que agregará los siguientes directorios y archivos:
Archivo Descripción config/webpacker.yml Cofiguración de webpacker config/webpack/development.js Configuración de webpack en sitio de desarrolo config/webpack/environment.js Configuración de webpack común config/webpack/production.js Configuración de webpack en sitio de producción config/webpack/test.js Configuración de webpack en sitio de prueba postcss.config.js Configuración del cargador PostCSS babel.config.js Configuración de paquete babel usado para soportar ES-6 .browserslistrc Lista navegadores objetivo de la aplicación app/javascript Directorio donde pueden ubicar su aplicación JS o packs que requiera app/javascript/packs/application.js Referencia packs de webpacker por desplegar packages.json Declara paquetes javascript requeridos y versión de cada. Inicia con paquete webpacker
. Podrá actualizar versión de todos conCXX=c++ yarn upgrade
. Se agregan paquetes conyarn add paquete
y se eliminan conyarn remove paquete
node_modules Directorio donde quedan los paquetes JS declarados en packages.json
y descargados conCXX=c++ yarn install
-
Como se explica en las guías de rails (https://edgeguides.rubyonrails.org/action_text_overview.html), ejecute:
rails action_text:install
y corra migracionesbin/rails db:migrate
. Con esto se realizarán entre otros los siguientes cambios:- En
packages.json
al diccionario del atributodependencies
se añadirá"trix": "^1.0.0"
. Recuerde ejecutarCXX=c++ yarn install
que descargará la versión 1.0.0 ennode_modules/trix/
- En
app/javascript/packs/application.js
se agregará código para cargar módulos (en notación de CommonJS)require("trix") require("@rails/actiontext")
puede ver más sobre el sistema de módulos en ES6 en https://hacks.mozilla.org/2015/08/es6-in-depth-modules/ y sobre módulos com CommonJS en https://blog.risingstack.com/node-js-at-scale-module-system-commonjs-require/.
- Se creará el archivo
app/assets/stylesheets/actiontext.scss
que permite sobrecargar estilo por omisión detrix
. Que se carga mediante sprockets con:
- En
//= require trix/dist/trix
- Como el CSS por omisión del paquete Javascript
trix
, se carga consprockets
, es necesario agregar enconfig/initializers/assets.rb
:Rails.application.config.assets.paths << Rails.root.join('node_modules')
- En su archivo de layout, digamos
app/layouts/application.html.erb
recuerde cargar la aplicación javascript y los packs compilados con webpack con:
<%= javascript_pack_tag 'application' %>
- Si está montando la aplicación en una ruta diferente a
/
, recomendamos que aliste un enlacepacks
en su punto de montaje que apunte apublic/packs
. Por ejemplo si la monta enmiorg/miap
ejecute:
cd public/miorg/miapp
ln -s ../../packs .
Migrar una aplicación o un motor de sprockets a webpacker no es trivial, pues es importante (1) entender la operación de sprockets, de webpacker y de webpack (que a su vez requiere entender módulos de Javascript), (2) debe analizarse de forma particular cada gema con Javascript de la cual dependa la aplicación para realizar el cambio de la misma y (3) debe migrarse el Javascript de la aplicación a módulos.
Ver por ejemplo https://github.com/pasosdeJesus/sip/wiki/Actualizaci%C3%B3n-de-sip-2.0b6-a-sip-2.0b7
Tenga en cuenta que antes iniciar la aplicación en modo desarrollo, ahora es preferible ejecutar:
RAILS_ENV=development bin/rails assets:precompile --trace
de no hacerse es posible que la aplicación se congele al cargarla en modo desarrollo tipicamente después de presentar:
bin/rails s ...
...
formato_fecha:
yyyy-mm-dd
relative_url_root:
/
Nos ha ocurrido cuando hemos tenido problemas de dependencias, por ejemplo cuando en package.json
hemos agregado bootstrap
y boostrap-datepicker
pero en app/javascript/packs/application.js
hemos incluido import 'bootstrap-datepicker'
sin haber incluido antes import 'bootstrap'
.
En tales bloqueos no se logra detener con Control-C sino que debe matarse el proceso con señal 9. Por ejemplo puede buscar la instancia de puma que corre en puerto 2400 con
ps ax | grep 2400
y si el número del proceso es 54837 puede matarlo con:
kill -9 54837
De lo que hemos experimentando notamos que si javascript/packs/application.js
usa import
de ES6 la aplicación corre en el ambiente de ese archivo. Los script de los HTML se cargan en el mismo ambiente, así como los recursos servidos por sprockets.
Si se hace otro archivo javascript/packs/modulo2.js
por lo visto se carga en un ambiente diferente.
En general los nuevos Javascript evitan jQuery y es recomendable ir convirtiendo lo que falte a Javascript plano (también llamado Javascript vainilla).
En el transito, para tener disponible jQuery en el ambiente global (cargado con sprockets) y en todo módulo (cargado con webpacker) nos ha funcionado una solución que mezcla https://inopinatus.org/2019/09/14/webpacker-jquery-and-jquery-plugins/ con https://github.com/rails/webpacker/blob/master/docs/webpack.md:
$ yarn add expose-loader
$ CXX=c++ yarn install
Y en config/webpack/environment.js
:
const { environment } = require('@rails/webpacker')
...
const webpack = require('webpack')
environment.plugins.prepend(
'Provide',
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
jquery: 'jquery',
Popper: ['popper.js', 'default'],
})
)
environment.loaders.append('expose', {
test: require.resolve('jquery'),
use: [
{ loader: 'expose-loader', options: '$' },
{ loader: 'expose-loader', options: 'jQuery' }
]
})
...
module.exports = environment
Además de agregarlo con yarn add tinycolor2
exponerlo globalment desde config/webpack/environment.js
:
const webpack = require('webpack')
environment.plugins.prepend(
'Provide',
new webpack.ProvidePlugin({
...
tinycolor: 'tinycolor2'
})
)
environment.loaders.append('expose', {
test: require.resolve('jquery'),
use: [
... { loader: 'expose-loader', options: 'tinycolor' }
]
})
Desarrollado por Pasos de Jesús. Dominio público de acuerdo a legislación colombiana. Agradecemos financiación para personalizaciones de dominio público a diversas organizaciones, ver https://github.com/pasosdeJesus/sivel2/blob/master/CREDITOS.md
- Validación de etiquetas de Colombia y sus departamentos entre OSM de Sep.2022 y DIVIPOLA 2022
- Actualización a DIVIPOLA 2022-07 y Resumen ejecutivo de la actualización a DIVIPOLA 2022-07
- Actualización a DIVIPOLA 2021 y Resumen ejecutivo de la actualización a DIVIPOLA 2021
- Actualización a Rails 7
- Actualización a DIVIPOLA 2020 y Resumen ejecutivo de la actualización a DIVIPOLA 2020
- Extensiones para Chomium útiles para desarrollo
- Actualización de sip 2.0b11 a 2.0b12
- Actualización de sip 2.0b10 a 2.0b11
- Actualización de Rails 6.0 a Rails 6.1
- Resumen ejecutivo de la actualización a DIVIPOLA 2019
- Actualización a DIVIPOLA 2019
- Actualización-de-sip-2.0b6-a-sip-2.0b7
- Pasando de sprockets a webpacker con Rails 6
- Actualización a Rails 6 en 6 pasos
- Actualización a DIVIPOLA 2018
- Actualización de Rails 5.1 a Rails 5.2
- Actualizando a Rails 5
- Actualización a PostgreSQL posterior a 10.2