Skip to main content


Andrzej Osmialowski
Grunt

Farewell Assetic. Symfony Meets Modern Frontend Development Tools

Being a programmer who works mainly with Symfony taught me to use the best tools available – modern, well tested and widely used. I don’t like Assetic for many reasons, and I’ve decided to not touch it any more. In this article, I’ll illustrate how to replace Assetic with modern frontend tools: RequireJS, Grunt & Bower.

bower

  • Grunt – for automated tasks
  • Bower – for dependency management
  • RequireJS – for writing modular Javascript code (and manage modules dependencies).

Introduction

There are many advantages of using this approach:

  • frontend developers can use the tools of their choice
  • assets aren’t coupled with the core of the application (Assetic forces assets to be part of the bundle 1). You can use Git submodules or an independent repository for your assets.
  • frontend changes do not require application update 2
  • speeding up the process – Assetic is ridiculously slow in big projects (large amount of assets) especially in dev environment
  • modular Javascript code (thanks to RequireJS)
  • ease of use – the configuration requires one single command to
  • you stop using software that has no advantages.

If you’re still not convinced – ask yourself if you can find one real advantage of using Assetic, then continue reading.

Requirements

There are some requirements that needs that need to be met before processing.

Install Git

Bower requires Git. It will most likely be installed on your development environment server. It can be installed from the system repository (yum install git on RHEL based distros).

Install Node.js

This can be achieved in many ways:
+ installing from your system repository (yum install nodejs npm on RHEL based distros)
+ downloading Linux binaries from Node.js website
+ compiling from sources.

Install Bower

Bower is a package manager for frontend dependencies (libraries, frameworks, fonts & stuff).

After installing Node.js & npm you can install Bower simply by running the following command: npm install -g bower (this will install Bower globally into your system).

Install Grunt

Grunt is a Javascript task runner.

Grunt installation requires two steps. You need to install grunt-cli package globally and provide grunt as a dependency in your package.json file. At this step, just install it globally by running npm install -g grunt-cli command.

Configuration

After all required software has been installed, you can proceed to the next step.

Important! Before moving to the next step, you need to add node_modules directory to your .gitignore file as you don’t want to add all node.js modules to your repository.

At this stage, you need to prepare following files:

  • package.json – npm dependencies (most likely Grunt plugins)
  • bower.json – Bower dependencies
  • .bowerrc – Bower runtime configuration
  • .jshintrc – JSHint runtime configuration
  • Gruntfile.js – Grunt configuration.

.bowerrc

{
  "directory": "web/assets/vendor"
}

.jshintrc

{
  "bitwise": true,
  "browser": true,
  "curly": true,
  "eqeqeq": true,
  "eqnull": true,
  "esnext": true,
  "immed": true,
  "jquery": true,
  "latedef": true,
  "newcap": true,
  "noarg": true,
  "node": true,
  "strict": true,
  "trailing": true,
  "globals": {
    "define": true
  }
}

bower.json

{
  "name": "symfony-frontend",
  "version": "1.0.0",
  "authors": [
    "Andrzej Ośmiałowski <me@osmialowski.co.uk>"
  ],
  "license": "MIT",
  "private": true,
  "ignore": [
    "**/.*",
    "node_modules",
    "test",
    "tests"
  ],
  "dependencies": {
    "jquery": "~1.11.1",
    "bootstrap": "~3.3"
  }
}

package.json

{
  "name": "symfony-frontend",
  "version": "1.0.0",
  "private": true,
  "engines": {
    "node": ">= 0.10.0"
  },
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-autoprefixer": "^1.0.1",
    "grunt-contrib-concat": "^0.5.0",
    "grunt-contrib-jshint": "~0.10.0",
    "grunt-contrib-less": "~0.12.0",
    "grunt-contrib-requirejs": "~0.4.4",
    "grunt-contrib-symlink": "^0.3.0",
    "grunt-contrib-uglify": "~0.6.0",
    "grunt-contrib-watch": "~0.6.1"
  }
}

As soon as the package.json file is ready, you can install all Grunt plugins by running npm install command. All required modules should in installed into node_modules directory.

All bootstrap files have been created and it’s time to take care of the Gruntfile.js.

"use strict";

module.exports = function (grunt) {
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),

    appDir: 'web/assets',
    buildDir: 'web/assets-dist'
  });

  grunt.loadNpmTasks('grunt-autoprefixer');
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-less');
  grunt.loadNpmTasks('grunt-contrib-requirejs');
  grunt.loadNpmTasks('grunt-contrib-symlink');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-watch');

  grunt.registerTask('default', ['jshint', 'requirejs', 'less:dev', 'symlink', 'watch']);
  grunt.registerTask('dist', ['jshint', 'requirejs', 'uglify', 'less:dist', 'symlink']);
};

At this step, there are two path configuration variables, all required Grunt modules are loaded and two main tasks have been created. Two words about those tasks:

  • default – this is the default task and should be used at the development stage
  • dist – this task should be used to prepare production-ready assets.

Grunt plugins configuration:

RequireJS

