Cum configurezi un Gulpfile.js?

Pentru că de câteva ori am fost întrebat despre cum ar trebui configurat Gulp pentru frontend „normal” (adică doar slice, fără ES6, Babel sau alte minuni), m-am gândit să fac un mic tutorial despre asta.

De ce Gulp și nu Grunt? Din două motive:

  • în testele mele, Gulp a fost întotdeauna mai rapid. Vorbim de diferențe majore, de secunde bune. Același task care dura ~2 secunde în Grunt, „costă” în jur de 200ms în Gulp.
  • Gulp nu trebuie instalat global, putând fi instalat doar ca modul Node. Din câte am înțeles, Grunt 1.0 permite în sfârșit asta, dar… too little, too late :smiley:

Primul pas este generarea unui package.json, fișierul ce se va ocupa, printre altele, de managementul dependențelor.

Minimum necesar este reprezentat de următoareale linii:

{
  "version": "0.0.1",
  "scripts": {
    "gulp": "gulp"
  }
}

Asta ne va permite să rulăm Gulp fără a-l avea instalat global, prin npm run gulp.

În același folder unde avem package.json deschidem o consolă și instalăm Gulp: npm i -D gulp. Opțional, poți instala gulp și global rulând npm i -g gulp.

Facem și un fișier nou, numit Gulpfile.js în care punem minimum necesar:

var gulp = require('gulp');

gulp.task('default', function() {

});

În acest moment putem rula npm run gulp (sau doar gulp dacă ai instalat Gulp global). Evident, nu se întâmplă nimic; trebuie să adăugăm task-uri!


Ce vrem să facem?

CSS

  • Compilare Sass;
  • Rulare Autoprefixer;
  • Generare sourcemap;

JavaScript

  • Rulare JSHint;
  • Concatenare a fișierelor JavaScript;
  • Minificare a fișierelor JavaScript;
  • Genearare sourcemap;

Altele

  • Curățare (ștergerea fișierelor compilate)
  • Watch (cu livereload);
  • Copierea câtorva fișiere (e.g. fonts, imagini);
  • Atât JS-ul cât și CSS-ul va fi generat atât în format normal cât și minified.

Primii pași

Plecăm de la premisa că structura directoarelor arată așa:

- package.json
- Gulpfile.js
- src
    \---assets
        +---fonts
        +---images
        +---javascripts
        |   \---main
        |   \---vendor
        \---stylesheets
            \---screen
            \---grid

Vom instala întâi lucrurile ce sunt comune în majoritatea task-urilor:

npm i -D gulp-sourcemaps gulp-util gulp-plumber gulp-clone merge-stream gulp-rename path fs

Și le vom defini în Gulpfile.js:

var gulp = require('gulp');
+ var livereload  = require('gulp-livereload');
+ var sourcemaps = require('gulp-sourcemaps');
+ var gutil = require('gulp-util');
+ var plumber = require('gulp-plumber');
+ var clone = require('gulp-clone');
+ var merge = require('merge-stream');
+ var rename = require('gulp-rename');
+ var path = require('path');
+ var fs = require('fs');

Ne vor fi necesare un pic mai târziu :slight_smile:

De asemenea, tot în Gulpfile.js adăugăm o funcție ce ne va ajuta la afișarea erorilor fără a întrerupe task-urile:

function err(err) {
  var displayErr = gutil.colors.red(err.message);
  gutil.log(displayErr);
  gutil.beep();
  this.emit('end');
}

De asemenea, pentru a avea un config cât mai DRY, vom defini toate fișierele într-un array multidimensional:

var srcFiles = {
  scripts : {
    defaultDest: 'dist/assets/javascripts',
    main : {
      files: ['src/assets/javascripts/main/**/*.js'],
      dest: 'dist/assets/javascript2',
      skipLint: false
    },
    utils: [],
    admin : [],
    vendor : []
  },

  stylesheets: {
    defaultDest: 'dist/assets/stylesheets',
    screen : {
      files: ['src/assets/stylesheets/screen/screen.scss'],
      watch: ['src/assets/stylesheets/screen/**/*.scss'],
      dest: 'dist/assets/stylesheets2'
    },
    grid : [
      'src/assets/stylesheets/grid/grid.scss'
    ],
    vendor : [
      'src/assets/stylesheets/vendor/vendor.scss'
    ],
  },

  assets: {
    images : ['src/assets/images/**/*'],
    content : ['src/content/**/*'],
    fonts : ['src/assets/fonts/**/*'],
  }
};


Atât pentru JS cât și pentru CSS avem câteva opțiuni:

  • putem specifica destinația fișierelor compilate.
  • JS-ul poate fi sau nu linted

Pentru CSS avem nevoie de ceva mai mult control, prin urmare folosim path spre un fișier pentru task-ul sass și un glob pentru watch. Asta pentru că vrem să compileze doar un singur fișier dar să facă watch la toate.


CSS

Instalăm modulele necesare:

