Ещё один пост о сборке front-end проекта

  • Tutorial
Js app starter

Я потратил прилично времени на структуризацию и автоматизацию сборки фронта. Задача это интересная и стоит того, чтобы о ней рассказать.

Что умеет делать сборщик:
  • Собирать front-end проект для development & production окружений.
  • Собирать по несколько js/css бандлов на проект.
  • Использовать стиль CommonJS модулей в браузере.
  • Использовать ES6-синтаксис.
  • Спрайты, картинки и многое другое.


Вступительное


Чтобы было удобней следить за мыслью, сразу кидаю ссылку на репозиторий с шаблоном проекта: github.com/alexfedoseev/js-app-starter

Как его завести
Убедитесь, что установлен npm.
npm -v


Установите необходимые глобальные модули (если ещё не установлены):
npm install -g gulp browserify babel jade stylus http-server


Сделайте форк репозитория.
git clone https://github.com/alexfedoseev/js-app-starter.git


Установите зависимости проекта (исполнять в корне репозитория):
npm install


Соберите проект в development окружении и запустите локальный сервер:
npm start


Откройте браузер и перейдите на lvh.me:3500

В качестве сборщика будем использовать Gulp.
Что включает процесс сборки и какие технологии используются:
  • Сборка HTML
    Шаблонизатор: Jade
  • Сборка CSS
    Препроцессор: Stylus
    Префиксер: Autoprefixer
  • Сборка JS
    Модульная система: Browserify + Babel (ES6 transpiler)
    Проверка качества кода: jsHint
  • Оптимизация изображений
    Оптимизатор: Imagemin
  • При необходимости: сборка спрайтов, обработка json, копирование фонтов и прочих файлов в public папку
    Сборщик спрайтов: Spritesmith
    Обработка json: gulp-json-editor

Вобще я люблю Slim и Sass, но Ruby к Ruby, a JS к JS: для frontend-проекта будем использовать только штуки из npm. При желании любой инструмент можно заменить.

Структура проекта


| dist/

| lib/
  |-- gulp/
      |-- helpers/
      |-- tasks/
      |-- config.js

| node_modules/

| public/
  |-- css/
  |-- files/
  |-- fonts/
  |-- img/
  |-- js/
  |-- json/
  |-- favicon.ico
  |-- index.html

| src/
  |-- css/
  |-- files/
  |-- fonts/
  |-- html/
  |-- img/
  |-- js/
  |-- json/
  |-- sprite/
  |-- favicon.ico

| .gitignore
| .npmignore
| gulpfile.js
| npm-shrinkwrap.json
| package.json

Github

.gitignore & .npmignore
Внутри этих файлов находится список того, что будет игнорироваться git и npm при коммитах/паблишах.

node_modules/
В эту директорию падают все модули, которые мы установим через npm.

npm-shrinkwrap.json
Я не держу в репозитории содержимое node_modules/. Вместо этого лочу все зависимости через этот файл. Он генерируется автоматически командой: `npm shrinkwrap`.

package.json
Это файл с глобальными настройками проекта. К нему ещё вернемся.

gulpfile.js
Обычно тут хранятся все таски для сборки проекта, но в нашем случае он просто определяет значение переменной окружения и пробрасывает нас дальше в папку с gulp-тасками.

lib/gulp/
Здесь храним все настройки и задачи сборщика.

|-- config.js
Выносим настройки для всех тасков в отдельный файл, чтобы минимизировать правку самих тасков.

|-- helpers/
Вспомогательные методы сборщика.

|-- tasks/
И сами gulp-таски.

src/
Исходники проекта.

public/
Результат сборки. Абсолютно всё содержимое этой папки генерируется сборщиком и перед каждой новой сборкой она полностью очищается, поэтому тут никогда и ничего не храним.

dist/
Иногда я пишу opensource-модули. В этой папке после сборки оказываются обычная и минифицированная версии написанной js-библиотеки. При этом директория public/ используется как хранилище для демки. Если вы делаете обычный сайт или страницу приземления, то оно не понадобится.

Настройка проекта


package.json

Это файл, в котором хранятся глобальные настройки проекта.
Подробное описание его внутренностей можно посмотреть тут: browsenpm.org/package.json
Ниже я остановлюсь только на некоторых важных частях.

