I will explain how to work with assets in Symfony framework without having to use Assetic Bundle at all.
We will build a stack that will
- download and prepare dependencies (jQuery, Bootstrap and Font Awesome icons)
- merge and minify css and javascript files
- copy necessary fonts (font-awesome) in the right place so the path in css is correct
- automate deploy to S3 bucket which you can optionally convert to CDN
The process will be really fast and easy to understand even if you never used software listed here. However if you experience any problems do not hesitate to ask for help in comments. Post is quite long because it contain a lot of different configs but don't run away just yet. They are ready to copy & paste.
Also ... I'm working with Symfony here but you can use it for literally any other web framework.
Table of contents
- Introduction
- Prerequisites
- Use cases
- Scenario - download dependencies and copy them to
/web/assets/*
dir- Scenario - download dependencies, copy and minify them
- Scenario - download, copy, concat (merge) and minify
- Deploying to S3/CDN - all above plus automated deploy to the CDN
Introduction
Why you would want to do it?
- assets are generated on your computer once, so the server doesn't have to do anything (less software on server = the better)
- if you are (or will be) using load balancer you definitely should (actually must) keep assets on CDN
- one less bundle to load in your Symfony application (Assetic)
What we will use? NodeJS, Bower, Grunt JS and some grunt tasks
Before we begin please add these to your .gitignore
file, you don't want to keep vendor libraries in your repository.
bower_components/
node_modules/
Install NodeJS
NodeJS is a runtime platform for applications written in javascript, it's required by Bower and GruntJs we will install in a moment.
Those who already have node js installed can go to the next step
Installing Bower
Bower is like a composer for frontend libraries. If you like composer you will definitely like Bower.
npm install -g bower
Create file bower.json
, it will hold information about all required dependencies.
{
"name": "symfony-application",
"dependencies": {
"jquery": "1.11.*",
"bootstrap": "3.1.*",
"font-awesome": "4.1.*"
}
}
run bower install
bower install
Folder structure will look like this
.
├── bower_components
│ ├── bootstrap
│ ├── font-awesome
│ └── jquery
├── bower.json
└── .gitignore
Installing Grunt JS
GruntJS is our task runner, by adding a set of tasks created by Grunt community we can automate many tasks we used to do manually with a very little effort.
To install it we will once again use node package manager, npm:
npm install -g grunt-cli
Create file package.json
, it will hold all dependencies for grunt tasks
{
"name": "symfony-application",
"version": "0.1.0"
}
Run the following commands (--save-dev
option will add it to package.json
)
npm install grunt --save-dev
npm install grunt-bowercopy --save-dev
npm install grunt-contrib-clean --save-dev
npm install grunt-contrib-concat --save-dev
npm install grunt-contrib-copy --save-dev
npm install grunt-contrib-cssmin --save-dev
npm install grunt-contrib-uglify --save-de
npm install grunt-contrib-watch --save-dev
Now package.json
should look like this.
{
"name": "symfony-application",
"version": "0.1.0",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-bowercopy": "^1.0.1",
"grunt-contrib-clean": "^0.5.0",
"grunt-contrib-concat": "^0.4.0",
"grunt-contrib-copy": "^0.5.0",
"grunt-contrib-cssmin": "^0.10.0",
"grunt-contrib-uglify": "^0.5.0",
"grunt-contrib-watch": "^0.6.1"
}
}
Once package.json
is updated in the future you can use simple npm install
to install grunt dependencies
Now when all dependencies are ready we can configure how we want assets to be processed.
Scenario 1
Download latest jQuery, Bootstrap, Font Awesome with Bower and copy the only necessary files to web/assets/*
Create file Gruntfile.js
, it will contain all configurations
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
bowercopy: {
options: {
srcPrefix: 'bower_components',
destPrefix: 'web/assets'
},
scripts: {
files: {
'js/jquery.js': 'jquery/dist/jquery.js',
'js/bootstrap.js': 'bootstrap/dist/js/bootstrap.js'
}
},
stylesheets: {
files: {
'css/bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
'css/font-awesome.css': 'font-awesome/css/font-awesome.css'
}
},
fonts: {
files: {
'fonts': 'font-awesome/fonts'
}
}
}
});
grunt.loadNpmTasks('grunt-bowercopy');
grunt.registerTask('default', ['bowercopy']);
};
Run grunt with simple
grunt
It will fetch latest dependencies with Bower and copy them to desired locations
web
└── assets
├── css
│ ├── bootstrap.css
│ └── font-awesome.css
├── fonts
│ ├── FontAwesome.otf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ └── fontawesome-webfont.woff
└── js
├── bootstrap.js
└── jquery.js
Scenario 2
Download dependencies with Bower, copy necessary files to web/assets/*
. Then minify javascript and stylesheet files.
Although most frontend libraries comes with both normal and minified versions we will do it for the sake of learning.
Update Gruntfile.js
you created in previous scenario with configuration for cssmin
and uglify
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
bowercopy: {
options: {
srcPrefix: 'bower_components',
destPrefix: 'web/assets'
},
scripts: {
files: {
'js/jquery.js': 'jquery/dist/jquery.js',
'js/bootstrap.js': 'bootstrap/dist/js/bootstrap.js'
}
},
stylesheets: {
files: {
'css/bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
'css/font-awesome.css': 'font-awesome/css/font-awesome.css'
}
},
fonts: {
files: {
'fonts': 'font-awesome/fonts'
}
}
},
cssmin : {
bootstrap:{
src: 'web/assets/css/bootstrap.css',
dest: 'web/assets/css/bootstrap.min.css'
},
"font-awesome":{
src: 'web/assets/css/font-awesome.css',
dest: 'web/assets/css/font-awesome.min.css'
}
},
uglify : {
js: {
files: {
'web/assets/js/jquery.min.js': ['web/assets/js/jquery.js'],
'web/assets/js/bootstrap.min.js': ['web/assets/js/bootstrap.js']
}
}
}
});
grunt.loadNpmTasks('grunt-bowercopy');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['bowercopy', 'cssmin', 'uglify']);
};
Run grunt by typing grunt
and the the structure should look like this
web
└── assets
├── css
│ ├── bootstrap.css
│ ├── bootstrap.min.css
│ ├── font-awesome.css
│ └── font-awesome.min.css
├── fonts
│ ├── FontAwesome.otf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ └── fontawesome-webfont.woff
└── js
├── bootstrap.js
├── bootstrap.min.js
├── jquery.js
└── jquery.min.js
Scenario 3
Download dependencies with Bower, merge them with your custom css and js files, then minify.
Assume we have the following structure
src
└── KP
└── LearningBundle
└── Resources
└── public
├── css
│ └── main.css
├── images
│ └── no-photo.gif
└── js
├── editor.js
└── notification.js
First task we configure will be copy
, it will copy image(s) to web/assets/images/*
directory.
Second task we will add is concat
, it will merge all scripts into single file which we will later uglify
to make it smaller
Same thing for stylesheet file
Updated Gruntfile.js
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
bowercopy: {
options: {
srcPrefix: 'bower_components',
destPrefix: 'web/assets'
},
scripts: {
files: {
'js/jquery.js': 'jquery/dist/jquery.js',
'js/bootstrap.js': 'bootstrap/dist/js/bootstrap.js'
}
},
stylesheets: {
files: {
'css/bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
'css/font-awesome.css': 'font-awesome/css/font-awesome.css'
}
},
fonts: {
files: {
'fonts': 'font-awesome/fonts'
}
}
},
cssmin : {
bundled:{
src: 'web/assets/css/bundled.css',
dest: 'web/assets/css/bundled.min.css'
}
},
uglify : {
js: {
files: {
'web/assets/js/bundled.min.js': ['web/assets/js/bundled.js']
}
}
},
concat: {
options: {
stripBanners: true
},
css: {
src: [
'web/assets/css/bootstrap.css',
'web/assets/css/font-awesome.css',
'src/KP/LearningBundle/Resources/public/css/*.css'
],
dest: 'web/assets/css/bundled.css'
},
js : {
src : [
'web/assets/js/jquery.js',
'web/assets/js/bootstrap.js',
'src/KP/LearningBundle/Resources/public/js/*.js'
],
dest: 'web/assets/js/bundled.js'
}
},
copy: {
images: {
expand: true,
cwd: 'src/KP/LearningBundle/Resources/public/images',
src: '*',
dest: 'web/assets/images/'
}
}
});
grunt.loadNpmTasks('grunt-bowercopy');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['bowercopy','copy', 'concat', 'cssmin', 'uglify']);
};
Deploy to S3/CDN
Cloudfront is one of the most popular and in my opinion one of the easiest to begin with cdn servers Uploading assets to S3 storage and setting it as a source for CDN is enough to get started.
S3 Integration is handled by another grunt task we will add to our stack
npm install grunt-s3 --save-dev
You can find the full documentation on the official repository https://github.com/pifantastic/grunt-s3
We will only use upload feature.
Create a new file where you will store credentials to the S3 bucket. Lets call it aws-credentials.json
Don't forget to add it to .gitignore
, last thing you want to do is to push your key/secret to the repository.
# .gitignore
bower_components/
node_modules/
aws-credentials.json
and the content of aws-credentials.json
should be like this
{
"key": "your_aws_key",
"secret": "your_aws_secret",
"bucket": "name_of_your_bucket"
}
Updated Gruntfile.js
with grunt-s3
task
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
bowercopy: {
options: {
srcPrefix: 'bower_components',
destPrefix: 'web/assets'
},
scripts: {
files: {
'js/jquery.js': 'jquery/dist/jquery.js',
'js/bootstrap.js': 'bootstrap/dist/js/bootstrap.js'
}
},
stylesheets: {
files: {
'css/bootstrap.css': 'bootstrap/dist/css/bootstrap.css',
'css/font-awesome.css': 'font-awesome/css/font-awesome.css'
}
},
fonts: {
files: {
'fonts': 'font-awesome/fonts'
}
}
},
cssmin : {
bundled:{
src: 'web/assets/css/bundled.css',
dest: 'web/assets/css/bundled.min.css'
}
},
uglify : {
js: {
files: {
'web/assets/js/bundled.min.js': ['web/assets/js/bundled.js']
}
}
},
concat: {
options: {
stripBanners: true
},
css: {
src: [
'web/assets/css/bootstrap.css',
'web/assets/css/font-awesome.css',
'src/KP/LearningBundle/Resources/public/css/*.css'
],
dest: 'web/assets/css/bundled.css'
},
js : {
src : [
'web/assets/js/jquery.js',
'web/assets/js/bootstrap.js',
'src/KP/LearningBundle/Resources/public/js/*.js'
],
dest: 'web/assets/js/bundled.js'
}
},
copy: {
images: {
expand: true,
cwd: 'src/KP/LearningBundle/Resources/public/images',
src: '*',
dest: 'web/assets/images/'
}
},
aws: grunt.file.readJSON('aws-credentials.json'),
s3: {
options: {
key: '<%= aws.key %>',
secret: '<%= aws.secret %>',
bucket: '<%= aws.bucket %>'
},
cdn: {
upload: [
{
src: 'web/assets/css/*',
dest: 'css/'
},
{
src: 'web/assets/fonts/*',
dest: 'fonts/'
},
{
src: 'web/assets/images/*',
dest: 'images/'
},
{
src: 'web/assets/js/*',
dest: 'js/'
}
]
}
}
});
grunt.loadNpmTasks('grunt-bowercopy');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-s3');
grunt.registerTask('default', ['bowercopy','copy', 'concat', 'cssmin', 'uglify']);
grunt.registerTask('deploy', ['s3']);
};
Now you can generate assets like you did until now with command grunt
, but also deploy them directly to the CDN with command grunt:deploy
.
grunt
grunt:deploy
What next?
Article is already pretty long and I still didn't cover everything I wanted. There will be another part of this article that will cover
- Working with less/sass
- Automated cleaning legacy assets
- Watching for changes in assets
- Better explain how to correctly configure S3 bucket to act as a CDN
- How to use Require JS (the most interesting topic)
- Disabling Assetic Bundle from Symfony application