[Из песочницы] Запускаем Gulp с вотчерами на обычном хостинге через админпанель

У тебя есть сайт с админпанелью и ты используешь или только собираешься использовать Gulp в этом проекте? Хочешь максимально работать с сайтом через админпанель, включая контроль над генератором ресурсов Gulp? Тогда под катом я покажу тебе простой способ управления Gulp’ом с вотчерами на сервере прямо из админпанели.


Это небольшой туториал по настройке и запуску Gulp’а на сервере средствами любой админпанели на PHP. Подразумевается, что запускается Gulp не для единовременной сборки ресурсов, а в режиме с вотчерами. Отлично подходит, если ты работаешь с проектом через Deployment Tools и локально он у тебя не запущен. При этом каждый раз билдить, например, SCSS локально и заливать результат уже надоело.


1. Тонкости хостинга


Так уж получилось, на моём хостинге не было поддержки Node. Пришлось поставить её отдельно в свою папку рядом с сайтами. Какую версию ставить, не принципиально, я использовал Node.js v5.12.0. Для установки просто распакуй архив куда-нибудь к себе на хостинг и добавь NodeJS bin в Path (подробнее ниже).


Обрати внимание, если на твоём хостинге включено изолирование сайтов, то папку с Node нужно добавить в общедоступные. В противном случае, процессу gulp будет отказано в доступе к Node.


2. Настройка Node и Gulp


Тут ты подготовишь всё необходимое для запуска Gulp на сервере. В принципе, им уже можно будет отлично пользоваться через SSH, но основная цель: Настроить управление через админпанель.


2.1 Настраиваем Node


Если ты уже пользовался npm’ом на хостинге и у тебя там уже стоит Node, то пол дела сделано и можешь сразу переходить к пункту 2.2.


Для установки на сервере Gulp и всего необходимого для его запуска тебе нужно будет в корне проекта создать файл package.json. Искренне надеюсь, что корень проекта у тебя лежит чуть выше папки public_html.


Примерное содержимое файла package.json:


{
  "name": "Project Namet",
  "version": "1.0.0",
  "devDependencies": {
    "browser-sync": "^2.14.0",
    "gulp": "~3.9.0",
    "gulp-autoprefixer": "^3.1.0",
    "gulp-browserify": "~0.5.1",
    "gulp-clean-css": "^2.0.12",
    "gulp-concat": "~2.6.0",
    "gulp-notify": "^2.2.0",
    "gulp-plumber": "^1.1.0",
    "gulp-sass": "~2.1.0",
    "gulp-sourcemaps": "^1.6.0",
    "gulp-uglify": "^1.5.1",
    "gulp-util": "^3.0.7"
  },
}

Набор плагинов для Gulp может быть любой, нужно смотреть, что именно ты используешь и зачем тебе это нужно. Что касается BrowserSync, то его запустить так и не удалось. Почему? Читай в самом конце в списке возможных проблем.


Далее, заблаговременно подключившись к серверу по SSH, ты должен настроить Path, чтобы установленная вручную Node попала в директории запуска (это, кстати, вовсе не гарантирует, что процесс сайта будет видеть эту Node или вообще хоть что-то. Там своё окружение со своим Path. Его настроишь уже через PHP.).


Установи Gulp и его компоненты из директории, где лежит package.json:


npm imstall

У тебя появится в этой директории папка node_modules, в которой будет всё что нужно для запуска Gulp.


Теперь нам нужно сконфигурировать Gulp.


2.2 Настраиваем Gulp под проект


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


Для этого создай файл gulpfile.js рядом с файлом package.json с примерным наполнением:


'use strict';

var gulp = require('gulp');
var plumber = require('gulp-plumber');
var notify = require("gulp-notify");
var sass = require('gulp-sass');
var minifyCss = require('gulp-clean-css');
var sourcemaps = require('gulp-sourcemaps');
var autoprefixer = require('gulp-autoprefixer');
var browserSync = require('browser-sync').create();

var errorHandler = {
    errorHandler: notify.onError({
        title: 'Ошибка в плагине <%= error.plugin %>',
        message: "Ошибка: <%= error.message %>"
    })
};

gulp.task('scss', function(){
    gulp.src('./scss/**/*.scss')
        .pipe(plumber(errorHandler))
        .pipe(sourcemaps.init())
        .pipe(sass())
        .pipe(autoprefixer())
        .pipe(minifyCss({compatibility: 'ie8'}))
        .pipe(sourcemaps.write({
            includeContent: false
        }))
        .pipe(gulp.dest('./public_html/css/user'));
});

gulp.task('compiler', [
    'scss'
]);

gulp.task('watch', ['compiler'], function(){
    /*
    browserSync.init({
        host: 'http://mysite.ru',
        online: false,
        files: [
            './public_html/css/user/app.css'
        ]
    });
    */
    gulp.watch('./scss/**/*.scss', ['scss']);
});

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

