Сборка фронтенда в Django

Из коробки в Django нет средств для автоматизации сборки фронтенда. Ну заете, обычное дело: перевести sass в css, применить к этому автопрефиксер, а сверху кофескриптом намазать.

На манер пайплайна из рельс есть сторонние django-pipeline и django-compressor. Но собирать фронтенд его родными инструментами всё-таки удобнее. Пришла идея сделать из gulp замену collectstatic и подружить его с runserver и структурой проекта.

Структура задач

Организовать красивую иерархию gulp-тасков поможет gulp-django-utils. В корне проекта, рядом с manage.py, разместим package.json и gulpfile.js.

project
├─ blog
├─ static
├─ project
├─ manage.py
├─ gulpfile.js  ←
└─ package.json ←

Способы сборки статики в приложениях могут различаться. Поэтому каждому приложению нужен свой локальный gulpfile.js.

blog
├─ static
│  └─ blog
│     ├─ styles
│     └─ scripts
├─ templates
├─ gulpfile.js ←
├─ models.py
└─ views.py

Корневой gulpfile будет собирать только общую для проекта статику, например, упаковывать сторонние библиотеки, или собирать общий для проекта UI-фреймворк.

var django = require('gulp-django-utils');
var concat = require('gulp-concat');

// Имена приложений, которые надо обойти.
var apps = ['blog'];

// Инициализируем проект в текущей директории.
var project = new django.Project(apps);

// Обходим указанные приложения и загружаем оттуда `gulpfile`.
project.discoverApps();

// Создаем таск, у которого в зависимостях окажутся одноименные
// таски из приложений.
project.task('js', function() {
  // Возьми все `.js` файлы из `django-project/static/main/js`,
  // объедини в один и положи в `django-project/static/build`.
  project.src('static/main/js/*.js')
    .pipe(concat('main.js'))
    .pipe(project.dest('static/build'));
});

Таски внутри приложений будут собирать статику этого приложения. Например, JavaScript-контроллеры для страниц или наборы уникальных стилей.

var django = require('gulp-django-utils');
var concat = require('gulp-concat');

module.exports = function(project) {
  // Инициализируем приложение в проекте.
  var app = new django.Application('blog', project);

  // Создаём таск в пространстве имён приложения.
  app.task('js', function() {
    // Возьми все `.js` файлы из `django-project/blog/static/blog/js`,
    // объедини в один и положи в `django-project/static/build`.
    app.src('static/blog/js/*.js')
      .pipe(concat('blog.js'))
      .pipe(project.dest('static/build'));
  });
};

При запуске gulp js выполнится таск js из корневого gulpfile. Но благодаря gulp-django-utils, этот таск притянет за собой все одноименные таски из приложений, в данном случае таск js из приложения blog. Таким образом собирается весь JavaScript проекта. Таски приложений можно запускать отдельно, добавив имя приложения перед названием задачи: gulp blog:js.

Одной командой

Неудобно запускать две команды: runserver и gulp watch. Исправить ситуацию поможет django-frontserver. В корневом gulpfile должен быть определён таск watch, который будет следить за изменениями в файлах и пересобирать изменившиеся части приложения.

project.task('watch', function() {
  // Запускай таск `js` каждый раз, когда изменится любой
  // из `.js` файлов в `django-project/static/main/js`.
  project.watch('static/main/js/*.js', ['js']);
});

Таск watch может быть определён внутри gulpfile приложения:

app.task('watch', function() {
  // Запускай `blog:js` каждый раз, когда изменится любой
  // из `.js` файлов в `django-project/blog/static/blog/js`.
  app.watch('static/blog/js/*.js', ['js']);
});

Теперь, запустив python manage.py frontserver вместе со статическим сервером будет запускаться gulp и пересобирать изменившиеся файлы.