Simplifying your Django Frontend Tasks with Grunt
Grunt is a powerful task runner with an amazing assortment of plugins. It's not limited to the frontend, but there are many frontend-oriented plugins that you can take advantage of to combine and minify your static media, compile sass and less files, watch for changes during development and reload your browser automatically, and much more.
In the last several years, the amount of tooling around frontend development has expanded dramatically. Frameworks, libraries, preprocessors and postprocessors, transpilers, template languages, module systems, and more! Wiring everything together has become a significant challenge, and a variety of build tools have emerged to help ease this burden. Grunt is the current leader because of its fantastic plugin community, and it contains a wide array of plugins that can be very valuable to a Django developer. Today I'm going to talk about an easy way to integrate Grunt with Django's runserver, and highlight a few plugins to handle common frontend tasks that Django developers often deal with.
Installing Grunt
Grunt uses Node.js, so you'll need to have that installed and configured on your system. This process will vary depending on your platform, but once it's done you'll need to install Grunt. From the documentation:
@syntax: shell
$ npm install -g grunt-cli
This will put the grunt command in your system path, allowing it to be run from any directory.
Note that installing grunt-cli does not install the Grunt task runner! The job of the Grunt CLI is simple: run the version of Grunt which has been installed next to a Gruntfile. This allows multiple versions of Grunt to be installed on the same machine simultaneously.
Next, you'll want to install the Grunt task runner locally, along with a few plugins that I'll demonstrate:
@syntax: shell
$ npm install --save-dev grunt grunt-contrib-concat grunt-contrib-uglify grunt-sass grunt-contrib-less grunt-contrib-watch
Managing Grunt with runserver
There are a few different ways to get Grunt running alongside Django on your local development environment. The method I'll focus on here is by extending the runserver command. To do this, create a gruntserver command inside one of your project's apps. I commonly have a "core" app that I use for things like this. Create the "management/command" folders in your "myproject/apps/core/" directory (adjusting that path to your own preferred structure), and make sure to drop an "init.py" in both of them. Then create a "gruntserver.py" inside "command" to extend the built-in.
In your new "gruntserver.py" extend the built-in and override a few methods so that you can automatically manage the Grunt process:
@syntax: python
import os
import subprocess
import atexit
import signal
from django.conf import settings
from django.contrib.staticfiles.management.commands.runserver import Command\
as StaticfilesRunserverCommand
class Command(StaticfilesRunserverCommand):
def inner_run(self, *args, **options):
self.start_grunt()
return super(Command, self).inner_run(*args, **options)
def start_grunt(self):
self.stdout.write('>>> Starting grunt')
self.grunt_process = subprocess.Popen(
['grunt --gruntfile={0}/Gruntfile.js --base=.'.format(settings.PROJECT_PATH)],
shell=True,
stdin=subprocess.PIPE,
stdout=self.stdout,
stderr=self.stderr,
)
self.stdout.write('>>> Grunt process on pid {0}'.format(self.grunt_process.pid))
def kill_grunt_process(pid):
self.stdout.write('>>> Closing grunt process')
os.kill(pid, signal.SIGTERM)
atexit.register(kill_grunt_process, self.grunt_process.pid)
A barebones grunt config
To get started with Grunt, you'll need a barebones "Gruntfile.js" at the root of your project to serve as your config.
@syntax: javascript
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
// Task configuration goes here.
});
// Load plugins here.
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-sass');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-watch');
// Register tasks here.
grunt.registerTask('default', []);
};
Combining static media
A common task for the frontend, and one that we often use complex apps for in Django, is combining and minifying static media. This can all be handled by Grunt if you like, avoiding difficulties sometimes encountered when using an integrated Django app.
To combine files, use the concat plugin. Add some configuration to the "grunt.initConfig" call, using the name of the task as the key for the configuration data:
@syntax: javascript
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
// Task configuration goes here.
concat: {
app: {
src: ['myproject/static/js/app/**/*.js'],
dest: 'build/static/js/app.js'
},
vendor: {
src: ['myproject/static/js/vendor/**/*.js'],
dest: 'build/static/js/lib.js'
}
}
});
This will combine all Javascript files under "myproject/static/app/js" into one file called "myproject/build/static/js/app.js". It will also combine all Javascript files under "myproject/static/vendor" into one file called "myproject/build/static/js/lib.js". You'll likely want to refine this quite a bit to pick up only the files you want, and possibly build different bundles for different sections of your site. This will also work for CSS or any other type of file, though you may be using a preprocessor to combine your CSS and won't need this.
You'll probably want to use this along with the "watch" plugin for local development, but you'll use the "uglify" plugin for deployment.
Minifying static media
Once your app is ready for production, you can use Grunt to minify the JavaScript with the uglify plugin. As with concatenation, minification of your CSS will likely be handled by your preprocessor.
This task should be run as part of your deploy process, or part of a pre-deploy build process. The uglify config will probably be very similar to your concat config:
@syntax: javascript
uglify: {
app: {
files: {'build/static/js/app.min.js': ['myproject/static/js/app/**/*.js']}
},
vendor: {
files: {'build/static/js/lib.min.js': ['myproject/static/js/vendor/**/*.js']}
}
}
The main difference is that uglify takes the new-style "files" option instead of the classic "src" and "dest" options that concat uses.
Compiling Sass
You can compile Sass with Compass using the compass plugin, but I prefer to use the speedier sass plugin that uses libsass. Here's an example that includes the Foundation library:
@syntax: javascript
sass: {
dev: {
options: {
includePaths: ['bower_components/foundation/scss']
},
files: {
'build/static/css/screen.css': 'myproject/static/scss/screen.scss'
}
},
deploy: {
options: {
includePaths: ['bower_components/foundation/scss'],
outputStyle: 'compressed'
},
files: {
'build/static/css/screen.min.css': 'myproject/static/scss/screen.scss'
}
}
},
Compiling Less
Less is compiled using the less plugin.
@syntax: javascript
less: {
dev: {
options: {
paths: ['myproject/static/less']
},
files: {
'build/static/css/screen.css': 'myproject/static/less/screen.less'
}
}
deploy: {
options: {
paths: ['myproject/static/less'],
compress: true
},
files: {
'build/static/css/screen.min.css': 'myproject/static/less/screen.less'
}
}
},
Watching for changes and live reloading
Now that you've got your initial operations configured, you can use the watch plugin to watch for changes and keep the files up to date. It also will send livereload signals, which you can use to automatically refresh your browser window.
@syntax: javascript
watch: {
options: {livereload: true}
javascript: {
files: ['myproject/static/js/app/**/*.js'],
tasks: ['concat']
},
sass: {
files: 'myproject/static/scss/**/*.scss',
tasks: ['sass:dev']
}
}
Note the way the task is specified in the "sass" watch config. Calling "sass:dev" instructs it to use the "dev" config block from the "sass" task. Using "sass" by itself as the name of the task would have invoked both "sass:dev" and "sass:deploy" from our configuration above.
Also note how we're using a top-level "options" definition here to make livereload the default. You can then override that for an individual watch definition if you don't need livereload for that one.
In order for the browser to make use of the livereload signals, we'll need to add a <script>
tag that retrieves code from the livereload server that Grunt starts in the background. In Django, you'll want to hide this tag behind a DEBUG check.
{% if debug %}
<script src="//localhost:35729/livereload.js"></script>
{% endif %}
You can also use a LiveReload browser extension instead.
More to come
Grunt is a fantastic tool and one that makes it easier to work with the growing set of frontend tools that are emerging. There's a vibrant plugin ecosystem, and its capabilities are growing all the time. I'll be covering more of those tools in the future, and I'll be sure to include Grunt configuration for each one. Enjoy!