{
  // Название проекта
  "name": "js-app-starter",

  // Версия проекта
  // Использую для версионирования модулей / обновления js+css в кэше браузера при обновлении версии сборки
  "version": "0.0.1",

  // Если вы пишете js-библиотеку, то тут указываем путь к файлу,
  // который будет отзываться на `require('your-lib')`
  "main": "./dist/app.js",

  // Настойки browserify
  // В данном случае говорим, что нужно перед сборкой превратить ES6 в ES5
  "browserify": {
    "transform": [
      "babelify"
    ]
  },

  // Консольные команды (подробнее ниже)
  "scripts": {
    "start": "NODE_ENV=development http-server -a lvh.me -p 3500 & gulp",
    "build": "NODE_ENV=production gulp build"
  },

  // Настройки jshint (проверка качества кода)
  "lintOptions": {
    "esnext": true
    ...
  },

  // Frontend зависимости
  "dependencies": {
    "jquery": "^2.1.3"
    ...
  },

  // Development зависимости
  "devDependencies": {
    "gulp": "^3.8.11"
   ...
  }
}
Github

Консольные команды

В package.json мы можем прописать алиасы для консольных команд, которые будем часто выполнять в процессе разработки.

"scripts": {
  "start": "NODE_ENV=development http-server -a lvh.me -p 3500 & gulp",
  "build": "NODE_ENV=production gulp build"
}


Development сборка
Перед началом работы с проектом нам нужно:
  • собрать его из исходников (с sourcemaps для дебага)
  • запустить «наблюдателей», которые будут пересобирать проект при изменении исходных файлов
  • запустить локальный сервер

# команда, которую исполняем
npm start

# что исполняется на самом деле
NODE_ENV=development http-server -a lvh.me -p 3500 & gulp

Разбираем по частям
# устанавливаем переменную окружения
NODE_ENV=development

# запускаем локальный сервер на домене lvh.me и порте 3500
http-server -a lvh.me -p 3500

# запускаем gulp таски
gulp



Production сборка
Когда мы готовы релизить проект — делаем production-сборку.

# нажмите Ctrl+C, чтобы остановить локальный сервер и наблюдателей, если они запущены

# команда, которую исполняем
npm run build

# что исполняется на самом деле
NODE_ENV=production gulp build

Разбираем по частям
# устанавливаем переменную окружения
NODE_ENV=production

# запускаем gulp-таск `build`
gulp build



Gulp


Переходим к Gulp. Структура тасков взята из сборщика от Dan Tello.

Перед тем, как нырнуть, небольшой комментарий по порядку выполнения обычного gulp-таска:

var gulp = require('gulp');

gulp.task('task_1', ['pre_task_1', 'pre_task_2'], function() {
  console.log('task_1 is done');
});

// Здесь мы объявили `task_1`, который выводит в консоль сообщение `task_1 is done`
// Запускается он командой `gulp task_1`
// Но перед выполнением основного `task_1` должны выполниться задачи `['pre_task_1', 'pre_task_2']`
// Важно понимать, что 'pre_task_1' & 'pre_task_2' - выполняются асинхронно,
// то есть порядок выполнения не зависит от позиции задачи в массиве,
// а `task_1` стартует только после того, как отработали 2 pre-задачи - то есть синхронно



Теперь разберемся что и в каком порядке будем собирать.