requirejs: {
  main: {
    options: {
      mainConfigFile: '<%= appDir %>/js/main.js',
      appDir: '<%= appDir %>/js',
      baseUrl: '.',
      dir: '<%= buildDir %>/js',
      optimize: "none",
      modules: [
        {
          name: 'main',
          include: ['jquery', 'bootstrap']
        },
        {
          name: 'common',
          exclude: ['main']
        },
        {
          name: 'homepage',
          exclude: ['main']
        }
      ]
    }
  }
},

There are three modules configured:

  • main.js – the main RequireJS configuration file. Why are jQuery and Bootstrap included as dependencies for this module? Both libraries will be included into main.js file (in fact, this file is always used) and are excluded in other modules. Note: in fact, jQuery and Bootstrap are still dependencies for common and homepage modules but those files won’t be included into both modules’ output file.
  • common.js – module that contains code that’s being shared on all pages.
  • homepage.js – module for homepage.
main.js
requirejs.config({
  paths: {
    'jquery': '../vendor/jquery/dist/jquery'
    'bootstrap': '../vendor/bootstrap/dist/js/bootstrap'
  },
  shim: {
    'bootstrap': ['jquery']
  }
});
common.js
"use strict";

define(['jquery', 'bootstrap'], function ($) {
  // Set up Bootstrap tooltips
  $('[data-toggle="tooltip"]').tooltip();
});
homepage.js
"use strict";

define(['jquery'], function ($) {
  $('body').css('background', 'red');
});

JSHint

jshint: {
  options: {
    jshintrc: '.jshintrc'
  },
  all: [
    'Gruntfile.js',
    '<%= appDir %>/js/**/*.js'
  ]
}
symlink: {
  main: {
    files: [
      {
        expand: true,
        overwrite: false,
        cwd: '<%= appDir %>',
        src: ['img', 'vendor'], // img, fonts, etc.
        dest: '<%= buildDir %>'
      }
    ]
  }
}

Uglify

uglify: {
  options: {
    banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */n'
  },
  dist: {
    files: [{
      src: '<%= buildDir %>/js/**/*.js',
      dest: './',
      expand: true
    }]
  }
}

Less

less: {
  dev: {
    files: {
      '<%= buildDir %>/css/main.css': '<%= appDir %>/less/main.less'
    },
    options: {
      cleancss: false,
      compress: false,
      relativeUrls: true
    }
  },
  dist: {
    files: {
      '<%= buildDir %>/css/main.css': '<%= appDir %>/less/main.less'
    },
    options: {
      cleancss: true,
      compress: true,
      relativeUrls: true
    }
  }
}

Watch

watch: {
  config: {
    files: ['Gruntfile.js'],
    tasks: ['jshint', 'requirejs'],
    options: {
      reload: true,
      spawn: false
    }
  },
  scripts: {
    files: ['<%= appDir %>/js/**/*.js'],
    tasks: ['jshint', 'requirejs'],
    options: {
      spawn: false
    }
  },
  less: {
    files: [
      '<%= appDir %>/less/*.less',
      '<%= appDir %>/less/**/*.less'
    ],
    tasks: ['less:dev'],
    options: {
      spawn: false
    }
  }
}

Symfony configuration

There are two steps required to prepare Symfony to use the new approach: configure the assets path and prepare RequireJS view.

Add the assets variable to your Twig globals in app/config/config.yml file:

twig:
  globals:
    assets: "assets-dist"

Note: as you surely must have noticed, I use the same path for development and production assets (same goes for CSS/JS filenames). Why? Ease of use, as you provide only one configuration path and load only one file regardless of environment you are actually working with. It’s still flexible as you can customize this though by providing two separate variables in config_dev.yml & config_prod.yml files and modifying Gruntfile paths.

I store the RequireJS view in app/Resources/views/requirejs.html.twig file.

<script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.15/require.min.js"></script>
<script>
    requirejs.config({
        baseUrl: '//js'
    });

    require(['main'], function () {
        
            require(['common']);
        
    });
</script>

I think this code is quite self-explanatory and doesn’t need to be elaborated on.

I always define requirejs block in base.html.twig file:

{% block requirejs %}
    {{ include('requirejs.html.twig', { module: false }) }}
{% endblock requirejs %}

To use it in a single view, ex. AppBundle/Resources/views/Homepage/index.html.twig:

{% block requirejs %}
    {{ include('requirejs.html.twig', { module: 'homepage' }) }}
{% endblock requirejs %}

Everything is set up. You can now start writing your top-notch code and forget about Assetic 3.

I’ve prepared a test repository on Github.

  1. Visit this Stackoverflow question for more details.
  2. Imagine the following situation: you need to change a some code/fix some bugs in your LESS/SASS file. In the classic approach it would require updating the code, pushing the changes to the application repository (as assets are part of Symfony bundle), rebuilding & installing assets. In this approach, you only need to update assets, update your assets repository and update them in the application.
  3. Do not remove it thought as it’s still required for some bundles (ex: Web Debug Toolbar).

Article originally published on osmialowski.net

Zapisz

Zapisz

Share:Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on Pinterest

Like what you see?

Get in touch! We'll respond quickly, and we'll keep your data confidential.