Я покажу на примере генерации только CSS, ты же можешь настроить Gulp как хочешь.
Подразумевается, что рядом с gulpfile.js лежит папка scss, в которой полная структура всех SCSS файлов. Если что, то найти и поменять путь к SCSS файлам в gulpfile.js не сложно, их всего два:


  1. Место, куда смотрит таск генерации CSS gulp.src('./scss/**/*.scss')
  2. Место, куда смотрит Watcher gulp.watch('./scss/**/*.scss', ['scss']);

2.3 Проверка готовности инструментов


Если всё сделано правильно, то уже сейчас можно, подключившись по SSH, перейти в папку с gulpfile.js и выполнить там:


gulp

При этом увидим какой-то дебаг от Gulp, либо предупреждения, которые тебе предстоит разрулить до перехода к следующему пункту. Сразу скажу, что проблема может быть в конфигурировании переменной Path для текущего подключенного пользователя, в ней обязательно должна быть папка bin от Node и папка /node_modules/.bin, которую должен устанавливать в Path сам NPM.


Другого рода проблемы, скорее будут относиться к неверной настройке gulpfile.js.


3 Подготовка PHP для работы с Gulp


Для этого тебе сначала нужно понять, как это вообще будет работать, не опираясь на конкретный язык программирования, и лишь потом решить, что же из PHP нужно использовать. Благо вариантов не очень много, да и продумывать почти нечего.


Алгоритм следующий:


  • Нужно при запросе на включение из админпанели запустить процесс Gulp в фоне и сохранить его PID где-нибудь.
  • При запросе на выключение нужно проверить PID и убить работающий под ним процесс Gulp.
  • При запросе статистики можно проверить существование работающего по текущему PID процесса Gulp и вернуть результат.
  • Весь output фонового процесса Gulp стоит сохранять в файл и при желании можно также настроить отправку содержимого этого файла в админпанель по запросу (В статье не рассматривается этот пункт, можешь реализовать его в виде домашнего задания).

Вроде просто, теперь, какие средства PHP тебе понадобятся.


Самое первое и простое, это класс ExecProcess где-то из обсуждения exec на сайте PHP. Я его немного модифицировал и использую вот такой:


envList)){
            $command .= 'export PATH=$PATH:'.implode(":", $this->envList).'; ';
        }
        if(!empty($this->root)){
            $command .= 'cd '.$this->root.'; ';
        }
        if(!empty($this->command)){
            $command .= 'nohup ' . $this->command . ' > /dev/null 2>&1 & echo $!;';
        }
        if(!empty($this->additional)){
            $command .= implode("; ", $this->additional);
        }
        exec($command, $op);
        $this->pid = intval($op[0]);
    }

    public function setPid($pid)
    {
        $this->pid = $pid;
    }

    public function setRoot($root)
    {
        $this->root = $root;
    }

    public function setEnv($envList)
    {
        $this->envList = $envList;
    }

    public function setCommand($commandLine)
    {
        $this->command = $commandLine;
    }

    public function setAdditional($additionalCommands)
    {
        $this->additional = $additionalCommands;
    }

    public function getPid()
    {
        return $this->pid;
    }

    public function status()
    {
        if(empty($this->pid)) return false;
        $command = 'ps -p ' . $this->pid;
        exec($command, $op);
        return isset($op[1]);
    }

    public function start()
    {
        $this->runCom();
    }

    public function stop()
    {
        if(empty($this->pid)) return true;
        $command = 'kill ' . $this->pid;
        exec($command);
        return !$this->status();
    }
}

И класс, который будет запускать именно Gulp и контролировать его состояние, GulpProcess:


execProcess = new ExecProcess();
        // Начальная директория для запускаемого нами в итоге скрипта (gulp). У меня так сложилось, что она находится выше на один уровень скрипта index.php. Простыми словами, директория, в которой лежит gulpfile.js
        $this->execProcess->setRoot("../");
        // Те самые директории (абсолютные пути) к Node и node_modules для Path
        $this->execProcess->setEnv([
            NODE_MODULES_BIN,
            NODE_BIN,
        ]);
        /* Тестировал. Может пригодиться)
        $this->execProcess->setAdditional([
            "echo \$PATH",
            "pwd",
            "whoami",
            "ps -ela",
            "id",
        ]);
        */
        $this->execProcess->setCommand('gulp');
    }

    public function start()
    {
        if(!$this->isActive()){
            $this->execProcess->start();
            $this->setPid();
            return $this->checkStatus();
        }
        return true;
    }

    public function stop()
    {
        if($this->isActive()){
            $this->execProcess->stop();
            $this->clearPid();
        }
        return true;
    }

    public function isActive()
    {
        return $this->checkStatus();
    }

    private function getPid()
    {
        if(is_file($this->tempPidFile)){
            $pid = intval(file_get_contents($this->tempPidFile));
            $this->execProcess->setPid($pid);
            return $pid;
        }
        return null;
    }

    private function setPid()
    {
        file_put_contents($this->tempPidFile, $this->execProcess->getPid());
    }

    private function clearPid()
    {
        if(is_file($this->tempPidFile)){
            unlink($this->tempPidFile);
        }
        $this->execProcess->setPid(null);
    }

    private function checkStatus()
    {
        $pid = $this->getPid();
        if(!empty($pid)){
            if($this->execProcess->status()){
                return true;
            }
            $this->clearPid();
            return false;
        }
        return false;
    }
}

