Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
-
Upload
ryan-weaver -
Category
Technology
-
view
29.566 -
download
0
description
Transcript of Cool like a Frontend Developer: Grunt, RequireJS, Bower and other Tools
Deck the halls with: Grunt, RequireJS & Bower
by your friend: !
Ryan Weaver @weaverryan
The “Docs” guy !!KnpLabs US - Symfony consulting, training, Kumbaya !
Writer for KnpUniversity.com screencasts
knplabs.com github.com/weaverryan@weaverryan
Husband of the much more talented @leannapelham
Who is this jolly guy?
@weaverryan
Shout-out to the Docs team!
“Hack” with us on Sat!
@weaverryan
!
!
Your friendly neighborhood JavaScript developer is all grown up… and kicking butt
Intro
No Node.js !
Minifying and combining done with a backend solution !
No RequireJS, AngularJS !
SASS/LESS were virtually non-existent
@weaverryan
5 years ago
Node.js for running server-side JavaScript !
RequireJS/AMD !
JavaScript task runners (e.g. Grunt) for uglifying and much more !
SASS/LESS are very mature and can compile themselves
@weaverryan
Today
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
http://www.flickr.com/photos/calsidyrose/4183559218/
Node.js !
it’s on your Christmas list
@weaverryan
* Symfony 2.3 - simple events website !
* The code: http://bit.ly/sfcon-js-github
The Project
<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>
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>
base.html.twig
@weaverryan
* 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)
Pages
!
!
Server-side JavaScript
Node.js and npm
@weaverryan
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
3
Node.js
sys = require('sys');!
for (i=0; i<5; i++) { sys.puts(i);}
extra stuff added by Node.js
play.js
play.js
OMG!
http://www.flickr.com/photos/nocturnenoir/8305081285/
Node.js gives us the ability to use JavaScript as yet-another-server-side-scripting-language
@weaverryan
1) Composer for Node.js
3
npm
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
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/photos/perry-pics/5251240991/
@weaverryan
1) Downloads frontend libraries (usually JS)
into a directory in your project
3
Bower
2) Reads from a bower.json file
3) Handles dependencies!
Installation !
sudo npm install -g bower
this means “install it globally” on your machine - i.e. a bit like how Pear works
.bowerrc
Yo Bower, put the libraries over there:
{ "directory": "web/assets/vendor"}
bower init
bower install bootstrap --save
creates a bower.json file, with nothing important in it
Download the “bootstrap” library and adds it as a dependency to bower.json
{ "dependencies": { "bootstrap": "~3.0.2" }}
bower.json
** 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"> console.log(Event.someMethod());</script>
Composer does 2 things:
1) Downloads libraries and their dependencies !
2) Sets up autoloading so you don’t need “require” statements
Bower does 1 thing:
1) Downloads libraries and their dependencies !
2) Sets up autoloading so you don’t need “require” statements
<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>
before
<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>
after
RequireJS!
!
Autoloading for client-side JavaScript
Before we reference something in JavaScript, we need to make sure it’s been included via a <script> tag
Problem:
http://www.flickr.com/photos/sewtechnicolor/8213938281/
@weaverryan
* 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
RequireJS
RequireJS works by requiring “modules”, not files.
(though one file will contain one module)
bower install requirejs --save
Get it!
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>
<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' }});!require(['app/homepage']);</script>
base.html.twig
1) Bring in the require.js file downloaded via Bower using a normal script tag
<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' }});!require(['app/homepage']);</script>
2) Configure RequireJSAll modules live relative to this directory
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' }});!require(['app/homepage']);</script>
2) Configure RequireJSExceptions: when I ask for “jquery” look for it here (relative to baseUrl), instead of assets/js/jquery
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' }});!require(['app/homepage']);</script>
2) Configure RequireJS
Loads assets/js/app/homepage.js
base.html.twig
app/homepage.js
define([], function() { console.log("It's alive!");});
But how does it work?
@weaverryan
* 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.
But how!
now, what if we need jQuery?
http://www.flickr.com/photos/danaberlith/4207059574
Remember, jQuery isn’t even downloaded yet - the global $ is not available
define([], function() { $ = require('jquery'); $('...');});
… it might look something like this?
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
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
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
app/homepage.js
requirejs.config({ baseUrl: '/assets/js', paths: { jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' }, shim: { bootstrap: ['jquery'] }});
fixes an issue where Bootstrap *needs* jQuery before it’s downloaded
shim is a way for you to configure libraries that aren’t proper RequireJS modules
base.html.twig
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
app/modules/love.jsdefine(['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(); }); } }});
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”
app/modules/love.js
define( ['jquery', 'app/modules/love'], function ($, Love) {!
$(document).ready(function() { Love.spiritOfXmas(); });});
app/homepage.js
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.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' } // …});!require(['{{ module }}']);</script>
... and make the module a variable
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.twig', { 'module': 'app/homepage'}) }}{% endblock %}
4) Repeat!
EventBundle:Event:new.html.twig
{% block requirejs %} {{ include('::_requirejs.html.twig', { 'module': 'app/event_new'}) }}{% endblock %}
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 them, we already have them.
Let’s start by creating a common “module” that’s always loaded
requirejs.config({ paths: { domReady: '../vendor/requirejs-domready/domReady', jquery: '../vendor/jquery/jquery.min', bootstrap: '../vendor/bootstrap/dist/js/bootstrap.min' }, shim: { bootstrap: ['jquery'] }});
assets/js/common.js
<script src="{{ asset('/assets/vendor/requirejs/require.js') }}"></script>!
<script> requirejs.config({ baseUrl: '/assets/js' });!
require(['common'], function (common) { require(['{{ module }}']); });</script>
::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>
Setup baseUrl so we can reference the common module below
::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>
Load common and *then* load our real module
::requirejs.html.twig
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
@weaverryan
* 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
Installing the Optimizer
npm init
Create an empty package.json
npm install requirejs --save-dev
{ "devDependencies": { "requirejs": "~2.1.9" }}
package.json
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
build.js
Configuration tells RequireJS how to minify and combine files
({ 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'] } ]})
({ 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'] } ]})
The entire web/assets directory is first copied to web/assets-built
({ 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'] } ]})
These files are then re-written. RequireJS reads their dependencies and includes them in the file automatically
({ 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'] } ]})
... plus we can manually include more modules
({ 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'] } ]})
Avoids packaging jquery , bootstrap into homepage since we now already have it in common
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><script>!
requirejs.config({ baseUrl: '/{{ assetsPath }}/js' });!
require(['common'], function (common) { require(['{{ module }}']);!
});</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 it
Compass
Sass with style
Problem:
Static CSS files are *so* 2010
http://www.flickr.com/photos/stevendepolo/8409407391
@weaverryan
* Processes sass files into CSS
!
* A sass “framework”: adds a lot of extra
functionality, including CSS3 mixins, sprites,
etc
Compass
{ "dependencies": { "sass-bootstrap": "~3.0.0" "requirejs": "~2.1.9", }}
bower.json
Use Bower to bring in a sass Bootstrap
bower install
Rename and reorganize CSS into SASS files
web/assets/sass/ * _base.scss * _event.scss * _events.scss * event_form.scss * layout.scss
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
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
Rename and reorganize CSS into SASS files
web/assets/sass/ * _base.scss * _event.scss * _events.scss * event_form.scss * layout.scss
These are the only CSS files that will be included directly
EventBundle:Event:new.html.twig
base.html.twig
{% block stylesheets %} <link rel="stylesheet" href="{{ asset('assets/css/layout.css') }}"/>{% endblock %}
{% block stylesheets %} {{ parent() }}!
<link rel="stylesheet" href="{{ asset('assets/css/event_form.css') }}"/>{% endblock %}
base.html.twig
EventBundle:Event:new.html.twig
{% block stylesheets %} <link rel="stylesheet" href="{{ asset('assets/css/layout.css') }}"/>{% endblock %}
{% block stylesheets %} {{ parent() }}!
<link rel="stylesheet" href="{{ asset('assets/css/event_form.css') }}"/>{% endblock %}
We link directly to non-existent CSS files, which we’ll generate
base.html.twig
EventBundle:Event:new.html.twig
@import "base";@import "../vendor/sass-bootstrap/lib/bootstrap";@import "event";@import "events";!
body { // ...}
layout.scss
@import "base";@import "../vendor/sass-bootstrap/lib/bootstrap";@import "event";@import "events";!
body { // ...}
Sets variables and imports mixins used in all SASS files
layout.scss
@import "base";@import "../vendor/sass-bootstrap/lib/bootstrap";@import "event";@import "events";!
body { // ...}Import other files that contain actual CSS rules. These will eventually be concatenated into 1 file.
layout.scss
@import "base";!
/* for play, make the inputs super-rounded */.form-group input { @include border-radius(20px, 20px);}
event_form.scss
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 RequireJS optimizer 2) Before deploy, run Compass 3) During development, watch Compass
sudo npm install -g grunt-cli
Install the Grunt executable
{ "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" }}
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" }}
Install Grunt into your project + some modules
npm install
Grunt works by creating a Gruntfile.js file, where we define tasks (like app/console)
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');};
Gruntfile.js
grunt -h
Eventually we can run grunt RequireJS but we need to configure each command
Remove the RequireJS build.js and moves its contents here
Gruntfile.js
Use Grunt to run the RequireJS optimizer
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 ...] } } }
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 ...] } } }
We can setup variables and use them
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 ...] } } }
RequireJS *can* uglify CSS and JS, but we’ll leave this to Uglify and Compass
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 ...] } } }
This is a sub-task. We can now run grunt requirejs:main or grunt requirejs to run all sub-tasks
Repeat for Compass!
compass: { dist: { options: { sassDir: '<%= builtDir %>/sass', cssDir: '<%= builtDir %>/css', environment: 'production', outputStyle: 'compressed' } }, dev: { options: { sassDir: '<%= appDir %>/sass', cssDir: '<%= appDir %>/css', outputStyle: 'expanded' } }}
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 bit more control
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 developmentgrunt.registerTask('dev', [ 'jshint', 'compass:dev']);!
// task for before deploymentgrunt.registerTask('production', [ 'jshint', 'requirejs', 'uglify', 'compass:dist']);
!
!
!
!
!
!
// task for before deploymentgrunt.registerTask('production', [ 'jshint', 'requirejs', 'uglify', 'compass:dist']);
1) 2) 3) 4)
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
What about “watching”
watch: { scripts: { files: [ '<%= appDir %>/js/*.js', // ... ], tasks: ['jshint'] }, compass: { files: '<%= appDir %>/sass/*.scss', tasks: ['compass:dev'] }}
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 correct directory?
Simple Solution
parameters: assets_directory: 'assets'!
twig: # ... globals: assetsPath: %assets_directory%
config.yml
parameters: assets_directory: 'assets-prod'
config_prod.yml
<script src="{{ asset(assetsPath~'/vendor/requirejs/require.js') }}"></script><script> requirejs.config({ baseUrl: '/{{ assetsPath }}/js' });!
// ...</script>
::requirejs.html.twig
{% block stylesheets %}<link rel="stylesheet" href="{{ asset(assetsPath~'/css/layout.css') }}"/>{% endblock %}
::base.html.twig
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 Twig function to replace asset()
Bower downloads JS dependencies !
Modules included via RequireJS !
Compass compiles our SASS files !
Grunt optimizes for RequireJS, Uglifies, runs Compass, and watches for changes@weaverryan
Now
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!
@weaverryan
Brutal Feedback appreciated https://joind.in/10372
The code: http://bit.ly/sfcon-js-github
Keep learning: KnpUniversity.com