Development сборка
`npm start` запускает команду `gulp`. Что происходит дальше:

  • Gulp ищет в текущей директории gulpfile.js. Обычно в него складываются все таски, но здесь он просто определит значение переменной окружения и пробросит нас дальше в папку с gulp-тасками.

    Код с комментариями
    /* file: gulpfile.js */
    
    // модуль, позволяющий включать таски из вложенных директорий
    var requireDir = require('require-dir');
    
    // устанавливаем значение глобальной переменной,
    // позволяющей различать в тасках development & production окружения
    global.devBuild = process.env.NODE_ENV !== 'production';
    
    // пробрасываем сборщик в папку с тасками и конфигом
    requireDir('./lib/gulp/tasks', { recurse: true });
    
    Github

  • После того, как нас пробросило в директорию, сборщик ищет таск с названием `default`, который сначала запускает «наблюдателей» над исходниками, потом:
    • очищает папки `public/` & `dist/`
    • линтит js-файлы
    • и собирает спрайты

    После этого собирается проект (html, css, js и всё остальное).

    Код с комментариями
    default

    /* file: lib/gulp/tasks/default.js */
    
    var gulp = require('gulp');
    
    // Запускаем пустой таск `default`, но предварительно исполняем таск `watch`
    gulp.task('default', ['watch']);
    
    
    Github

    watch

    /* file: lib/gulp/tasks/watch.js */
    
    var gulp   = require('gulp'),
        finder = require('../helpers/finder'),  // хелпер для поиска файлов
        config = require('../config');          // конфиг
    
    // Запускаем таск `watch`, перед ним исполняем таски `watching` & `build`
    gulp.task('watch', ['watching', 'build'], function() {
    
      // Вешаем наблюдателей на все файлы в директориях `css`, `images` & `html` 
      // При изменении одного из файлов в указанной директории gulp выполнит соответствующий таск
      gulp.watch(finder(config.css.src),  ['css']);
      gulp.watch(finder(config.images.src), ['images']);
      gulp.watch(finder(config.html.src), ['html']);
    
    });
    
    gulp.task('watching', function() {
    
      // Объявляем глобальную переменную `isWatching`, 
      // которая сигнализирует, что наблюдатели запущены
      global.isWatching = true;
    
    });
    
    
    Github

    build

    /* file: lib/gulp/tasks/build.js */
    
    var gulp = require('gulp');
     
    // Запускаем таск `build`, перед ним исполняем таски: 
    // `clean`  - перед сборкой очищаем директории `public/` & `dist/`
    // `lint`   - проходимся jshint по js-файлам (проверка качества кода) 
    // `sprite` - собираем спрайты
    gulp.task('build', ['clean', 'lint', 'sprite'], function() {
    
      // После того, как отработали три таска выше, запускается таск `bundle`
      // Вобще метод `gulp.start` deprecated, 
      // но нормальное управление sync/async задачами появится только в Gulp 4.0,
      // поэтому используем пока его
      gulp.start('bundle');
    
    });
    
    // Собираем проект
    gulp.task('bundle', ['scripts', 'css', 'images', 'html', 'copy'], function() {
    
      // Если мы в dev-окружении, то после сборки выставляем значение переменной `doBeep` = true
      // `notifier` хелпер покажет нам уведомления об ошибках или окончании работы тасков 
      // (в консоли и всплывающим баннером)
      if (devBuild) global.doBeep = true;
    
    });
    
    
    Github

Production сборка
С ней всё проще. `npm run build` запускает команду `gulp build`, которая очищает целевые папки, линтит js-код, собирает спрайты и после этого собирет проект (без sourcemaps). Код с комментариями выше.

Файл конфигураций gulp-тасков

Все основные конфигурации тасков вынесены в отдельный файл lib/gulp/config.js:

/* file: lib/gulp/config.js */

var pkg     = require('../../package.json'),   // импортируем package.json
    bundler = require('./helpers/bundler');    // импортируем хелпер для созлания бандлов


/* Настраиваем пути */

var _src    = './src/',     // путь до исходников
    _dist   = './dist/',    // куда будем сохранять дистрибутив будущей библиотеки
    _public = './public/';  // куда будем сохранять сайт или примеры использования библиотеки

var _js     = 'js/',        // папка с javascript файлами
    _css    = 'css/',       // папка с css
    _img    = 'img/',       // папка с картинками
    _html   = 'html/';      // папка с html


/* 
 * Настраиваем js / css бандлы 
 *
 * Пример: app.js, app.css     - сайт
 *         admin.js, admin.css - админка
 *
 * Пример: your-lib.js        - модуль без зависимостей
 *         your-lib.jquery.js - модуль в формате jquery-плагина
 *
 */

var bundles = [
  {
    name        : 'app', // название бандла
    global      : 'app', // если пишем модуль, это имя объекта, экспортируемого в глобальное пространство имён
    compress    : true,  // минифицируем?
    saveToDist  : true   // сохраняем в папку `/dist`? (true - если пишем модуль, false - если делаем сайт)
  }
];


module.exports = {

  /* тут настройки тасков */

};
Github

Сборка HTML

Для шаблонизации используем Jade. Он позволяет делать вставки партиалов, использовать inline-javascript, переменные, миксины и ещё много разных крутых штук.

Gulp
Конфиг

/* file: lib/gulp/config.js */

html: {
  src: _src + _html,              // путь до jade-исходников
  dest: _public,                  // куда сохраняем собранное
  params: {                       // параметры для jade
    pretty: devBuild,             // убиваем отступы в html?
    locals: {                     // переменные, которые мы передаем в шаблоны
      pkgVersion: pkg.version     // сохраняем версию релиза в переменную `pkgVersion`
    }
  }
}

Github

Таск

/* file: lib/gulp/tasks/html.js */

var gulp        = require('gulp'),
    jade        = require('gulp-jade'),
    jadeInherit = require('gulp-jade-inheritance'),
    gulpif      = require('gulp-if'),
    changed     = require('gulp-changed'),
    filter      = require('gulp-filter'),
    notifier    = require('../helpers/notifier'),
    config      = require('../config').html;

gulp.task('html', function(cb) {

  // берём все jade-файлы из директории src/html
  gulp.src(config.src + '*.jade')
      // если dev-сборка, то watcher пересобирает только изменённые файлы
      .pipe(gulpif(devBuild, changed(config.dest)))
      // корректно обрабатываем зависимости
      .pipe(jadeInherit({basedir: config.src}))
      // отфильтровываем не-партиалы (без `_` вначале)
      .pipe(filter(function(file) {
        return !/\/_/.test(file.path) || !/^_/.test(file.relative);
      }))
      // преобразуем jade в html
      .pipe(jade(config.params))
      // пишем html-файлы
      .pipe(gulp.dest(config.dest))
      // по окончании запускаем функцию
      .on('end', function() {
        notifier('html');  // уведомление (в консоли + всплывашка)
        cb();              // gulp-callback, сигнализирующий о завершении таска
      });

});

Github
Исходники
Структура папки src/html

| src
  |-- html
      |-- index.jade        # скелет страницы
      |-- components/       # компоненты страницы
          |-- _header.jade
      |-- helpers/          # переменные, миксины
          |-- _params.jade
          |-- _mixins.jade
      |-- meta/             # содержимое head, коды аналитики и пр.
          |-- _head.jade

Github

Все партиалы снабжаем префиксом `_` (нижнее подчеркивание), чтобы при сборке мы могли их отфильтровать и игнорировать.

helpers/_variables.jade
Сохраняем необходимые параметры в переменные. Например, если у нас телефон стоит в нескольких местах страницы, то его лучше сохранить в переменную и в шаблонах использовать именно её.

/* file: src/html/helpers/_variables.jade */

- var release = pkgVersion          // переменная из gulp-конфига
- var phone = '8 800 CALL-ME-NOW'   // телефон

Github

helpers/_mixins.jade
Часто используемые блоки можно обернуть в mixin.

/* file: src/html/helpers/_mixins.jade */

mixin phoneLink(phoneString)
  - var cleanPhone = phoneString.replace(/\(|\)|\s|\-/g, '')
  a(href="tel:#{cleanPhone}")= phoneString

// в верстке вставляем
// +phoneLink(phone)

Github

index.jade
Скелет главной страницы.

/* file: src/html/index.jade */

include helpers/_variables    // импортируем переменные
include helpers/_mixins       // импортируем миксины

doctype html
html

  head
    include meta/_head

  body
    include components/_header
    include components/_some_component
    include components/_footer

Github

meta/_head.jade
Содержимое head.

/* file: src/html/meta/_head.jade */

meta(charset="utf-8")

...

// Используем версию сборки, если нужно обновить js/css в кэше браузеров
link(rel="stylesheet" href="css/app.min.css?v=#{release}")
script(src="js/app.min.js?v=#{release}")

...

Github


Сборка JavaScript

В качестве модульной системы используем Browserify. C ним мы можем использовать стиль подключения CommonJS модулей непосредственно в браузере. Кроме этого мы теперь можем использовать ES6-синтаксис: Babel преобразует его в ES5 перед тем, как Browserify соберет js. И перед сборкой мы проходимся jsHint для проверки качества кода.

У Browserify есть один минус: если вы пишете библиотеку с внешними зависимостями (например jQuery-плагин), то он не сможет сделать правильную UMD-обертку. В этом случае я заменяю Browserify на конкатенацию и пишу обёртку руками.

О бандлах
На проекте может возникнуть необходимость формировать несколько наборов js/css.

Например вы пишите фронт + админку. Или библиотеку в 2 вариантах: без зависимостей и в формате jQuery-плагина. Эти сборки нужно разделять. Для этого в настройках сборщика мы создаем массив:

/* file: lib/gulp/config.js */

/* Для библиотеки */
var bundles = [
  {
    name        : 'myLib', // название бандла
    global      : 'myLib', // это имя объекта, экспортируемого в глобальное пространство имён
    compress    : true,    // минифицируем? (неминифицированная версия сохранятся всегда)
    saveToDist  : true     // сохраняем в папку `/dist`?
  }
];


/* Для сайта / страницы приземления */
var bundles = [
  {
    name        : 'app', // название бандла
    global      : false, // ничем отсвечивать не надо
    compress    : true,  // минифицируем?
    saveToDist  : false  // сохраняем в папку `/dist`?
  },
    name        : 'admin',
    global      : false,
    compress    : true,
    saveToDist  : false
  }
];

Github

js/css cборщики будут искать в папке с js/css исходниками соответствующий end-point файл (`app.js` или `app.styl`). Через этот end-point файл мы управляем всеми зависимостями бандла. Их структуру я покажу чуть ниже.

Перед передачей бандлов сборщику, мы предварительно пропускаем массив через хелпер `bundler`, который формирует объект с настройками.
Gulp
Конфиг

/* file: lib/gulp/config.js */

scripts: {
  bundles: bundler(bundles, _js, _src, _dist, _public),       // пакуем бандлы
  banner: '/** ' + pkg.name + ' v' + pkg.version + ' **/\n',  // задаем формат баннера для min.js
  extensions: ['.jsx'],                                       // указываем дополнительные расширения
  lint: {                                                     // параметры для jshint
    options: pkg.lintOptions,
    dir: _src + _js
  }
}

Github

Таск

/* file: lib/gulp/tasks/scripts.js */

var gulp       = require('gulp'),
    browserify = require('browserify'),
    watchify   = require('watchify'),
    uglify     = require('gulp-uglify'),
    sourcemaps = require('gulp-sourcemaps'),
    derequire  = require('gulp-derequire'),
    source     = require('vinyl-source-stream'),
    buffer     = require('vinyl-buffer'),
    rename     = require('gulp-rename'),
    header     = require('gulp-header'),
    gulpif     = require('gulp-if'),
    notifier   = require('../helpers/notifier'),
    config     = require('../config').scripts;


gulp.task('scripts', function(cb) {

  // считаем кол-во бандлов
  var queue = config.bundles.length;

  // поскольку бандлов может быть несколько, оборачиваем сборщик в функцию, 
  // которая в качестве аргумента принимает bundle-объект с параметрами
  // позже запустим её в цикл
  var buildThis = function(bundle) {

    // отдаем bundle browserify
    var pack = browserify({
      // это для sourcemaps
      cache: {}, packageCache: {}, fullPaths: devBuild,
      // путь до end-point (app.js)
      entries: bundle.src,
      // если пишем модуль, то через этот параметр
      // browserify обернет всё в UMD-обертку
      // и при подключении объект будет доступен как bundle.global
      standalone: bundle.global,
      // дополнительные расширения
      extensions: config.extensions,
      // пишем sourcemaps?
      debug: devBuild
    });

    // сборка
    var build = function() {

      return (
          // browserify-сборка
          pack.bundle()
              // превращаем browserify-сборку в vinyl
              .pipe(source(bundle.destFile))
              // эта штука нужна, чтобы нормально работал `require` собранной библиотеки
              .pipe(derequire())
              // если dev-окружение, то сохрани неминифицированную версию в `public/` (зачем - не помню))
              .pipe(gulpif(devBuild, gulp.dest(bundle.destPublicDir)))
              // если сохраняем в папку `dist` - сохраняем
              .pipe(gulpif(bundle.saveToDist, gulp.dest(bundle.destDistDir)))
              // это для нормальной работы sourcemaps при минификации
              .pipe(gulpif(bundle.compress, buffer()))
              // если dev-окружение и нужна минификация — инициализируем sourcemaps
              .pipe(gulpif(bundle.compress && devBuild, sourcemaps.init({loadMaps: true})))
              // минифицируем
              .pipe(gulpif(bundle.compress, uglify()))
              // к минифицированной версии добавляем суффикс `.min`
              .pipe(gulpif(bundle.compress, rename({suffix: '.min'})))
              // если собираем для production - добавляем баннер с названием и версией релиза
              .pipe(gulpif(!devBuild, header(config.banner)))
              // пишем sourcemaps
              .pipe(gulpif(bundle.compress && devBuild, sourcemaps.write('./')))
              // сохраняем минифицированную версию в `/dist`
              .pipe(gulpif(bundle.saveToDist, gulp.dest(bundle.destDistDir)))
              // и в `public`
              .pipe(gulp.dest(bundle.destPublicDir))
              // в конце исполняем callback handleQueue (определен ниже)
              .on('end', handleQueue)
      );

    };

    // если нужны watchers
    if (global.isWatching) {
      // оборачиваем browserify-сборку в watchify
      pack = watchify(pack);
      // при обновлении файлов из сборки - пересобираем бандл
      pack.on('update', build);
    }

    // в конце сборки бандла
    var handleQueue = function() {
      // сообщаем, что всё собрали
      notifier(bundle.destFile);
      // если есть очередь
      if (queue) {
        // уменьшаем на 1
        queue--;
        // если бандлов больше нет, то сообщаем, что таск завершен 
        if (queue === 0) cb();
      }
    };

    return build();
  };

  // запускаем массив бандлов в цикл
  config.bundles.forEach(buildThis);

});