Вроде всё лаконично и понятно. Осталось привязать это всё к админпанели.


4. Настройка экшенов админпанели


Какую систему ты используешь и как работаешь с экшенами известно лишь тебе одному. Я приведу пример своих экшенов, но уже всё должно быть очевидно:


start();
    }

    public function stopGulpProcess()
    {
        $gulpProcess = new GulpProcess();
        return $gulpProcess->stop();
    }

    public function getGulpStatus()
    {
        $gulpProcess = new GulpProcess();
        $this->jsonData["is_active"] = $gulpProcess->isActive();
        return true;
    }
}

Как было сказано в пункте 3, можно также получить текущий output от процесса Gulp, но для этого сначала надо модифицировать класс ExecProcess, чтобы писал он весь output Не в /dev/null, а в конкретный файл.


Итог


Теперь ты имеешь в руках довольно простой и приятный инструмент для управления процессом Gulp на сервере (даже на обычном хостинге) и не нужно заниматься компиляцией ресурсов на локальной машине, работая через Deployment Tools.


Все эти наработки можно красиво обернуть в Composer плагин и подшлифовать для конкретной системы (например, Yii2). Этим я обязательно займусь, когда переведу CMS на неё.


В купе с возможностью править файлы ресурсов на сервере через админпанель ты получаешь мощный инструмент, не требующий от тебя настроенной рабочей машины. Можно срочно поправить мелкие баги по требованию заказчика из любого места, с любого компьютера и т.д.


Возможные проблемы и попытки решения


Отсутствие NodeJS на хостинге

Всё просто, качаем NodeJS в виде архива и заливаем на хостинг. Далее обязательно прописываем его в Path (Это поможет корректно работать NPM и Gulp при запуске через SSH).


Система молча запускается, но процесс неактивен.

После запуска через админпанель может так получиться, что процесс запустится и сразу же выключится. Это связано с какими-то проблемами запуска Gulp, которые, к сожалению, ты не увидишь. Всё ведь посылается в /dev/null. Надо настроить класс ExecProcess так, чтобы output сохранялся в файл, и уже в нём искать проблему.


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

Это ты сможешь узнать только из файла с output’ом процесса Gulp. Это скорее всего означает, что на хостинге включено изолирование сайтов и твой текущий сайт не имеет доступа к папке, где установлен Node. Это решается средствами управления твоего хостинга. Нужно сделать папку с Node общедоступной.


Какие-то ошибки при запуске Gulp

Ну тут ты уже накосячил с gulpfile.js, в теории, пример из статьи должен работать (я его, конечно, немного модифицировал, и он отличается от настоящего, рабочего в проекте, но правки были незначительные), попробуй оставить в нём минимум тасков и логики.


Что-то не работает BrowserSync

Отличная это штука, скажу я тебе, но мне так и не удалось её запустить. Всё дело в том, что она работает на определённых портах, используя Node, но на многих хостингах доступ к портам заблокирован. Если хочешь пользоваться BrowserSync’ом, придётся раскошелиться на VDS.


Я переживаю, что постоянно будут запускаться новые фоновые процессы Gulp

Вообще это контролирует класс GulpProvess, он настраивает класс ExecProcess таким образом, что контроль текущего процесса идёт по его PID. Как только процесс был создан, система получает его PID и дальше всегда работает с ним, при попытке создать новый процесс, при рабочем старом, система этого не позволит.


На крайний случай ты всегда можешь подключиться по SSH и проверить список активных запущенных процессов.


Ищи тот, что называется gulp, это и будет твой фоновый процесс.

Комментарии (3)

  • 13 декабря 2016 в 13:10

    0

    У вас ошибка в примере с установкой зависимостей: «npm imstall»
  • 13 декабря 2016 в 14:54

    +1

    И самый главный вопрос — зачем? Gulp, Webpack, etc — это те штуки, которые нафиг не упали на проде висеть. Компиляция должна происходить локально и в прод деплоиться уже собранные сырцы.


    А ли я не прав?

  • 13 декабря 2016 в 16:08

    +1

    Не ожидал в 2016 году прочесть статью о новом остроумном способе редактирования файлов на боевом сервере

© Habrahabr.ru