Nice build frontend project

  • Tutorial
In this article, we will analyze in detail the process of assembling the front-end project, which has taken root in my daily work and has greatly facilitated the routine.

The article does not claim to be the ultimate truth, since today there are a large number of different assemblers and approaches to assembly, and everyone chooses to taste. I will only share my thoughts on this topic and show my workflow.

UPD (March 13, 2015): Replaced several plugins with more relevant ones + solved the problem with importing CSS files inside SCSS


We will use the Gulp collector . Accordingly, Node js must be installed on your system. We will not consider installing a node for a specific platform, because it is google in a couple of minutes.
And to begin with, I will answer the question - why Gulp?

Of more or less tolerable alternatives, we have Grunt and Brunch .

When I just started to get involved in assemblers, both Grunt and Gulp were already on the market. The first appeared earlier and therefore has a larger community and a variety of plugins. According to npm :
Grunt - 11171 package
Gulp - 4371 package

But Grunt seemed a bit too verbose to me. And after reading several comparison articles, I preferred Gulp for its simplicity and clarity.

Brunch is a relatively young project, with all the pros and cons resulting from this. I watch him with interest, but have not yet used it in my work.

Proceed:

Create a folder for our project, for example, “habr”. Open it in the console and execute the command:

npm init

You can just press Enter on all questions of the installer, because Now it does not matter.
As a result, in the folder with the project, we will generate the package.json file, something like this:

{
  "name": "habr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

We will slightly modify it to our needs:

{
  "name": "habr",
  "version": "1.0.0",
  "description": "",
  "author": "",
  "license": "ISC",
  "dependencies": {
      "gulp": "^3.8.11"
   }
}

In the dependencies block, we indicated that we need gulp and we will immediately register all our plugins.

Plugins

gulp-autoprefixer - automatically adds vendor prefixes to CSS properties (a couple of years ago I would kill for such a tool)
gulp-minify-css - needed to compress the
browser-sync CSS code - with this plugin we can easily deploy a local dev server with blackjack and livereload, as well as with its help, we can make a tunnel to our localhost, so that it would be easy to demonstrate layout for the customer
gulp-imagemin - for compressing images
imagemin-pngquant - additions to the previous plugin, for working with PNG
gulp-uglify - will compress our JS
gulp-sass - to compile our SCSS code
Not for the sake of holivara
I used LESS in my work for a very long time. I was very impressed with this preprocessor for its speed and ease of learning. I even made a report on it at a Rostov hackathon. And in particular in this report, I did not speak very flattering about SASS.
But time passed, I became older and wiser :) and now I joined this preprocessor.
The basis of my dissatisfaction with SASS - was that I do not write in rub. And when it was necessary to compile SASS / SCSS code - you had to drag in the project with the necessary bundles - which really upset me.
But that all changed with the advent of such a thing as LibSass . This is the C / C ++ compiler port for SASS. The gulp-sass plugin uses it. Now we can use SASS in the native node environment - which makes me extremely happy.

gulp-sourcemaps - let's take css sourscemaps to generate, which will help us with debugging the
gulp-rigger code - it's just a killer feature. The plugin allows you to import one file into another with a simple design
//= footer.html

and this line at compilation will be replaced by the contents of the file footer.html
gulp-watch - Will be needed to monitor file changes. I know that Gulp has a built-in watch, but I had some problems with it, in particular, he did not see the newly created files, and had to restart it. This plugin solved the problem (I hope this will be fixed in future versions of gulp).
rimraf - rm -rf for the node

Install all the plugins and get the following package.json in the output:

{
  "name": "habr",
  "version": "1.0.0",
  "description": "",
  "author": "",
  "license": "ISC",
 "dependencies": {
    "browser-sync": "^2.2.3",
    "gulp": "^3.8.11",
    "gulp-autoprefixer": "^2.1.0",
    "gulp-imagemin": "^2.2.1",
    "gulp-minify-css": "^1.0.0",
    "gulp-rigger": "^0.5.8",
    "gulp-sass": "^1.3.3",
    "gulp-sourcemaps": "^1.5.0",
    "gulp-uglify": "^1.1.0",
    "gulp-watch": "^4.1.1",
    "imagemin-pngquant": "^4.0.0",
    "rimraf": "^2.3.1"
  }
}


