Uno de los grandes “problemas” que nos encontramos en Elemento115 a la hora de hacer despliegues rápidos en un corto lapso de tiempo es, precisamente, cómo mostrar estos cambios al usuario final. Me explico.

Nuestro flujo de trabajo nos permite hacer cambios rápidos de diseño y funcionalidad en la parte cliente, por lo que podemos llegar a desplegar una aplicación bastantes veces al día. ¿Nuestra némesis? La caché. Ese elemento que ayuda tanto a que nuestros desarrollos carguen a una velocidad óptima también provoca que el usuario no vea los cambios de manera inmediata.

Para solucionarlo tenemos varias opciones, como dejar los archivos servidos expiren en tiempos relativamente cortos (en nuestro caso debería ser de horas) o forzar el de nuevo la bajada haciendo que expire manualmente. Ninguna de estas opciones nos convence. Hasta Symfony 2.8 utilizábamos Assetic para la gestión de assets. Este componente estaba integrado en Symfony, era fácil de utilizar y además nos solucionaba este problema con un sistema simple, el versionado de archivos. Pero, y siempre hay un pero, Assetic pasó a estar deprecado en Symfony 2.8.

 

Gulp al rescate.

¿Cómo afrontamos esto? Fácil, nos subimos al barco de Gulp. La integración con Symfony fue sencilla y además ganamos tiempo a la hora de hacer los despliegues gracias a que Gulp resultaba ser más rápido que Assetic a la hora de procesar los assets. Pero (otra vez) perdimos la facilidad con la que versionabamos los archivos, ya que con Assetic solo era necesario cambiar el número de versión en un archivo de configuración.

No íbamos a hacer de esto un drama, así que nos pusimos manos a la obra para solucionar el tema del versionado de archivos de la manera más limpia y automatizada posible. Queríamos que, cada vez que un archivo, ya sea CSS o JS, fuera modificado, se actualizase la versión de este para servir el nuevo contenido a los usuarios de manera inmediata. Para ello integramos gulp-rev, un paquete de Gulp que se encarga de añadir un hash a los archivos que necesitamos versionar. Un ejemplo práctico con archivos Sass/Css:


gulp.task('public-clean-assets', function() {
    return gulp.src(['web/public/css/*.css', 'web/public/**/*.js'], {read: false})
        .pipe(clean({force: true}));
});

gulp.task('public-sass', function() {
    return gulp.src('app/Resources/front/public/scss/*.scss')
        .pipe(sass({includePaths: ['app/Resources/front/public/scss/modules']}))
        .pipe(gulp.dest('app/Resources/front/public/css'))
        .pipe(notify({message: 'PUBLIC SASS OK', onLast: true}));
});
gulp.task('public-styles', ['public-clean-assets', 'public-sass'], function() {
    return gulp.src('app/Resources/front/public/css/**/*.css')
        .pipe(cleancss())
        .pipe(rename({suffix: '.min'}))
	.pipe(rev())
        .pipe(gulp.dest('web/public/css'))
	.pipe(rev.manifest('web/rev-manifest.json', {
            merge: true
        }))
	.pipe(gulp.dest('.'))
        .pipe(notify({message: 'PUBLIC CSS OK', onLast: true}));
});

Aquí actúan básicamente tres tareas de Gulp.

Vale, ya tenemos los archivos listos para servir, ¿como sabe Symfony qué archivos servir?

 

Servir la versión correcta con TwigExtensions.

Vamos a aprovechar el archivo rev-manifest.json para servir los assets correctos. En el archivo rev-manifest.json tenemos, por ejemplo, el siguiente contenido:


{
  "jquery-3-2-1.min.js": "jquery-3-2-1-c9f5aeeca3.min.js",
  "jquery-ui.min.js": "jquery-ui-fdf4d9013c.min.js",
  "main.min.css": "main-6ca53126cb.min.css",
  "main.min.js": "main-32fc16b6cd.min.js"
}

Aquí tenemos la relación entre el archivo que queremos utilizar (main.min.css por ejemplo) y el nombre versionado del archivo (main-32fc16b6cd.min.js). Para hacerlo de manera automática vamos a crear una extensión de Twig que realice este proceso por nosotros:



namespace AppBundle\Twig; 

/** 
 * AssetVersionExtension 
 */ 
class AssetVersionExtension extends \Twig_Extension 
{ 
    private $rootDir; 

    /** 
     * Constructor 
     * 
     * @param string $rootDir
     * @param string $cssDir
     * @param string $jsDir 
     * @param string $pluginsDir 
     */ 
    public function __construct($rootDir, $cssDir, $jsDir, $pluginsDir)
    { 
        $this->rootDir = $rootDir;
        $this->css = $cssDir;
        $this->js = $jsDir;
        $this->plugins = $pluginsDir;
    }

    /**
     * @return array
     */
    public function getFilters()
    {
        return array(
            new \Twig_SimpleFilter('assetVersion', [$this, 'getAssetVersion']),
        );
    }

    /**
     * Retrives the hashed version of the file
     *
     * @param string $filename
     * @param string $fileType  Should match js or css property
     * @param string $subfolder If the file is in a subdirectory it must by especified
     *
     * @return string
     */
    public function getAssetVersion($filename, $fileType, $subfolder = '')
    {
        $manifestPath = $this->rootDir . '/../web/rev-manifest.json';
        if (!file_exists($manifestPath)) {
            throw new \Exception(sprintf('Cannot find manifest file: "%s"', $manifestPath));
        }

        $paths = json_decode(file_get_contents($manifestPath), true);

        if (!isset($paths[$filename])) {
            throw new \Exception(sprintf('There is no file "%s" in the version manifest!', $filename));
        }

        return $this->{$fileType} . $subfolder .  $paths[$filename];
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'asset_version_extension';
    }
}

Declaración del servicio en services.yml:


...
app.twig.assets.version.extension:
    class: AppBundle\Twig\AssetVersionExtension
    arguments: ['%kernel.root_dir%', 'public/css/', 'public/js/', 'public/js/vendor/']
    tags:
        - { name: twig.extension }
...

¿Como funciona la extensión? Básicamente se le pasa como parámetros el nombre original del archivo y el tipo de archivo. Con esto, se encarga de leer desde el rev-manifest.json cual es el archivo correcto a utilizar.

Un ejemplo de uso:


...
< link rel="stylesheet" href="{{ asset('main.min.css'|assetVersion('css')) }}" />
...

Con esto, ya tenemos automatizado el versionado de los assets, además de servir siempre el archivo correspondiente sin necesidad de cambiar ningún tipo de configuración en los despliegues.

Un apunte sobre Gulp y la «sincronicidad».

Gulp, al ser una herramienta escrita puramente en JavaScript, ejecuta las tareas de manera completamente asíncrona. Debido a este comportamiento, es posible que intente hacer un versionado de archivos que aún no existen, por ejemplo cuando aún no se ha terminado de procesar los ficheros Sass.

Esto es fácil de corregir, podemos «forzar» a que Gulp actúe de manera síncrona. Tan solo es necesario pasar como segundo parámetro a la función gulp.task que lo requiera un array de tareas a las que debe esperar antes de ejecutarse. En el caso de la tarea public-styles, esta no se ejecuta hasta que public-clean-assets y public-sass han terminado.