npm i -D gulp-sass gulp-postcss autoprefixer gulp-cssmin

După care, În Gulpfile.js adăugăm:

var sass = require('gulp-sass');
var postcss = require('gulp-postcss');
var autoprefixer = require('autoprefixer');
var cssmin = require('gulp-cssmin');

Atât pentru CSS cât și pentru JS vom folosi câte o funcție spre care vom trimite doar numele (e.g. screen, grid șamd):

function getSassTask(name, style) {
  var sassOptions = { outputStyle: 'expanded' };
  var dest = srcFiles.stylesheets[name].dest || srcFiles.stylesheets.defaultDest;
  var source = gulp.src(srcFiles.stylesheets[name].files)
    .pipe(plumber())
    .pipe(sourcemaps.init())
    .pipe(sass(sassOptions)).on('error', err)
    .pipe(postcss([
      autoprefixer({browsers: ['last 2 versions']}),
    ]));

  var pipe1 = source.pipe(clone())
    .pipe(sourcemaps.write('.', { sourceRoot: null }))
    .pipe(gulp.dest(dest));

  var pipe2 = source.pipe(clone())
    .pipe(cssmin())
    .pipe(rename({suffix: '.min'}))
    .pipe(sourcemaps.write('.', { sourceRoot: null }))
    .pipe(gulp.dest(dest));

  return merge(pipe1, pipe2).pipe(livereload());
}

Ce se întâmplă?

Pentru că avem nevoie de două versiuni ale fișierelor compilate (una normală, alta minfied), folosim plumber și clone pentru a sparge task-ul în două, după care aplicăm fiecărui subtask transformările necesare:

  1. Primul nu are nevoie de nimic după ce îl compilăm cu sass, prin urmare îi generăm sourcemap-ul și îl scriem pe disc;
  2. Al doilea subtask constă în minificare, redenumirea fișierului, generarea sourcemap-ului și scrierea pe disc.

Acum putem înregistra task-uri noi adăugând doar:

gulp.task('sass:screen', function () {
  return getSassTask('screen');
});

gulp.task('sass:vendor', function () {
  return getSassTask('vendor');
});

Șamd.

Putem grupa toate task-urile Css înregistrând un nou task:

gulp.task('sass', ['sass:screen', 'sass:vendor']);

Dar pentru că dificultatea de a menține așa ceva crește odată cu numărul de task-uri, facem o funcție care grupează task-urile:

function groupTasks(name) {
  return Object.keys(gulp.tasks).filter(function(task){
    return task.indexOf(name) != -1;
  });
}

După care vom putea înregistra din nou task-ul de mai sus, dar ușor diferit:

gulp.task('sass', groupTasks('sass:'));

JavaScript

Pentru JavaScript avem aceeași strategie ca și pentru CSS: facem o funcție și o reutilizăm de câte ori avem nevoie.

Instalăm modulele necesare:

npm i -D gulp-concat gulp-uglify jshint gulp-jshint jshint-stylish

și le adăugăm în Gulpfile.js:

var cssmin = require('gulp-cssmin');

+ var concat = require('gulp-concat');
+ var uglify = require('gulp-uglify');
+ var jshint = require('gulp-jshint');
+ var stylish = require('jshint-stylish');

După care facem și funcția-minune:

function getScriptTask(name) {
  var dest = srcFiles.scripts[name].dest || srcFiles.scripts.defaultDest;;
  var source = gulp.src(srcFiles.scripts[name].files);

  if (!srcFiles.scripts[name].skipLint) {
    source = source.pipe(jshint()).pipe(jshint.reporter('jshint-stylish'))
  }

  source = source.pipe(sourcemaps.init())
    .pipe(concat(name + '.js'));

  var pipe1 = source.pipe(clone())
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest(dest));

  var pipe2 = source.pipe(clone())
    .pipe(rename({
      basename: name,
      suffix: '.min'
    }))
    .pipe(uglify()).on('error', err)
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest(dest));

  return merge(pipe1, pipe2).pipe(livereload());
}

Ce se întâmplă?

La fel ca și la CSS, mai sus, împărțim task-ul în două: o parte doar scrie fișierele concatenate, a doua parte face uglify.

Adițional, ai opțiunea de a sări linting. De exemplu, dacă faci concat la mai multe fișiere asupra cărora nu ai posibilitatea de a impune o anumită calitate a codului (e.g. vendors), e posibil să vrei să poți face măcar concat & uglify.


Adăugăm și grupăm task-urile la fel ca și la CSS:

gulp.task('scripts:main', function() {
  return getScriptTask('main');
});

gulp.task('scripts:vendor', function() {
  return getScriptTask('vendor');
});

gulp.task('scripts', groupTasks('scripts:'));

Altele

Curățare

De multe ori avem nevoie să curățăm folderul dist de toate fișierele compilate în prealabil. Pentru asta instalăm modulul rimraf:

npm i -D gulp-rimraf