Bower

I can’t imagine my work without the Bower package manager, and I hope so too. If not, then read about what it is and what it is eaten with here .
Let's add it to our project. To do this, execute the command in the console:

bower init

You can also Enter all questions.
At the end, we get something like this bower.json file:

{
  "name": "habr",
  "version": "0.0.0",
  "authors": [
    "Insayt "
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ]
}

And we modify it to the state we need:

{
  "name": "habr",
  "version": "0.0.0",
  "authors": [
    "Insayt "
  ],
  "license": "MIT",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ],
  "dependencies": {
    "normalize.css": "*",
    "jquery": "2.*"
  }
}

In the dependencies block, we will indicate the dependencies of our project. Now just for the test it's normalize and jQuery (although I don’t remember when I started the project without these things).
And of course, install them with the command:

bower i

Well, now the most interesting. Create the structure of our project and configure the collector.

Project Structure:


This is a very controversial moment. Of course, projects are different, as are the preferences of the developers. One has only to look at the yeoman.io website (by the way, this is a very cool tool that provides a large number of prepared frameworks for a project with all sorts of goodies. Definitely worth a look at it). We will not invent anything and make the simplest structure.

First we need 2 folders. One (src) in which we will actually write the code, and the second (build), in which the collector will spit out the finished files. Add them to the project. Our current structure looks like this:



In the src folder, create a typical structure of an average project. Let's make the main files in the js / and style / folders and create the first html page of such content.

index.html
Я собираю проекты как рок звезда
Header
Content

The src folder structure will now look like this:



Everything is trivial here:
fonts -
img fonts -
js pictures - scripts. At the root of this folder will be only the main.js file, which is useful to us for assembly. All your js files - you will need to put in the partials
style folder - styles. There is also only main.scss in the root, and the working files in the partials
template folder - here we will store repeating pieces of html code
All the html pages that we make up will lie in the src root /
Add the first js and scss files to partials and finally - let's go to the root of our project and create the gulpfile.js file there. The entire project folder now looks like this:



Now everything is ready to configure our collector, so let's rock!

Gulpfile.js

All magic will be enclosed in this file. To start, we import all our plugins and gulp itself

gulpfile.js
'use strict';
var gulp = require('gulp'),
    watch = require('gulp-watch'),
    prefixer = require('gulp-autoprefixer'),
    uglify = require('gulp-uglify'),
    sass = require('gulp-sass'),
    sourcemaps = require('gulp-sourcemaps'),
    rigger = require('gulp-rigger'),
    cssmin = require('gulp-minify-css'),
    imagemin = require('gulp-imagemin'),
    pngquant = require('imagemin-pngquant'),
    rimraf = require('rimraf'),
    browserSync = require("browser-sync"),
    reload = browserSync.reload;

Of course, it’s not necessary to do so. There is a gulp-load-plugins plugin which allows you not to write all this noodles from require. But I like it when I clearly see what is connected and where, and if I want, I can turn it off. For this I write in the old fashioned way.

We also create a js object in which we write all the paths we need, so that if necessary it is easy to edit them in one place:

var path = {
    build: { //Тут мы укажем куда складывать готовые после сборки файлы
        html: 'build/',
        js: 'build/js/',
        css: 'build/css/',
        img: 'build/img/',
        fonts: 'build/fonts/'
    },
    src: { //Пути откуда брать исходники
        html: 'src/*.html', //Синтаксис src/*.html говорит gulp что мы хотим взять все файлы с расширением .html
        js: 'src/js/main.js',//В стилях и скриптах нам понадобятся только main файлы
        style: 'src/style/main.scss',
        img: 'src/img/**/*.*', //Синтаксис img/**/*.* означает - взять все файлы всех расширений из папки и из вложенных каталогов
        fonts: 'src/fonts/**/*.*'
    },
    watch: { //Тут мы укажем, за изменением каких файлов мы хотим наблюдать
        html: 'src/**/*.html',
        js: 'src/js/**/*.js',
        style: 'src/style/**/*.scss',
        img: 'src/img/**/*.*',
        fonts: 'src/fonts/**/*.*'
    },
    clean: './build'
};