Github
Исходники
Структура папки src/js

| src/
  |-- js/
      |-- components/  # код компонентов
      |-- helpers/     # js-хелперы
      |-- app.js       # end-point бандла

Github

app.js
Через этот файл мы рулим всеми зависимостями и порядком исполнения js-компонентов. Имя файла должно совпадать с именем бандла.

/* file: src/js/app.js */

/* Vendor */
import $ from 'jquery';

/* Components */
import myComponent from './components/my-component';


/* App */

$(document).ready(() => {

  myComponent();

});

Github
Что делать, если зависимости нет в npm
В таких случаях используем browserify-shim: плагин, который позволяет превращать обычные библиотеки в CommonJS-совместимые модули. Итак, у нас есть jQuery-плагин `maskedinput`, которого нет в npm.

Добавляем в `package.json` преобразование и выставляем настройки для зависимости:

/* file: package.json */

"browserify": {
  "transform": [
    "babelify",
    "browserify-shim"  // добавляем преобразование
  ]
},

// у `browserify-shim` много вариантов подключения библиотек
// смотрите доки на github: https://github.com/thlorenz/browserify-shim
"browser": {
  "maskedinput": "./path/to/jquery.maskedinput.js"
},
"browserify-shim": {
  "maskedinput": {
    "exports": "maskedinput",
    "depends": [
      "jquery:jQuery"
    ]
  }
}



После этого мы можем подключать модуль:
require('maskedinput');


Сборка CSS

В качестве препроцессора используем Stylus. Плюс проходимся по css автопрефиксером, чтобы не прописывать вендорные префиксы руками.

Gulp
Конфиг

/* file: lib/gulp/config.js */

css: {
  bundles: bundler(bundles, _css, _src, _dist, _public), // пакуем бандлы
  src: _src + _css,                                      // указываем где лежать исходники для watcher
  params: {},                                            // если нужны настройки для stylus - указываем тут
  autoprefixer: {                                        // настраиваем autoprefixer
    browsers: ['> 1%', 'last 2 versions'],               // подо что ставим префиксы
    cascade: false                                       // красиво не надо, всё равно минифицируем
  },
  compress: {}                                           // если нужны настройки минификации - указываем тут
}

Github

Таск

/* file: lib/gulp/tasks/css.js */

var gulp     = require('gulp'),
    process  = require('gulp-stylus'),
    prefix   = require('gulp-autoprefixer'),
    compress = require('gulp-minify-css'),
    gulpif   = require('gulp-if'),
    rename   = require('gulp-rename'),
    notifier = require('../helpers/notifier'),
    config   = require('../config').css;

/* Логика css-таска повторяет логику js-таска */

gulp.task('css', function(cb) {

  var queue = config.bundles.length;

  var buildThis = function(bundle) {

    var build = function() {
      return (
          gulp.src(bundle.src)
              .pipe(process(config.params))
              .pipe(prefix(config.autoprefixer))
              .pipe(gulpif(bundle.compress, compress(config.compress)))
              .pipe(gulpif(bundle.compress, rename({suffix: '.min'})))
              .pipe(gulp.dest(bundle.destPublicDir))
              .on('end', handleQueue)
      );
    };

    var handleQueue = function() {
      notifier(bundle.destFile);
      if (queue) {
        queue--;
        if (queue === 0) cb();
      }
    };

    return build();
  };

  config.bundles.forEach(buildThis);

});

