быстрый старт bower + grunt

или дань моде

Постановка задачи: что ни сервер, все время приходится настраивать пару удобных утилит любого уважающего себя front-end разработчика — Bower+Grunt. Bower — незаменимый менеджер пакетов. Grunt — замечательное средство для автоматизации рутинных операций со статикой проекта. Ах да, для установки всего этого безобразия нам потребуется Node.js.

0. Обновление системы и пакетов

Чисто чтобы все собралось и взлетело…

dmitry@dev:~$ sudo apt-get update && apt-get upgrade
dmitry@dev:~$ sudo apt-get install git-core curl build-essential openssl libssl-dev

1. Установка Node.js и NPM (Node Package Manager)

Замечательная инструкция имеется тут — https://nodejs.org/en/download/package-manager/. Для Debian можно поставить сразу из официального репозитория, а можно собрать пакет ручками.

dmitry@dev:~$ curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
dmitry@dev:~$ sudo apt-get install -y nodejs

dmitry@dev:~$ node -v
v4.4.5

dmitry@dev:~$ npm -v
2.15.5

Собственно говоря, мы установили npm и node.js. Шутки мощные и полезные, но подробнее о них в следующий раз и в другом контексте.

3. Установка Bower

Bower как менеджер пакетов поставим в системе глобально, используя ключ -g:

dmitry@dev:~$ sudo npm install -g bower

Теперь перейдем в папку проекта и создадим пару файлов .bowerrc и bower.json. Если Вы уже создаввали проект и он хранится где-нибудь в репозитории GitHub, то достаточно просто клонировать репозиторий к себе в папку с проектом. В ином случаем, создадим файлы «с нуля»:

dmitry@dev:~/projects/djangoproject/static$ cat .bowerrc
{
  "directory" : "libraries",
  "json" : "bower.json",
  "endpoint" : "https://bower.herokuapp.com"
}

Чтобы создать файл конфигурации bower.json можно воспользоваться мастером Bower — bower init или создать/перенести файл самостоятельно:

dmitry@dev:~/projects/djangoproject/static$ cat bower.json

{
  "name": "demon.of.by",
  "version": "2.2.1",
  "description": "demon.of.by project",
  "authors": [
    "Dmitry Vl. Rendov"
  ],
  "license": "MIT",
  "private": true,
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "libraries",
    "test",
    "tests"
  ],
  "dependencies": {
    "jquery": "latest",
    "bootstrap": "latest",
    "font-awesome": "latest",
    "modernizr": "latest",
    "html5shiv": "latest",
    "respond": "latest",
    "SyntaxHighlighter": "latest"
  }
}

Запустим установку выбранных пакетов:

dmitry@dev:~/projects/djangoproject/static$ bower install

В итоге, все пакеты, заявленные в секции dependencies будут найдены, скачены и аккуратно складированы в папке libraries, которая была указана выше в файле .bowerrc.

Примечание: По прошествии времени пакеты можно и обновить:

dmitry@dev:~/projects/djangoproject/static$ bower update

Логично, что bower.json можно допиливать по необходимости. В данном случае я указал минимальный джентельменский набор библиотек, которые используются от проекта к проекту.

4. Установка grunt

Хрю-хрю! Или нет, не так — Grunt-Grunt! И все вдруг стало красиво. Предупреждаю сразу — инструмент вызывает привыкание и является классическим «бесконечным адом перфекциониста«. Чтобы начать пользоваться Grunt-ом, необходимо установить пакет для работы из командной строки grunt-cli всё также глобально, используя ключ -g.

dmitry@dev:~$ sudo npm install -g grunt-cli

Теперь о настройке. Она выполняется в два этапа. Вначале создадим в корневой папке проекта файл package.json, в котором перечислим те пакеты/моудли которые будут выполнять за нас всю грязную, черновую, рутинную работу:

dmitry@dev:~/projects/djangoproject/static$ cat package.json

{
  "name": "demon-of-by",
  "version": "0.0.1",
  "description": "demon.of.by project",
  "main": "package.json",
  "scripts": {
    "test": "grunt test"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/DmitryRendov/demon-theme.git"
  },
  "author": "Dmitry Vl. Rendov",
  "license": {
    "type": "MIT",
    "url": "https://github.com/DmitryRendov/demon-theme/blob/master/LICENSE"
  },
  "bugs": {
    "url": "https://github.com/DmitryRendov/demon-theme/issues"
  },
  "homepage": "http://demon.of.by/",
  "devDependencies": {}
}

Мы поступим довольно скромно и решим только самые насущные проблемы на нашем проекте. Установим модули:

  1. grunt-contrib-copy — копирования некоторых библиотек/ресурсов из libraries в нужную нам папку;
  2. grunt-contrib-clean — очистки/удаления файлов;
  3. grunt-contrib-less — компиляции less в css;
  4. grunt-contrib-concat — склеивания файлов в один (css и js);
  5. grunt-contrib-uglify — сжатия css и js;
  6. grunt-contrib-jshint — проверки соответствия js стандартам ECMA;
  7. grunt-modernizr — установки custom-пакета modernizr.js;
  8. grunt-contrib-watch — наблюдения в реальном времени за файлами и их компиляции/валидации/склеивания/сжатия по каждому чиху.

Для этого установим необходимые нам пакеты

dmitry@dev:~$ npm install grunt --save-dev

dmitry@dev:~$ npm install grunt-contrib-copy --save-dev
dmitry@dev:~$ npm install grunt-contrib-clean --save-dev
dmitry@dev:~$ npm install grunt-contrib-less --save-dev 
dmitry@dev:~$ npm install grunt-contrib-concat --save-dev
dmitry@dev:~$ npm install grunt-contrib-uglify --save-dev
dmitry@dev:~$ npm install grunt-contrib-jshint --save-dev 
dmitry@dev:~$ npm install grunt-contrib-watch --save-dev 
dmitry@dev:~$ npm install grunt-modernizr --save-dev 

Примечание: Вам не обязательно устанавливать вручную все пакеты (кроме первого пакета «grunt») командами, которые указаны выше, в случае если они уже указаны в файле package.json в секции devDependencies. В этом случае Вам будет достаточно сделать

dmitry@dev:~$ npm update 
и все пакеты загрузятся самостоятельно.

После того, как Grunt и все модули установлены создадим файл с именем Gruntfile.js в корне нашего проекта, рядом с ранее созданным файлом package.json. Это основной файл для конфигурации Grunt, который содержит все наши заявленный ранее задачи и описание их работы. Дабы не разливаться в пояснениях весь файл приведен ниже с нужными комментариями по тексту скрипта.

dmitry@dev:~/vhosts/nanny.of.by/public_html$ vim Gruntfile.js

/*
 * Gruntfile.js
 *
 * Copyright (c) 2014 Dmitry Vl. Rendov
 * Licensed under the MIT license.
 * https://github.com/DmitryRendov/inv-theme/blob/master/LICENSE
 */

'use strict';