Create a variable with the settings of our dev server:

var config = {
    server: {
        baseDir: "./build"
    },
    tunnel: true,
    host: 'localhost',
    port: 9000,
    logPrefix: "Frontend_Devil"
};


We collect html

Let's write a task for html assembly:

gulp.task('html:build', function () {
    gulp.src(path.src.html) //Выберем файлы по нужному пути
        .pipe(rigger()) //Прогоним через rigger
        .pipe(gulp.dest(path.build.html)) //Выплюнем их в папку build
        .pipe(reload({stream: true})); //И перезагрузим наш сервер для обновлений
});

Let me remind you that rigger is our plugin that allows you to use this design to import files:

//= template/footer.html

Let's use it in action!
In the src / template / folder, create header.html and footer.html files with the following contents

header.html
Header


footer.html
Footer


and change our index.html file like this:
Я собираю проекты как рок звезда


    //= template/header.html
    
Content
//= template/footer.html


It remains to go to the console and run our task with the command:

gulp html:build

After it works, go to the build folder and see our index.html file there, which turned into this:

Я собираю проекты как рок звезда
Header
Content

It's just amazing!

I remember how many inconveniences it was to run through all the pages that were laid out and to make changes to some part repeating on them. Now this is done conveniently in one place.

Build javascript

The script assembly task will look like this:

gulp.task('js:build', function () {
    gulp.src(path.src.js) //Найдем наш main файл
        .pipe(rigger()) //Прогоним через rigger
        .pipe(sourcemaps.init()) //Инициализируем sourcemap
        .pipe(uglify()) //Сожмем наш js
        .pipe(sourcemaps.write()) //Пропишем карты
        .pipe(gulp.dest(path.build.js)) //Выплюнем готовый файл в build
        .pipe(reload({stream: true})); //И перезагрузим сервер
});

Remember our main.js file?
The whole idea here is to use rigger to include in it all the js files we need in the order we need. It is for the sake of control over the connection order - I do it this way, instead of asking gulp to find all * .js files and glue them.

Often, when searching for the place of an error, I turn off some files from the assembly in order to localize the place of the problem. If mindlessly glue all .js - debug will be complicated.

Fill out our main.js:

/*
 * Third party
 */
//= ../../bower_components/jquery/dist/jquery.js
/*
 * Custom
 */
//= partials/app.js

This is exactly what I do on combat projects. At the top of this file is always the connection of dependencies, below the connection of my own scripts.

By the way, bower packages can be connected through a plugin like gulp-bower . But again, I do not do this, because I want to independently determine what, where and how it will connect.

It remains only to launch our task from the console with the command:

gulp js:build  

And in the build / js folder - we will see our compiled and compressed file.

We collect styles


Let's write a task to build our SCSS:

gulp.task('style:build', function () {
    gulp.src(path.src.style) //Выберем наш main.scss
        .pipe(sourcemaps.init()) //То же самое что и с js
        .pipe(sass()) //Скомпилируем
        .pipe(prefixer()) //Добавим вендорные префиксы
        .pipe(cssmin()) //Сожмем
        .pipe(sourcemaps.write())
        .pipe(gulp.dest(path.build.css)) //И в build
        .pipe(reload({stream: true}));
});

Everything is simple here, but you may be interested in the auto-fix settings. By default, it writes the prefixes necessary for the last two versions of browsers. In my case, this is enough, but if you need other settings, you can find them here .