Github
Исходники
Структура папки src/css

| src/
  |-- css/
      |-- components/            # стили компонентов
          |-- header.styl
          |-- footer.styl
      |-- globals/
          |-- fonts.styl         # подключаем фонты
          |-- global.styl        # глобальные настройки проекта
          |-- normalize.styl     # нормализуем / ресетим
          |-- variables.styl     # переменные
          |-- z-index.styl       # z-индексы проекта
      |-- helpers/
          |-- classes.styl       # вспомогательные классы
          |-- mixins.styl        # и миксины
      |-- sprite/
          |-- sprite.json        # json, генерируемый gulp.spritesmith
          |-- sprite.styl        # создаем из json css-классы
      |-- vendor/                # вендорные css складываем сюда
      |-- app.styl               # end-point бандла

Github

app.styl
Через этот файл мы рулим порядком подключения css-компонентов. Имя файла должно совпадать с именем бандла.

/* file: src/css/app.styl */

@import "helpers/mixins"
@import "helpers/classes"
@import "globals/variables"
@import "globals/normalize"
@import "globals/z-index"
@import "globals/fonts"
@import "globals/global"
@import "sprite/sprite"
@import "vendor/*"
@import "components/*"

Github


Все остальные таски — картинки, спрайты, очистка и пр. — не требуют дополнительных комментариев (на самом деле я просто устал уже строчить). Исходники лежат в репозитории: github.com/alexfedoseev/js-app-starter

Если есть косяки или дополнения — буду рад обратной связи через комментарии тут или issues / pull requests на Github. Удач!
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 12

    +5
    Я бы на вашем месте собрал бы всё это в генератор для yeoman(отличная вещь) и ваш раздел «Как его завести» сведётся к одной команде.
      0
      Я до этого для скаффолдинга использовал Thor. Надо почитать доки Yeoman, спасибо.
        0
        Тогда уже лучше посмотрите в сторону slush, он спроектирован для работы с gulp.
      0
      Немного жутко, когда хабр читает твои мысли — только вот ночью задавал вопрос toster.ru/q/195631
      Спасибо за статью, буду дома подробнее ознакомлюсь.
        0
        И все таки я рекомендую попробовать browsersync.io для livereload. У него есть несколько приятных особенностей типа tunnel и дебага. И да, а почему не запускаете сервер из gulp?
          +3
          Так же советую autoprefixer собирать отдельный файл для IE8 и IE9 и подключать его через комментарии. Можно немного уменьшить файл стилей. Если идти дальше, то хорошо бы разбивать media-query и тоже подключать в зависимости от разрешения/устройства.

          Я сейчас собираю такой же проект на jade/stylus, для сборки JS присматриваюсь к Web Pack. Так же хочу принять файловую структуру как у BEM в плане хранения всех файлов блока в одном месте.
            0
            Почитал про BEM. Интересная и логичная штука, в духе компонентов React. Тоже покручу папку `src`.
            0
            Я вот как-то не чувствую острой необходимости в browsersync: прикрутил, попробовал и убрал. А http-server я просто ещё до вникания в автоматизацию сборки использовал и на `npm start` изначально его повесил. Есть какая-то разница откуда его запускать?
            0
            Вот по поводу зависимостей не очень понял, в чем проблема с оберткой? Просто в ситуации с сайтом практически наверняка придется бовером подключать jquery, angular, etc. Как вы в этом случае поступаете?
              0
              Если вы не пишите собственную бибилиотеку (jQuery плагин или что-то в этом духе), которая должна оборачиваться в UMD-обёртку с внешними зависимостями (которые не нужно включать в сборку), то беспокоиться не о чем.
              +3
              Это все похвально, но в webpack все это можно сделать из коробки, 10ой строк в конфиг файле. Не ломайте себе мозг, используйте webpack.
                0
                Что-то у меня никогда не получалось gulp-jade-inheritance настроить, с подключением его сборка jade становится моментальной, но и не замечает он изменений никаких )

                Only users with full accounts can post comments. Log in, please.