module.exports = function(grunt) {

  var globalConfig = {
    images : 'images',        /* папка для картинок сайта */
    styles : 'css',           /* папка для готовый файлов css стилей */
    fonts : 'fonts',          /* папка для шрифтов */
    scripts : 'js',           /* папка для готовых скриптов js */
    src : 'src',              /* папка с исходными кодами js, less , etc. */
    bower_path : 'libraries'  /* папка где хранятся библиотеки jquery, bootstrap, SyntaxHighlighter, etc. */
  };

  grunt.initConfig({
    globalConfig : globalConfig,
    pkg : grunt.file.readJSON('package.json'),
    /**
    * Задача "copy"
    *
    * выбрать из библиотек lobalConfig.bower_path = 'libraries'
    * нужные для проекта файлы и скоипровать их в соответствующие папки
    */
    copy : {
      main : {
        files : [{
          expand : true,
          flatten : true,
          src : '<%= globalConfig.bower_path %>/jquery/dist/jquery.min.js',
          dest : '<%= globalConfig.scripts %>/',
          filter : 'isFile'
        }, {
          expand : true,
          flatten : true,
          src : '<%= globalConfig.bower_path %>/html5shiv/dist/html5shiv.min.js',
          dest : '<%= globalConfig.scripts %>/',
          filter : 'isFile'
        }, {
          expand : true,
          flatten : true,
          src : '<%= globalConfig.bower_path %>/bootstrap/dist/js/bootstrap.min.js',
          dest : '<%= globalConfig.scripts %>/',
          filter : 'isFile'
        }, {
          expand : true,
          flatten : true,
          src : '<%= globalConfig.bower_path %>/bootstrap/dist/css/bootstrap.min.css',
          dest : '<%= globalConfig.styles %>/',
          filter : 'isFile'
        }, {
          expand : true,
          flatten : true,
          src : '<%= globalConfig.bower_path %>/font-awesome/css/font-awesome.min.css',
          dest : '<%= globalConfig.styles %>/',
          filter : 'isFile'
        }, {
          expand : true,
          flatten : true,
          src : '<%= globalConfig.bower_path %>/font-awesome/fonts/*',
          dest : '<%= globalConfig.fonts %>/',
          filter : 'isFile'
        }, {
          expand : true,
          flatten : true,
          src : '<%= globalConfig.bower_path %>/respond/dest/respond.min.js',
          dest : '<%= globalConfig.scripts %>/',
          filter : 'isFile'
        }]
      }
    },
    /**
    * Задача "modernizr"
    *
    * используя базовую версию библиотеки modernizr.js
    * выполнить кастомизацию и скопировать в папку scripts проекта
    */
    modernizr : {

      dist : {
        // [REQUIRED] Path to the build you're using for development.
        "devFile" : '<%= globalConfig.bower_path %>/modernizr/modernizr.js',

        // [REQUIRED] Path to save out the built file.
        "outputFile" : '<%= globalConfig.scripts %>/modernizr-custom.min.js',

        // Based on default settings on http://modernizr.com/download/
        "extra" : {
          "shiv" : true,
          "printshiv" : false,
          "load" : true,
          "mq" : false,
          "cssclasses" : true
        },

        // Based on default settings on http://modernizr.com/download/
        "extensibility" : {
          "addtest" : false,
          "prefixed" : false,
          "teststyles" : false,
          "testprops" : false,
          "testallprops" : false,
          "hasevents" : false,
          "prefixes" : false,
          "domprefixes" : false
        },

        // By default, source is uglified before saving
        "uglify" : true,

        // Define any tests you want to implicitly include.
        "tests" : [],

        // By default, this task will crawl your project for references to Modernizr tests.
        // Set to false to disable.
        "parseFiles" : true,

        // When parseFiles = true, this task will crawl all *.js, *.css, *.scss files, except files that are in node_modules/.
        // You can override this by defining a "files" array below.
        // "files" : {
        // "src": []
        // },

        // When parseFiles = true, matchCommunityTests = true will attempt to
        // match user-contributed tests.
        "matchCommunityTests" : false,

        // Have custom Modernizr tests? Add paths to their location here.
        "customTests" : []
      }

    },
    /**
    * Задача "clean"
    *
    * очистить(удалить) production-файлы перед их повторной "сборкой"
    */
   clean : {
      js : ['<%= globalConfig.scripts %>/app.js', '<%= globalConfig.scripts %>/app.min.js'],
      css : ['<%= globalConfig.styles %>/styles.css', '<%= globalConfig.styles %>/styles.min.css']
    },
    /**
    * Задача "less"
    *
    * преобразовать файлы less в css  с последующим сжатием, оптимизацией и 
    * копированием результатов в папку styles проекта
    */
    less : {
      development : {
        options : {
          paths : ["styles"],
        },
        files : {
          "<%= globalConfig.styles %>/styles.css" : "<%= globalConfig.src %>/less/styles.less"
        }
      },
      production : {
        options : {
          paths : ["styles"],
          compress : true,
          yuicompress : true,
          optimization : 2,
          cleancss : true
        },
        files : {
          "<%= globalConfig.styles %>/styles.min.css" : "<%= globalConfig.src %>/less/styles.less"
        }
      }
    },
    /**
    * Задача "watch"
    *
    * отслеживать изменения в файлах less и js  с последующим сжатием, оптимизацией и 
    * копированием результатов в соответствующие папки проекта
    */
    watch : {
      styles : {
        files : ['<%= globalConfig.src %>/less/*.less'],
        tasks : ['less'],
        options : {
          nospawn : true
        }
      },
      scripts : {
        files : ['<%= globalConfig.src %>/js/*.js', '!app.js'],
        tasks : ['js'],
        options : {
          nospawn : true
        }
      }
    },
    /**
    * Задача "concat"
    *
    * "склеить" все файлы js  в один файл, с добавлением "шапки-баннера"
    */
    concat : {
      dist : {
        src : ['<%= globalConfig.src %>/js/**/*.js'],
        dest : '<%= globalConfig.scripts %>/app.js',
        options : {
          banner : ";(function( window, undefined ){ \n 'use strict'; \n",
          footer : "\n}( window ));"
        }
      }
    },
    /**
    * Задача "jshint"
    *
    * проверить все файлы js на предмет соответствия стандартам
    * используя заданные в файле .jshintrc условия верфикации javascript кода
    */
    jshint : {
      all : ['Gruntfile.js', '<%= globalConfig.src %>/js/**/*.js'],
      options : {
        jshintrc : '.jshintrc'
      }
    },
    /**
    * Задача "uglify"
    *
    * сжать финальный js файл и добавить к нему шапку-баннер 
    */
    uglify : {
      options : {
        // the banner is inserted at the top of the output
        banner : '/*! \n * <%= pkg.name %> <%= pkg.version %> (<%= pkg.homepage %>) \n * Copyright <%= grunt.template.today("yyyy") %> Dmitry Vl. Rendov \n * Licensed under MIT (https://github.com/DmitryRendov/inv-theme/blob/master/LICENSE) \n */ \n'
      },
      dist : {
        files : {
          '<%= globalConfig.scripts %>/app.min.js' : ['<%= concat.dist.dest %>']
        }
      }
    }

  });

  grunt.loadNpmTasks('grunt-contrib-copy');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-modernizr');
  grunt.loadNpmTasks('grunt-contrib-less');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-jshint');
  grunt.loadNpmTasks('grunt-contrib-clean');

  // Default task(s).
  grunt.registerTask('default', ['copy', 'modernizr', 'clean:css', 'less', 'js']);
  grunt.registerTask('js', ['clean:js', 'concat', 'jshint', 'uglify']);

};

В двух последних строчках вы указали две групповых задачи, причем вторая зачада вложена в первую. В итоге, если выполнить команду grunt без параметров, будут выполнены все задачи, перечисленные в default:

dmitry@dev:~$ grunt

Running "copy:main" (copy) task
Copied 11 files

Running "modernizr:dist" (modernizr) task

Enabled Extras
>> shiv
>> load
>> cssclasses

Looking for Modernizr references

Downloading source files
cache modernizr-latest.js
cache modernizr.load.1.5.4.js

>> Generating a custom Modernizr build
>> Uglifying

>> Wrote file to js/modernizr-custom.min.js

Running "clean:css" (clean) task
>> 2 paths cleaned.

Running "less:development" (less) task
File css/styles.css created: 0 B > 3.48 kB

Running "less:production" (less) task
File css/styles.min.css created: 3.48 kB > 2.55 kB

Running "clean:js" (clean) task
>> 2 paths cleaned.

Running "concat:dist" (concat) task
File js/app.js created.

Running "jshint:all" (jshint) task
>> 3 files lint free.

Running "uglify:dist" (uglify) task
>> 1 file created.

Done, without errors.

Соответственно, вы можете выполнить отдельно только задачу js:

dmitry@dev:~$ grunt js

Running "clean:js" (clean) task
>> 2 paths cleaned.

Running "concat:dist" (concat) task
File js/app.js created.

Running "jshint:all" (jshint) task
>> 3 files lint free.

Running "uglify:dist" (uglify) task
>> 1 file created.

И наконец, самое полезное — запустить задачу watch и забыть навсегда про необходимость копировать, компилировать, сжимать файлы less, css, js:

dmitry@dev:~$ grunt watch

Running "watch" task
Waiting…
>> File "src/less/base.less" changed.

Running "less:development" (less) task
File css/styles.css created: 0 B > 3.48 kB

Running "less:production" (less) task
File css/styles.min.css created: 3.48 kB > 2.55 kB

Running "watch" task
Completed in 0.805s at Tue Jan 06 2015 11:46:47 GMT+0300 (FET) - Waiting…
>> File "src/js/main.js" changed.

Running "clean:js" (clean) task
>> 2 paths cleaned.

Running "concat:dist" (concat) task
File js/app.js created.

Running "jshint:all" (jshint) task
>> 3 files lint free.

Running "uglify:dist" (uglify) task
>> 1 file created.

Running "watch" task
Completed in 0.814s at Tue Jan 06 2015 11:47:26 GMT+0300 (FET) - Waiting…

Засим всё. Пользуйтесь.

Комментарии 2

  1. Павел — Aug 14, 2015 at 08:03 PM

    Здравствуйте, очень понравилась ваша статья но возникло пару вопросов:
    Как в bower указать последовательность склеивания файлов , он ведь может добавить jQuery в конец очереди и половина плагинов работать не будут?
    Папка проекта у вас тут? https://github.com/DmitryRendov/demon-theme

  2. Павел — Aug 14, 2015 at 08:13 PM

    И еще вопрос , если в bower например какой нибудь слайдер с картинками , примерами картинок и css , как тогда быть? Или если в нем несколько папок/подпапок?

  • 1
Разрешённые теги: <b><i><br>