Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Deck the halls with:
Grunt, RequireJS & Bower
by your friend:
!

Ryan Weaver
@weaverryan
Who is this jolly guy?
The “Docs” guy
!
!

KnpLabs US - Symfony consulting, training, Kumbaya
!

Writer for KnpUniversity....
Shout-out to the Docs team!

@weaverryan
“Hack” with us on Sat!

@weaverryan
!
!

Intro

Your friendly neighborhood
JavaScript developer is all
grown up… and kicking butt
No Node.js
!

5
years
ago
@weaverryan

Minifying and combining
done with a backend
solution
!

No RequireJS, AngularJS
!

...
Node.js for running
server-side JavaScript
!

RequireJS/AMD
!

Today

JavaScript task runners
(e.g. Grunt) for uglifying
a...
Your friend Pablo from
ServerGrove is
redeveloping the SG control
panel with a pure-JS
fronted

@weaverryan
Can we continue to use
JavaScript like we have
in the past?

@weaverryan
Maybe

@weaverryan
But we’re Symfony2
Developers…

@weaverryan
… with the highest
standards in PHP

@weaverryan
When we write JavaScript…

@weaverryan
Let’s write great JavaScript

@weaverryan
Our Goal
Take a traditional Symfony app
and make it much more jolly
by using Node.js, Bower,
RequireJS, Compass and Grunt
Node.js
!

it’s on your
Christmas list

http://www.flickr.com/photos/calsidyrose/4183559218/
The Project
* Symfony 2.3 - simple events website
!

* The code: http://bit.ly/sfcon-js-github

@weaverryan
base.html.twig
<head>	
{% block stylesheets %}	
{% stylesheets	
'bundles/event/css/event.css'	
'bundles/event/css/events.c...
base.html.twig
<body>	
{% block body %}{% endblock %}	
!

{% block javascripts %}	
{% javascripts	
'bundles/event/js/jquer...
Pages
* Homepage:
A) Has its own page-specific JS code
!

* New Event
A) Has its own page-specific JS code
B) Has page-spe...
!

Node.js and npm

!

Server-side JavaScript
Node.js
1) Executes JavaScript code
!

2) Adds extra functionality for using
JavaScript to deal with filesystems and other...
play.js
sys = require('sys');	
!

for (i=0; i<5; i++) {	
sys.puts(i);	
}

extra stuff added by Node.js
play.js
OMG!
http://www.flickr.com/photos/nocturnenoir/8305081285/
Node.js gives us the
ability to use JavaScript
as yet-another-serverside-scripting-language
npm
1) Composer for Node.js
2) Can be used to install things globally or
into your project (usually in a node_modules)
dir...
With Node.js and npm,
we can quickly create small
JavaScript files that use
external modules
With PHP and Composer,
we can quickly create small
PHP files that use
external libraries
Why should we care?
Fronted JavaScript library
build and development
tools are written in
JavaScript, executed with
Node.js
Bower
Composer-lite for client-side
JavaScript
Bower
(and one of those Node.js
libraries installed with npm)
Problem:
How do I get frontend libraries (e.g. jQuery,
Bootstrap) downloaded into my project?

http://www.flickr.com/photo...
Bower
1) Downloads frontend libraries (usually JS)
into a directory in your project
2) Reads from a bower.json file
3) Han...
Installation
!

sudo npm install -g bower

this means “install it globally” on your
machine - i.e. a bit like how Pear wor...
.bowerrc
Yo Bower, put the libraries over there:

{	
"directory": "web/assets/vendor"	
}
bower init
creates a bower.json file, with nothing
important in it

bower install bootstrap --save
Download the “bootstrap...
bower.json
{	
"dependencies": {	
"bootstrap": "~3.0.2"	
}	
}

** yes, this *is* composer.json for Bower
Now, how do we use these
files?
“Requiring” something in PHP

require 'Event.php';	
!

$event = new Event();	
echo $event->getName();
“Requiring” something in JS
<script type="text/javascript" 	
src="Event.js"></script>	
!

<script type="text/javascript">	...
Composer does 2 things:
1) Downloads libraries and
their dependencies
!

2) Sets up autoloading so
you don’t need “require...
Bower does 1 thing:
1) Downloads libraries and
their dependencies
!

2) Sets up autoloading so
you don’t need “require”
st...
before
<body>	
{% block body %}{% endblock %}	
!

{% block javascripts %}	
{% javascripts	
'bundles/event/js/jquery.min.js...
after
<body>	
{% block body %}{% endblock %}	
!

{% block javascripts %}	
{% javascripts	
'assets/vendor/jquery/jquery.min...
!

RequireJS

!

Autoloading for client-side
JavaScript
Problem:
Before we reference something in JavaScript, we
need to make sure it’s been included via a
<script> tag

http://w...
RequireJS
* A library that allows us to load JavaScript
resources without worrying about script tags
!

* Instead, we use ...
RequireJS works by requiring “modules”,
not files.

(though one file will contain one module)
Get it!

bower install requirejs --save
Remove all the JavaScript!
<body>	
{% block body %}{% endblock %}	
!

{% block javascripts %}	
{% javascripts	
'bundles/ev...
base.html.twig
<script	
src="{{ asset('assets/vendor/requirejs/require.js') }}">	

</script>	
!

<script>	
requirejs.confi...
base.html.twig
<script	

2) Configure RequireJS

src="{{ asset('assets/vendor/requirejs/require.js') }}">	

</script>	

Al...
base.html.twig
<script	

2) Configure RequireJS

src="{{ asset('assets/vendor/requirejs/require.js') }}">	

</script>	
!

...
base.html.twig
<script	

2) Configure RequireJS

src="{{ asset('assets/vendor/requirejs/require.js') }}">	

</script>	
!

...
app/homepage.js

define([], function() {	
console.log("It's alive!");	
});
But how does it work?
But how!
* All files are loaded by adding script tags
right into the HTML. But these use the async
tag, so do not block th...
now, what if we need jQuery?
Remember, jQuery isn’t even downloaded
yet - the global $ is not available

http://www.flickr.com/photos/danaberlith/42070...
app/homepage.js
define([], function() {	
$ = require('jquery');	
$('...');	
});
… it might look something like this?
app/homepage.js
define([], function() {	
$ = require('jquery');	
$('...');	
});
The require function *can’t* work like thi...
app/homepage.js
define(['jquery'], function ($) {	
$(document).ready(function() {	
$('a').on('click', function(e) {	
e.pre...
app/homepage.js
define(['jquery', 'bootstrap'], function ($,
Bootstrap) {	
$(document).ready(function() {	
$('a').on('clic...
base.html.twig
requirejs.config({	
baseUrl: '/assets/js',	
paths: {	
jquery: '../vendor/jquery/jquery.min',	
bootstrap: '....
Let’s create a new module (Love) that
takes down the grinch before he steals
Christmas.

http://en.wikipedia.org/wiki/File...
app/modules/love.js
define(['jquery', 'bootstrap'], function ($, Boots) {	

return {	
spiritOfXmas: function() {	
$('a').o...
app/modules/love.js
define(['jquery', 'bootstrap'], function ($, Boots) {	

return {	
spiritOfXmas: function() {	
$('a').o...
app/homepage.js
define(	
['jquery', 'app/modules/love'],	
function ($, Love) {	
!

$(document).ready(function() {	
Love.sp...
This takes care of bringing in JavaScript
for the homepage. But what about the
new event page?
1) Move the RequireJS code to a new template

::requirejs.html.twig
<script src="{{ asset('assets/vendor/requirejs/require...
2) Add a requirejs block to your <head>

::base.html.twig
{% block requirejs %}{% endblock %}
3) Include the module you need

EventBundle:Event:index.html.twig
{% block requirejs %}	
{{ include('::_requirejs.html.twi...
4) Repeat!

EventBundle:Event:new.html.twig
{% block requirejs %}	
{{ include('::_requirejs.html.twig', {	
'module': 'app/...
app/event_new.js

define(['jquery'], function ($) {	
!

$(document).ready(function() {	
// ...	
});	
});
Optimization
Combining JavaScript files
Problem:
Each module is loaded from an individual file
meaning there are lots of HTTP requests
Solution:
When we include the file containing moduleA,
let’s also packaged moduleB and moduleC in
there so when we need th...
Let’s start by creating a
common “module” that’s
always loaded
assets/js/common.js
requirejs.config({	
paths: {	
domReady: '../vendor/requirejs-domready/domReady',	

jquery: '../vendor/...
::requirejs.html.twig
<script src="{{ asset('/assets/vendor/
requirejs/require.js') }}"></script>	
!

<script>	
requirejs....
::requirejs.html.twig
<script src="{{ asset('/assets/vendor/
requirejs/require.js') }}"></script>	

Setup baseUrl so
<scri...
::requirejs.html.twig
<script src="{{ asset('/assets/vendor/
requirejs/require.js') }}"></script>	

Load common and
<scrip...
Why?
http://www.flickr.com/photos/danaberlith/4207059574
Because now we have a
module (common) that’s
*always* loaded
and we can use the
optimizer to “push” more
modules (e.g. bootstrap,
jquery) into it
Installing the Optimizer
* Optimization is a server-side JavaScript tool
!

* In other words it’s a node library installed...
Create an empty package.json

npm init
npm install requirejs --save-dev
package.json
{	
"devDependencies": {	
"requirejs": "~2.1.9"	
}	
}
and we also now have a
node_modules directory in our
project with requirejs in it

** We could have also installed it glob...
Configuration tells RequireJS how
to minify and combine files
build.js
({	

mainConfigFile: 'web/assets/js/common.js',	
baseUrl: './js',	
appDir: 'web/assets',	
dir: 'web/assets-built',	
module...
({	

mainConfigFile: 'web/assets/js/common.js',	
baseUrl: './js',	
appDir: 'web/assets',	
dir: 'web/assets-built',	
module...
({	

mainConfigFile: 'web/assets/js/common.js',	
These files are then re-written.
baseUrl: reads their
RequireJS './js',	 ...
({	

mainConfigFile: 'web/assets/js/common.js',	
baseUrl: './js',	
... plus we can manually include
appDir: 'web/assets',	...
({	

mainConfigFile: 'web/assets/js/common.js',	
baseUrl: './js',	
appDir: 'web/assets',	
dir: 'web/assets-built',	
module...
node node_modules/.bin/r.js -o build.js
Now, just point everything to
assets-built
{% set assetsPath = 'assets-built' %}	
!

<script src="{{ asset(assetsPath~'/vendor/
requirejs/require.js') }}"></script>	...
Not super dynamic yet... but it works!
assets-built is the same as assets
except when we include the common
module, it has jquery and bootstrap
packaged inside i...
Compass
Sass with style
Problem:
Static CSS files are *so* 2010

http://www.flickr.com/photos/stevendepolo/8409407391
Compass
* Processes sass files into CSS
!

* A sass “framework”: adds a lot of extra
functionality, including CSS3 mixins,...
Use Bower to bring in a sass Bootstrap

bower.json
{	
"dependencies": {	
"sass-bootstrap": "~3.0.0"	
"requirejs": "~2.1.9"...
bower install
Rename and reorganize CSS into SASS files

web/assets/sass/
* _base.scss
* _event.scss
* _events.scss
* event_form.scss
* ...
Rename and reorganize CSS into SASS files

web/assets/sass/
* _base.scss
* _event.scss
* _events.scss
* event_form.scss
* ...
Rename and reorganize CSS into SASS files

web/assets/sass/
* _base.scss
* _event.scss
* _events.scss
* event_form.scss
* ...
Rename and reorganize CSS into SASS files

web/assets/sass/
* _base.scss
* _event.scss
EventBundle:Event:new.html.twig
* _...
base.html.twig
{% block stylesheets %}	
<link rel="stylesheet"	

href="{{ asset('assets/css/layout.css') }}"/>	

{% endblo...
base.html.twig
{% block stylesheets %}	
<link rel="stylesheet"	

href="{{ asset('assets/css/layout.css') }}"/>	

{% endblo...
layout.scss
@import
@import
@import
@import

"base";	
"../vendor/sass-bootstrap/lib/bootstrap";	

"event";	
"events";	

!
...
layout.scss
@import
@import
@import
@import

"base";	
"../vendor/sass-bootstrap/lib/bootstrap";	

"event";	
"events";	

!
...
layout.scss
@import
@import
@import
@import

"base";	
"../vendor/sass-bootstrap/lib/bootstrap";	

"event";	
"events";	

!
...
event_form.scss

@import "base";	
!

/* for play, make the inputs super-rounded */	
.form-group input {	
@include border-r...
Now, just use more tools
sudo npm install -g compass
compass compile 
--css-dir=web/assets/css 
--sass-dir=web/assets/sass
“partials” (files beginning with “_”) are ignored
compass watch 
--css-dir=web/assets/css 
--sass-dir=web/assets/sass
watches for file changes and regenerates the
necessary CSS files
Grunt
app/console for JavaScript
Problem:
We have an increasing number of “build”
operations we need to run for JavaScript & CSS
1) Before deploy, run the ...
Install the Grunt executable
sudo npm install -g grunt-cli
package.json
{	
"devDependencies": {	
"requirejs": "~2.1.9",	
"grunt": "~0.4.2",	
"grunt-contrib-jshint": "~0.6.3",	
"grun...
Install Grunt into your
project + some modules
{	
"devDependencies": {	
"requirejs": "~2.1.9",	
"grunt": "~0.4.2",	
"grunt...
npm install
Grunt works by creating a
Gruntfile.js file, where we
define tasks (like app/console)
Gruntfile.js
module.exports = function (grunt) {	
grunt.initConfig({	
	
});	
!

grunt.loadNpmTasks('grunt-contrib-uglify')...
grunt -h

Eventually we can run grunt RequireJS
but we need to configure each command
Gruntfile.js

Use Grunt to run the RequireJS optimizer
Remove the RequireJS build.js and moves its
contents here
grunt.initConfig({	
appDir: 'web/assets',	
builtDir: 'web/assets-built',	
requirejs: {	
main: {	
options: {	
mainConfigFil...
grunt.initConfig({	
appDir: 'web/assets',	
builtDir: 'web/assets-built',	
We can setup
requirejs: {	
main: {	
variables an...
grunt.initConfig({	
appDir: 'web/assets',	
builtDir: 'web/assets-built',	
requirejs: {	 RequireJS *can* uglify CSS
and JS,...
grunt.initConfig({	
appDir: 'web/assets',	
builtDir: 'web/assets-built',	 We can
This is a sub-task.
requirejs: {	
now run...
Repeat for Compass!
compass: {	
dist: {	
options: {	
sassDir: '<%= builtDir %>/sass',	
cssDir: '<%= builtDir %>/css',	
environment: 'productio...
We have 2 sub-tasks:
1) compass:dist for deployment
2) compass:dev for development
Repeat for Uglify (to
minimize our JS files)!
** The RequireJS optimizer can uglify,
but using uglify directly gives us a ...
uglify: {	
build: {	
files: [	
{	
expand: true,	
cwd: '<%= builtDir %>',	
src: 'js/*.js',	
dest: '<%= builtDir %>'	
}	
]	
...
And even JsHint
jshint: {	
all: [	
'Gruntfile.js',	
'<%= appDir %>/js/{,*/}*.js'	
]	
},
Roll these up into some
grouped commands

http://www.flickr.com/photos/gazeronly/8206753938
// task for development	
grunt.registerTask('dev', [	
'jshint',	
'compass:dev'	
]);	
!

// task for before deployment	
gru...
!
!
!
!
!

1) syntax and style check our JS
2) copies assets to assets-dist and compiles
some files
3) uglifies all JS fil...
What about “watching”
watch: {	
scripts: {	
files: [	
'<%= appDir %>/js/*.js',	
// ...	
],	
tasks: ['jshint']	
},	
compass: {	
files: '<%= appDi...
assets versus assets-dist
How to handle in Symfony
Problem:
Grunt perfectly copies assets to assets-dist and
processes it. But how do we load our JS and
CSS files from the c...
Simple Solution
config.yml
parameters:	
assets_directory: 'assets'	
!

twig:	
# ...	
globals:	
assetsPath: %assets_directory%
config_prod.yml

parameters:	
assets_directory: 'assets-prod'
::requirejs.html.twig
<script src="{{ asset(assetsPath~'/vendor/
requirejs/require.js') }}"></script>	
<script>	
requirejs...
::base.html.twig

{% block stylesheets %}	
<link rel="stylesheet"	
href="{{ asset(assetsPath~'/css/layout.css') }}"/>	

{%...
Manual, but straightforward
If you wish, a fancier
solution exists, do it!
1) Extend the asset() function to change
the directory
!

2) Create a new T...
Bower downloads JS
dependencies
!

Modules included via
RequireJS

Now

!

Compass compiles our
SASS files
!

@weaverryan
...
When developing:
!

grunt watch
When deploying:
!

grunt production
and make your own Grunt
tasks for other processing
grunt.registerTask('symfonycon',
function() {	
sys = require('sys');	
sys.puts('Thanks everyone!');	
});
JavaScript is a first-class
tool in your stack

@weaverryan
Treat it with the same care
and quality as everything
else

@weaverryan
And (code) be cool like a
frontend developer

@weaverryan
Ho ho ho, thanks!
Brutal Feedback appreciated
https://joind.in/10372
The code:
http://bit.ly/sfcon-js-github

Keep learnin...
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Upcoming SlideShare
Loading in …5
×

Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools

Bower, Grunt, and RequireJS are just a few tools that have been re-shaping the frontend development world, replacing cluttered script tags and server-side build solutions with a sophisticated, but sometimes complex approach to dependency management and module loading. In this talk, we'll put on our trendy frontend developer hat and find out how these tools work and how they differ from what we might be used to. Most important, we'll see how using tools like this might look in Symfony2 and how our application can be a friendly place for a frontend guy/gal.

Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools

  1. 1. Deck the halls with: Grunt, RequireJS & Bower by your friend: ! Ryan Weaver @weaverryan
  2. 2. Who is this jolly guy? The “Docs” guy ! ! KnpLabs US - Symfony consulting, training, Kumbaya ! Writer for KnpUniversity.com screencasts Husband of the much more talented @leannapelham @weaverryan knplabs.com github.com/weaverryan
  3. 3. Shout-out to the Docs team! @weaverryan
  4. 4. “Hack” with us on Sat! @weaverryan
  5. 5. ! ! Intro Your friendly neighborhood JavaScript developer is all grown up… and kicking butt
  6. 6. No Node.js ! 5 years ago @weaverryan Minifying and combining done with a backend solution ! No RequireJS, AngularJS ! SASS/LESS were virtually non-existent
  7. 7. Node.js for running server-side JavaScript ! RequireJS/AMD ! Today JavaScript task runners (e.g. Grunt) for uglifying and much more ! SASS/LESS are very mature and can compile themselves @weaverryan
  8. 8. Your friend Pablo from ServerGrove is redeveloping the SG control panel with a pure-JS fronted @weaverryan
  9. 9. Can we continue to use JavaScript like we have in the past? @weaverryan
  10. 10. Maybe @weaverryan
  11. 11. But we’re Symfony2 Developers… @weaverryan
  12. 12. … with the highest standards in PHP @weaverryan
  13. 13. When we write JavaScript… @weaverryan
  14. 14. Let’s write great JavaScript @weaverryan
  15. 15. Our Goal Take a traditional Symfony app and make it much more jolly by using Node.js, Bower, RequireJS, Compass and Grunt
  16. 16. Node.js ! it’s on your Christmas list http://www.flickr.com/photos/calsidyrose/4183559218/
  17. 17. The Project * Symfony 2.3 - simple events website ! * The code: http://bit.ly/sfcon-js-github @weaverryan
  18. 18. base.html.twig <head> {% block stylesheets %} {% stylesheets 'bundles/event/css/event.css' 'bundles/event/css/events.css' 'bundles/event/css/main.css' 'assets/vendor/bootstrap/dist/css/bootstrap.css' 'assets/vendor/bootstrap/dist/css/bootstrap-theme.css' output='css/generated/layout.css' %} <link rel="stylesheet" href="{{ asset_url }}" /> {% endstylesheets %} {% endblock %} </head>
  19. 19. base.html.twig <body> {% block body %}{% endblock %} ! {% block javascripts %} {% javascripts 'bundles/event/js/jquery.min.js' 'bundles/event/js/bootstrap.js' output='js/generated/main.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} {% endblock %} </body>
  20. 20. Pages * Homepage: A) Has its own page-specific JS code ! * New Event A) Has its own page-specific JS code B) Has page-specific CSS (event_form.css) @weaverryan
  21. 21. ! Node.js and npm ! Server-side JavaScript
  22. 22. Node.js 1) Executes JavaScript code ! 2) Adds extra functionality for using JavaScript to deal with filesystems and other things that make sense on a server ! 3) Similar to the “php” executable that lets us interpret PHP code @weaverryan 3
  23. 23. play.js sys = require('sys'); ! for (i=0; i<5; i++) { sys.puts(i); } extra stuff added by Node.js
  24. 24. play.js
  25. 25. OMG! http://www.flickr.com/photos/nocturnenoir/8305081285/
  26. 26. Node.js gives us the ability to use JavaScript as yet-another-serverside-scripting-language
  27. 27. npm 1) Composer for Node.js 2) Can be used to install things globally or into your project (usually in a node_modules) directory 3) Reads dependencies from a package.json file @weaverryan 3
  28. 28. With Node.js and npm, we can quickly create small JavaScript files that use external modules
  29. 29. With PHP and Composer, we can quickly create small PHP files that use external libraries
  30. 30. Why should we care?
  31. 31. Fronted JavaScript library build and development tools are written in JavaScript, executed with Node.js
  32. 32. Bower Composer-lite for client-side JavaScript
  33. 33. Bower (and one of those Node.js libraries installed with npm)
  34. 34. Problem: How do I get frontend libraries (e.g. jQuery, Bootstrap) downloaded into my project? http://www.flickr.com/photos/perry-pics/5251240991/
  35. 35. Bower 1) Downloads frontend libraries (usually JS) into a directory in your project 2) Reads from a bower.json file 3) Handles dependencies! @weaverryan 3
  36. 36. Installation ! sudo npm install -g bower this means “install it globally” on your machine - i.e. a bit like how Pear works
  37. 37. .bowerrc Yo Bower, put the libraries over there: { "directory": "web/assets/vendor" }
  38. 38. bower init creates a bower.json file, with nothing important in it bower install bootstrap --save Download the “bootstrap” library and adds it as a dependency to bower.json
  39. 39. bower.json { "dependencies": { "bootstrap": "~3.0.2" } } ** yes, this *is* composer.json for Bower
  40. 40. Now, how do we use these files?
  41. 41. “Requiring” something in PHP require 'Event.php'; ! $event = new Event(); echo $event->getName();
  42. 42. “Requiring” something in JS <script type="text/javascript" src="Event.js"></script> ! <script type="text/javascript"> console.log(Event.someMethod()); </script>
  43. 43. Composer does 2 things: 1) Downloads libraries and their dependencies ! 2) Sets up autoloading so you don’t need “require” statements
  44. 44. Bower does 1 thing: 1) Downloads libraries and their dependencies ! 2) Sets up autoloading so you don’t need “require” statements
  45. 45. before <body> {% block body %}{% endblock %} ! {% block javascripts %} {% javascripts 'bundles/event/js/jquery.min.js' 'bundles/event/js/bootstrap.js' output='js/generated/main.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} {% endblock %} </body>
  46. 46. after <body> {% block body %}{% endblock %} ! {% block javascripts %} {% javascripts 'assets/vendor/jquery/jquery.min.js' 'assets/vendor/bootstrap/dist/js/bootstrap.js' output='js/generated/main.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} {% endblock %} </body>
  47. 47. ! RequireJS ! Autoloading for client-side JavaScript
  48. 48. Problem: Before we reference something in JavaScript, we need to make sure it’s been included via a <script> tag http://www.flickr.com/photos/sewtechnicolor/8213938281/
  49. 49. RequireJS * A library that allows us to load JavaScript resources without worrying about script tags ! * Instead, we use a require function, which is quite similar to the PHP require statement @weaverryan
  50. 50. RequireJS works by requiring “modules”, not files. (though one file will contain one module)
  51. 51. Get it! bower install requirejs --save
  52. 52. Remove all the JavaScript! <body> {% block body %}{% endblock %} ! {% block javascripts %} {% javascripts 'bundles/event/js/jquery.min.js' 'bundles/event/js/bootstrap.js' output='js/generated/main.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} {% endblock %} </body>
  53. 53. base.html.twig <script src="{{ asset('assets/vendor/requirejs/require.js') }}"> </script> ! <script> requirejs.config({ baseUrl: 'assets/js', paths: { jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' } }); 1) Bring in the require.js file downloaded via Bower using a normal script tag ! require(['app/homepage']); </script>
  54. 54. base.html.twig <script 2) Configure RequireJS src="{{ asset('assets/vendor/requirejs/require.js') }}"> </script> All modules live relative to this directory ! <script> requirejs.config({ baseUrl: '/assets/js', paths: { jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' } }); ! require(['app/homepage']); </script>
  55. 55. base.html.twig <script 2) Configure RequireJS src="{{ asset('assets/vendor/requirejs/require.js') }}"> </script> ! Exceptions: when I ask for “jquery” look for it here (relative to baseUrl), instead of assets/js/jquery <script> requirejs.config({ baseUrl: '/assets/js', paths: { jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' } }); ! require(['app/homepage']); </script>
  56. 56. base.html.twig <script 2) Configure RequireJS src="{{ asset('assets/vendor/requirejs/require.js') }}"> </script> ! Loads assets/js/app/homepage.js <script> requirejs.config({ baseUrl: '/assets/js', paths: { jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' } }); ! require(['app/homepage']); </script>
  57. 57. app/homepage.js define([], function() { console.log("It's alive!"); });
  58. 58. But how does it work?
  59. 59. But how! * All files are loaded by adding script tags right into the HTML. But these use the async tag, so do not block the page load. ! * You’ll commonly see a data-main attribute in setup. It loads that module. Our setup does the same thing. @weaverryan
  60. 60. now, what if we need jQuery?
  61. 61. Remember, jQuery isn’t even downloaded yet - the global $ is not available http://www.flickr.com/photos/danaberlith/4207059574
  62. 62. app/homepage.js define([], function() { $ = require('jquery'); $('...'); }); … it might look something like this?
  63. 63. app/homepage.js define([], function() { $ = require('jquery'); $('...'); }); The require function *can’t* work like this. ! Unlike PHP files, scripts need to be downloaded, which takes time. ! Our function can’t run until that happens
  64. 64. app/homepage.js define(['jquery'], function ($) { $(document).ready(function() { $('a').on('click', function(e) { e.preventDefault(); alert('Ah ah ah, you didn't say the magic word!'); }) }); }); Get the jquery module and *then* execute this function
  65. 65. app/homepage.js define(['jquery', 'bootstrap'], function ($, Bootstrap) { $(document).ready(function() { $('a').on('click', function(e) { e.preventDefault(); var $nope = $('<div>...</div>'); $nope.text('Ah ah ah, you didn't say the magic word!' ); $nope.modal(); }) }); Get the jquery and bootstrap modules }); and *then* execute this function
  66. 66. base.html.twig requirejs.config({ baseUrl: '/assets/js', paths: { jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' }, shim: { fixes an issue where bootstrap: ['jquery'] Bootstrap *needs* jQuery } before it’s downloaded }); shim is a way for you to configure libraries that aren’t proper RequireJS modules
  67. 67. Let’s create a new module (Love) that takes down the grinch before he steals Christmas. http://en.wikipedia.org/wiki/File:The_Grinch_(That_Stole_Christmas).jpg
  68. 68. app/modules/love.js define(['jquery', 'bootstrap'], function ($, Boots) { return { spiritOfXmas: function() { $('a').on('click', function(e) { e.preventDefault(); var $love = $('<div>...</div>'); $love.text('The Grinch’s heart grew three sizes that day' ); $nope.modal(); }); } } });
  69. 69. app/modules/love.js define(['jquery', 'bootstrap'], function ($, Boots) { return { spiritOfXmas: function() { $('a').on('click', function(e) { e.preventDefault(); var $love = $('<div>...</div>'); $love.text('The Grinch’s heart grew three sizes that day' ); $nope.modal(); }); } } }); Return some value (usually an object) that will be the app/modules/newman “module”
  70. 70. app/homepage.js define( ['jquery', 'app/modules/love'], function ($, Love) { ! $(document).ready(function() { Love.spiritOfXmas(); }); });
  71. 71. This takes care of bringing in JavaScript for the homepage. But what about the new event page?
  72. 72. 1) Move the RequireJS code to a new template ::requirejs.html.twig <script src="{{ asset('assets/vendor/requirejs/require.js') }}"></ script> <script> requirejs.config({ baseUrl: '/assets/js', paths: { domReady: '../vendor/requirejs-domready/domReady', jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' } // … }); ... and make the module a variable ! require(['{{ module }}']); </script>
  73. 73. 2) Add a requirejs block to your <head> ::base.html.twig {% block requirejs %}{% endblock %}
  74. 74. 3) Include the module you need EventBundle:Event:index.html.twig {% block requirejs %} {{ include('::_requirejs.html.twig', { 'module': 'app/homepage' }) }} {% endblock %}
  75. 75. 4) Repeat! EventBundle:Event:new.html.twig {% block requirejs %} {{ include('::_requirejs.html.twig', { 'module': 'app/event_new' }) }} {% endblock %}
  76. 76. app/event_new.js define(['jquery'], function ($) { ! $(document).ready(function() { // ... }); });
  77. 77. Optimization Combining JavaScript files
  78. 78. Problem: Each module is loaded from an individual file meaning there are lots of HTTP requests
  79. 79. Solution: When we include the file containing moduleA, let’s also packaged moduleB and moduleC in there so when we need them, we already have them.
  80. 80. Let’s start by creating a common “module” that’s always loaded
  81. 81. assets/js/common.js requirejs.config({ paths: { domReady: '../vendor/requirejs-domready/domReady', jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' }, shim: { bootstrap: ['jquery'] } });
  82. 82. ::requirejs.html.twig <script src="{{ asset('/assets/vendor/ requirejs/require.js') }}"></script> ! <script> requirejs.config({ baseUrl: '/assets/js' }); ! require(['common'], function (common) { require(['{{ module }}']); }); </script>
  83. 83. ::requirejs.html.twig <script src="{{ asset('/assets/vendor/ requirejs/require.js') }}"></script> Setup baseUrl so <script> we can reference requirejs.config({ the common baseUrl: '/assets/js' module below }); ! ! require(['common'], function (common) { require(['{{ module }}']); }); </script>
  84. 84. ::requirejs.html.twig <script src="{{ asset('/assets/vendor/ requirejs/require.js') }}"></script> Load common and <script> *then* load our requirejs.config({ real module baseUrl: '/assets/js' ! }); ! require(['common'], function (common) { require(['{{ module }}']); }); </script>
  85. 85. Why? http://www.flickr.com/photos/danaberlith/4207059574
  86. 86. Because now we have a module (common) that’s *always* loaded
  87. 87. and we can use the optimizer to “push” more modules (e.g. bootstrap, jquery) into it
  88. 88. Installing the Optimizer * Optimization is a server-side JavaScript tool ! * In other words it’s a node library installed via npm ! * We’ll install it into our project, by defining our project’s dependencies in package.json @weaverryan
  89. 89. Create an empty package.json npm init
  90. 90. npm install requirejs --save-dev
  91. 91. package.json { "devDependencies": { "requirejs": "~2.1.9" } }
  92. 92. and we also now have a node_modules directory in our project with requirejs in it ** We could have also installed it globally, like we did with Bower. All we really need is the r.js executable
  93. 93. Configuration tells RequireJS how to minify and combine files build.js
  94. 94. ({ mainConfigFile: 'web/assets/js/common.js', baseUrl: './js', appDir: 'web/assets', dir: 'web/assets-built', modules: [ { name: 'common', include: ['jquery', 'bootstrap'] }, { name: 'app/homepage', exclude: ['common'] } ] })
  95. 95. ({ mainConfigFile: 'web/assets/js/common.js', baseUrl: './js', appDir: 'web/assets', dir: 'web/assets-built', modules: web/assets directory is The entire [ { first copied to web/assets-built name: 'common', include: ['jquery', 'bootstrap'] }, { name: 'app/homepage', exclude: ['common'] } ] })
  96. 96. ({ mainConfigFile: 'web/assets/js/common.js', These files are then re-written. baseUrl: reads their RequireJS './js', dependencies appDir: 'web/assets', and includes them in the file dir: 'web/assets-built', automatically modules: [ { name: 'common', include: ['jquery', 'bootstrap'] }, { name: 'app/homepage', exclude: ['common'] } ] })
  97. 97. ({ mainConfigFile: 'web/assets/js/common.js', baseUrl: './js', ... plus we can manually include appDir: 'web/assets', dir: modules more 'web/assets-built', modules: [ { name: 'common', include: ['jquery', 'bootstrap'] }, { name: 'app/homepage', exclude: ['common'] } ] })
  98. 98. ({ mainConfigFile: 'web/assets/js/common.js', baseUrl: './js', appDir: 'web/assets', dir: 'web/assets-built', modules: [ { Avoids packaging name: 'common', jquery , bootstrap include: ['jquery', 'bootstrap'] into homepage since }, we now already { have it in common name: 'app/homepage', exclude: ['common'] } ] })
  99. 99. node node_modules/.bin/r.js -o build.js
  100. 100. Now, just point everything to assets-built
  101. 101. {% set assetsPath = 'assets-built' %} ! <script src="{{ asset(assetsPath~'/vendor/ requirejs/require.js') }}"></script> <script> ! requirejs.config({ baseUrl: '/{{ assetsPath }}/js' }); ! require(['common'], function (common) { require(['{{ module }}']); ! }); </script>
  102. 102. Not super dynamic yet... but it works!
  103. 103. assets-built is the same as assets except when we include the common module, it has jquery and bootstrap packaged inside it
  104. 104. Compass Sass with style
  105. 105. Problem: Static CSS files are *so* 2010 http://www.flickr.com/photos/stevendepolo/8409407391
  106. 106. Compass * Processes sass files into CSS ! * A sass “framework”: adds a lot of extra functionality, including CSS3 mixins, sprites, etc @weaverryan
  107. 107. Use Bower to bring in a sass Bootstrap bower.json { "dependencies": { "sass-bootstrap": "~3.0.0" "requirejs": "~2.1.9", } }
  108. 108. bower install
  109. 109. Rename and reorganize CSS into SASS files web/assets/sass/ * _base.scss * _event.scss * _events.scss * event_form.scss * layout.scss
  110. 110. Rename and reorganize CSS into SASS files web/assets/sass/ * _base.scss * _event.scss * _events.scss * event_form.scss * layout.scss (was event.css) (was events.css) (was main.css) ** these files were included on every page
  111. 111. Rename and reorganize CSS into SASS files web/assets/sass/ * _base.scss * _event.scss * _events.scss * event_form.scss * layout.scss (was event_form.css) ** included only on the “new event” page
  112. 112. Rename and reorganize CSS into SASS files web/assets/sass/ * _base.scss * _event.scss EventBundle:Event:new.html.twig * _events.scss * event_form.scss base.html.twig * layout.scss These are the only CSS files that will be included directly
  113. 113. base.html.twig {% block stylesheets %} <link rel="stylesheet" href="{{ asset('assets/css/layout.css') }}"/> {% endblock %} EventBundle:Event:new.html.twig {% block stylesheets %} {{ parent() }} ! <link rel="stylesheet" href="{{ asset('assets/css/event_form.css') }}"/> {% endblock %}
  114. 114. base.html.twig {% block stylesheets %} <link rel="stylesheet" href="{{ asset('assets/css/layout.css') }}"/> {% endblock %} EventBundle:Event:new.html.twig {% block stylesheets %} We link directly to non-existent {{ parent() }} ! CSS files, which we’ll generate <link rel="stylesheet" href="{{ asset('assets/css/event_form.css') }}"/> {% endblock %}
  115. 115. layout.scss @import @import @import @import "base"; "../vendor/sass-bootstrap/lib/bootstrap"; "event"; "events"; ! body { // ... }
  116. 116. layout.scss @import @import @import @import "base"; "../vendor/sass-bootstrap/lib/bootstrap"; "event"; "events"; ! body { // ... } Sets variables and imports mixins used in all SASS files
  117. 117. layout.scss @import @import @import @import "base"; "../vendor/sass-bootstrap/lib/bootstrap"; "event"; "events"; ! body { // ... } Import other files that contain actual CSS rules. These will eventually be concatenated into 1 file.
  118. 118. event_form.scss @import "base"; ! /* for play, make the inputs super-rounded */ .form-group input { @include border-radius(20px, 20px); }
  119. 119. Now, just use more tools
  120. 120. sudo npm install -g compass
  121. 121. compass compile --css-dir=web/assets/css --sass-dir=web/assets/sass
  122. 122. “partials” (files beginning with “_”) are ignored
  123. 123. compass watch --css-dir=web/assets/css --sass-dir=web/assets/sass
  124. 124. watches for file changes and regenerates the necessary CSS files
  125. 125. Grunt app/console for JavaScript
  126. 126. Problem: We have an increasing number of “build” operations we need to run for JavaScript & CSS 1) Before deploy, run the RequireJS optimizer 2) Before deploy, run Compass 3) During development, watch Compass
  127. 127. Install the Grunt executable sudo npm install -g grunt-cli
  128. 128. package.json { "devDependencies": { "requirejs": "~2.1.9", "grunt": "~0.4.2", "grunt-contrib-jshint": "~0.6.3", "grunt-contrib-uglify": "~0.2.2", "grunt-contrib-requirejs": "~0.4.1", "grunt-contrib-compass": "~0.6.0", "grunt-contrib-watch": "~0.5.3" } }
  129. 129. Install Grunt into your project + some modules { "devDependencies": { "requirejs": "~2.1.9", "grunt": "~0.4.2", "grunt-contrib-jshint": "~0.6.3", "grunt-contrib-uglify": "~0.2.2", "grunt-contrib-requirejs": "~0.4.1", "grunt-contrib-compass": "~0.6.0", "grunt-contrib-watch": "~0.5.3" } }
  130. 130. npm install
  131. 131. Grunt works by creating a Gruntfile.js file, where we define tasks (like app/console)
  132. 132. Gruntfile.js module.exports = function (grunt) { grunt.initConfig({ }); ! grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-requirejs'); grunt.loadNpmTasks('grunt-contrib-compass'); grunt.loadNpmTasks('grunt-contrib-watch'); };
  133. 133. grunt -h Eventually we can run grunt RequireJS but we need to configure each command
  134. 134. Gruntfile.js Use Grunt to run the RequireJS optimizer Remove the RequireJS build.js and moves its contents here
  135. 135. grunt.initConfig({ appDir: 'web/assets', builtDir: 'web/assets-built', requirejs: { main: { options: { mainConfigFile: '<%= appDir %>/js/ common.js', appDir: '<%= appDir %>', baseUrl: './js', dir: '<%= builtDir %>', optimizeCss: "none", optimize: "none", modules: [... same as build.js ...] } }
  136. 136. grunt.initConfig({ appDir: 'web/assets', builtDir: 'web/assets-built', We can setup requirejs: { main: { variables and use options: { them mainConfigFile: '<%= appDir %>/js/ common.js', appDir: '<%= appDir %>', baseUrl: './js', dir: '<%= builtDir %>', optimizeCss: "none", optimize: "none", modules: [... same as build.js ...] } }
  137. 137. grunt.initConfig({ appDir: 'web/assets', builtDir: 'web/assets-built', requirejs: { RequireJS *can* uglify CSS and JS, but we’ll leave this main: { options: { to Uglify and Compass mainConfigFile: '<%= appDir %>/js/ common.js', appDir: '<%= appDir %>', baseUrl: './js', dir: '<%= builtDir %>', optimizeCss: "none", optimize: "none", modules: [... same as build.js ...] } }
  138. 138. grunt.initConfig({ appDir: 'web/assets', builtDir: 'web/assets-built', We can This is a sub-task. requirejs: { now run grunt requirejs:main main: { or grunt requirejs to run all options: { sub-tasks '<%= appDir %>/js/ mainConfigFile: common.js', appDir: '<%= appDir %>', baseUrl: './js', dir: '<%= builtDir %>', optimizeCss: "none", optimize: "none", modules: [... same as build.js ...] } }
  139. 139. Repeat for Compass!
  140. 140. compass: { dist: { options: { sassDir: '<%= builtDir %>/sass', cssDir: '<%= builtDir %>/css', environment: 'production', outputStyle: 'compressed' } }, dev: { options: { sassDir: '<%= appDir %>/sass', cssDir: '<%= appDir %>/css', outputStyle: 'expanded' } }
  141. 141. We have 2 sub-tasks: 1) compass:dist for deployment 2) compass:dev for development
  142. 142. Repeat for Uglify (to minimize our JS files)! ** The RequireJS optimizer can uglify, but using uglify directly gives us a bit more control
  143. 143. uglify: { build: { files: [ { expand: true, cwd: '<%= builtDir %>', src: 'js/*.js', dest: '<%= builtDir %>' } ] } },
  144. 144. And even JsHint
  145. 145. jshint: { all: [ 'Gruntfile.js', '<%= appDir %>/js/{,*/}*.js' ] },
  146. 146. Roll these up into some grouped commands http://www.flickr.com/photos/gazeronly/8206753938
  147. 147. // task for development grunt.registerTask('dev', [ 'jshint', 'compass:dev' ]); ! // task for before deployment grunt.registerTask('production', [ 'jshint', 'requirejs', 'uglify', 'compass:dist' ]);
  148. 148. ! ! ! ! ! 1) syntax and style check our JS 2) copies assets to assets-dist and compiles some files 3) uglifies all JS files in assets-dist 4) compiles all assets-dist/sass files ! // task for before deployment grunt.registerTask('production', [ 1) 'jshint', 2)'requirejs', 3)'uglify', 'compass:dist' 4) ]);
  149. 149. What about “watching”
  150. 150. watch: { scripts: { files: [ '<%= appDir %>/js/*.js', // ... ], tasks: ['jshint'] }, compass: { files: '<%= appDir %>/sass/*.scss', tasks: ['compass:dev'] } }
  151. 151. assets versus assets-dist How to handle in Symfony
  152. 152. Problem: Grunt perfectly copies assets to assets-dist and processes it. But how do we load our JS and CSS files from the correct directory?
  153. 153. Simple Solution
  154. 154. config.yml parameters: assets_directory: 'assets' ! twig: # ... globals: assetsPath: %assets_directory%
  155. 155. config_prod.yml parameters: assets_directory: 'assets-prod'
  156. 156. ::requirejs.html.twig <script src="{{ asset(assetsPath~'/vendor/ requirejs/require.js') }}"></script> <script> requirejs.config({ baseUrl: '/{{ assetsPath }}/js' }); ! // ... </script>
  157. 157. ::base.html.twig {% block stylesheets %} <link rel="stylesheet" href="{{ asset(assetsPath~'/css/layout.css') }}"/> {% endblock %}
  158. 158. Manual, but straightforward
  159. 159. If you wish, a fancier solution exists, do it! 1) Extend the asset() function to change the directory ! 2) Create a new Twig function to replace asset()
  160. 160. Bower downloads JS dependencies ! Modules included via RequireJS Now ! Compass compiles our SASS files ! @weaverryan Grunt optimizes for RequireJS, Uglifies, runs Compass, and watches for changes
  161. 161. When developing: ! grunt watch
  162. 162. When deploying: ! grunt production
  163. 163. and make your own Grunt tasks for other processing
  164. 164. grunt.registerTask('symfonycon', function() { sys = require('sys'); sys.puts('Thanks everyone!'); });
  165. 165. JavaScript is a first-class tool in your stack @weaverryan
  166. 166. Treat it with the same care and quality as everything else @weaverryan
  167. 167. And (code) be cool like a frontend developer @weaverryan
  168. 168. Ho ho ho, thanks! Brutal Feedback appreciated https://joind.in/10372 The code: http://bit.ly/sfcon-js-github Keep learning: KnpUniversity.com @weaverryan

×