și în adăugăm în Gulpfile.js:

var fs = require('fs');
+ var rm = require('gulp-rimraf');

După care adăugăm task-ul, în același fișier:

gulp.task('clean', function() {
  return gulp.src(['dist']).pipe(rm());
});

Mai adăugm un task suplimentar - build - ce va executa întâi clean, apoi toate celelalte task-uri:

gulp.task('build', ['clean'], function(){
  gulp.start(['scripts', 'sass']);
});

modificăm și task-ul default pentru a rula întâi build:

gulp.task('default', ['build'], function() {
  // ...
});

De ce?

Pentru că vrem să putem rula gulp atât o singură dată (build) cât și să-l punem ruleze în timp real, în momentul în care modificăm un fișier (watch).


Cum facem Watch?

gulp.task('default', ['build'], function() {
  livereload.listen();

  Object.keys(srcFiles.stylesheets).forEach(function(source){
    if (source !== 'defaultDest' && srcFiles.stylesheets[source].watch) {
      gulp.watch(srcFiles.stylesheets[source].watch, ['sass:' + source]);
    }
  });

  Object.keys(srcFiles.scripts).forEach(function(source){
    if (source !== 'defaultDest') {
      gulp.watch(srcFiles.scripts[source].files, ['scripts:' + source]);
    }
  });
});

Copierea fișierelor

După cum probabil ți-ai dat seama, avem două foldere: src care nu are ce căuta în producție și dist care nu are ce căuta în Git. Tot în src punem și fonturi sau imagini ce țin de proiect, prin urmare trebuie să le putem copia și pe astea cumva în dist.

Plecăm de la premisa că vrem să copiem tot ce e în obiectul srcFiles.assets, prin urmare adăugăm automat tot:

Object.keys(srcFiles.assets).forEach(function(group){
  gulp.task('copy:' + group, function(){
    return gulp.src(srcFiles.assets[group]).pipe(gulp.dest('dist/assets/' + group)).pipe(livereload());
  });
});

Ajustăm task-ul build:

gulp.task('build', ['clean'], function(){
-  gulp.start(['scripts', 'sass']);
+  gulp.start(['scripts', 'sass', 'copy']);
});

Și adăugăm în task-ul default (pentru a avea și watch):

Object.keys(srcFiles.assets).forEach(function(source){
  gulp.watch(srcFiles.assets[source], ['copy:' + source]);
});

Refactor

Pentru că Gulpfile.js este mai mare decât ar trebui, poate că ar fi bine să-l simplificăm un pic:

  • Extragem toate funcțiile în module separate;
  • Extragem obiectul srcFiles într-un modul separat, disponibil în rădăcina proiectului

Rezultă un fișier frumușel, cu mai puțin de 60 de linii. Tot proiectul se poate vedea aici.


Alte task-uri utile

SVG Sprite

// https://github.com/jkphl/gulp-svg-sprite
var svgSprite = require('gulp-svg-sprite');

gulp.task('icons', function () {
  var config = {
    mode: {
      symbol: {
        dest: '.',
        sprite: 'sprite.svg',
        example: true
      }
    },
    svg: {
      xmlDeclaration: false,
      doctypeDeclaration: false
    }
  };


  return gulp.src(['src/assets/images/svg/*.svg'])
    .pipe(svgSprite(config).on('error', dealWithErrors))
    .pipe(gulp.dest('dist/assets/images'))
  .pipe(livereload());
});

WordPress Tasks

Dacă faci teme de WordPress, poate că ai vrea să păstrezi versiunea & descrierea temei într-un singur loc: package.json. Poți actualiza apoi versiunea din style.css sau din alte alte fișiere:

var replace = require('gulp-replace');
var fs = require('fs');
var pkg = JSON.parse(fs.readFileSync('./package.json'));

gulp.task('replace:style', function(){
  return gulp.src('src/assets/stylesheets/themeinfo.css', { base : './' })
    .pipe(rename('./style.css'))
    .pipe(replace(/(Theme Name:\s?)(.*)/g, '$1' + pkg.name.substring(0,1).toUpperCase() + pkg.name.slice(1)))
    .pipe(replace(/(Author:\s?)(.*)/g, '$1' + pkg.author))
    .pipe(replace(/(Author URI:\s?)(.*)/g, '$1' + pkg.author_uri))
    .pipe(replace(/(Description:\s?)(.*)/g, '$1' + pkg.description))
    .pipe(replace(/(Version:\s?)(.*)/g, '$1' + pkg.version))
    .pipe(replace(/(Text Domain:\s?)(.*)/g, '$1' + pkg.text_domain))
    .pipe(gulp.dest('./'));
});

gulp.task('replace:functions', function(){
  return gulp.src('functions.php', { base : './' })
    .pipe(replace(/(define\('THEME_VERSION', ').*'/g, '$1' + pkg.version + "'"))
    .pipe(gulp.dest('./'));
});

13 Likes