With styles, I do the same as with js, but only instead of rigger, I use the import built into SCSS.
UPD (March 13, 2015): Some people have a problem importing css files inline. As it turned out, gulp-sass does not know how to do this, and the output gives a simple CSS import. But our gulp-minify-css plugin solves this issue, which replaces CSS import with the contents of the file.
Our main.scss will look like this:
/*
* Third Party
*/
@import "../../bower_components/normalize.css/normalize.css";
/*
* Custom
*/
@import "partials/app";

This way you can easily control how styles are connected.
Let's check our task by running
gulp style:build


Collecting Pictures


Task in the pictures will look like this:

gulp.task('image:build', function () {
    gulp.src(path.src.img) //Выберем наши картинки
        .pipe(imagemin({ //Сожмем их
            progressive: true,
            svgoPlugins: [{removeViewBox: false}],
            use: [pngquant()],
            interlaced: true
        }))
        .pipe(gulp.dest(path.build.img)) //И бросим в build
        .pipe(reload({stream: true}));
});

I use the default imagemin settings, except for interlaced. Read more about the API of this plugin here .

Now, if we put some picture in src / img and run the command:

gulp image:build

we’ll see our optimized image in build. Also gulp will kindly write in the console how much space he saved for users of our site.

Fonts


I usually don’t need to manipulate fonts, but in order not to destroy the paradigm “We work in src / and assemble in build /”, I just copy files from src / fonts and paste them into build / fonts. Here is the task:

gulp.task('fonts:build', function() {
    gulp.src(path.src.fonts)
        .pipe(gulp.dest(path.build.fonts))
});


Now let's define a task called "build", which will run everything that you and I have uploaded here:

gulp.task('build', [
    'html:build',
    'js:build',
    'style:build',
    'fonts:build',
    'image:build'
]);


File changes


In order not to climb into the console all the time, let's ask gulp to run the necessary task every time you change a file. To do this, he will write such a task:

gulp.task('watch', function(){
    watch([path.watch.html], function(event, cb) {
        gulp.start('html:build');
    });
    watch([path.watch.style], function(event, cb) {
        gulp.start('style:build');
    });
    watch([path.watch.js], function(event, cb) {
        gulp.start('js:build');
    });
    watch([path.watch.img], function(event, cb) {
        gulp.start('image:build');
    });
    watch([path.watch.fonts], function(event, cb) {
        gulp.start('fonts:build');
    });
});

With understand there should be no problem. We just go along our paths defined in the path variable, and in the function that is called when the file changes, we ask you to run the task we need.

Try running in the console:

gulp watch

And swap different files.
Well, isn’t it cool?

Web server


To enjoy the miracle of livereload - we need to create a local web server for ourselves. To do this, write the following simple task:

gulp.task('webserver', function () {
    browserSync(config);
});

There is even nothing to comment on. We just start the livereload server with the settings that we defined in the config object. In addition, gulp will politely open our project in a browser, and in the console will write links to the local server, and to the tunnel, which we can drop to the customer for demonstration.

Cleaning


If you add a picture, then run the image: build task and then delete the picture - it will remain in the build folder. So it would be convenient to periodically clean it. Let's create a simple task for this.

gulp.task('clean', function (cb) {
    rimraf(path.clean, cb);
});

Now when you run the command
gulp clean

just the build folder will be deleted.

Final chord


The last thing - we will determine the default task, which will run our entire assembly.

gulp.task('default', ['build', 'webserver', 'watch']);


Finally your gulpfile.js will look something like this .
Now run in the console
gulp

And voila. The preparation for your project is ready and waiting for you.

A few words in conclusion


This article was conceived as a way to once again refresh the subtleties of assembling frontend projects, and to easily transfer this experience to new developers. You do not have to use just such an assembly option on your projects. There is yeoman.io where you will find generators for almost any need.
I wrote this collector for three reasons.
- I like to use rigger in my html code
- In almost all assemblies that I have seen - a temporary folder (usually .tmp /) is used to record intermediate assembly results. I do not like this approach and I wanted to get rid of temporary folders.
“And I wish all this was out of my box.”

You can download my working version of the collector on my github .

I hope this article has been helpful to you.

PS About all errors, shortcomings and jambs - please write in a personal.

Also popular now: