Ускоряем npm-скрипты
Таск раннеры существенно упростили жизнь веб разработчиками автоматизируя рутинные действия связанные с запуском тестов, проверкой кода, объединением в один файл, транспайлингом и прочими не менее полезными делами. Опустим вопрос необходимости подобных инструментов, конечно, можно и без них, но они существенно упрощают жизнь и делают более качественным процесс разработки.
Все пользуются таск раннерами в той или иной мере: кто-то старинным грантом, кто-то постепенно уходящим с арены галпом и многими другими, а кто-то уже во всю использует npm-скрипты.
Последние мы сегодня разберем во всех деталях, а так же способы их ускорения и расширения возможностей
Причины использовать npm как таск раннер
Написанное ниже является субъективным опытом, и на истину последней инстанции не претендует, тем-не-менее является важными причинами дальнейшего повествования.
По мере появления перечисленных выше таск раннеров, они были мной испробованы в бою, и в каждом из них (конечно же) есть плюсы, и минусы. Рассмотрим их вкратце.
У гранта низкий порог вхождения, благодаря конфигам, которые в тот же момент являются его краеугольным камнем, заставляя генерировать их программно, из-за большого количества передаваемых полей. Галп решает эту проблему целиком, добавляя минимализм и скорость выполнения, благодаря стримам, что повышает порог вхождения, требуя от пользователей понимания того, как все работает изнутри.
И так, отойдя от гранта, разобравшись со стримами и написав лаконичный Gulpfile я успокоился и продолжил разработку текущего проекта. Для следующего приложения, я, конечно, снова написал Gulpfile. И для следующих. И для многих других.
После чего сам gulp, и его плагины начали обновляться, а потом и его зависимости, которые бывало нужно было обновлять самостоятельно. Конечно можно не использовать сервисы, следящие за обновлениями пакетов, устранением уязвимостей, исправлением ошибок и всем прочим, но это не самый хороший подход к разработке.
Таким образом мы подошли к главному недостатку большинства такс раннеров: их нужно обновлять, их зависимости нужно обновлять и зависимость зависимостей тоже. Для монолитных приложений это не представляет проблемы. Проблемы начинаются тогда, когда частей приложения много, и каждая из них является независимым модулем, а это ли не основная парадигма разработки на node.js?
Итак, вместо того, что бы обновлять: gulp, gulp-jshint и jshint, я лучше просто буду использовать jshint напрямую, с помощью интерфейса командной строки, который не часто меняется, значительно упрощая себе этим жизнь.
Конечно у такого подхода есть и минусы, куда же без них? Если беспорядочно пихать все подряд в секцию scripts
файла package.json
, очень скоро там черт ногу сломит. По этому следует использовать короткие команды четко отражая их поведение в названии.
npm-скрипты
Когда я начал активно использовать npm-скрипты, я, как и многие разработчики, осознал неудобство и многословность такого подхода, к примеру, если нужно организовать проверку линтерами, код будет выглядеть таким образом:
{
"lint:jshint": "jshint lib test",
"lint:eslint": "eslint lib test",
"lint:jscs": "jscs lib test"
}
Тогда скрипт, который запускает все проверки будет выглядит так:
{
"lint": "npm run lint:jshint && npm run lint:eslint && npm run lint:jscs"
}
Очень уж многословно, а тут всего-то 3 скрипта запускаются.
Удобные npm-скрипты
Есть много библиотек, упрощающих взаимодействие с npm-скриптами. Мы рассмотрим одну главную, которая объединяет функционал всех существующих решений, и в то же время, не отходит от основной парадигмы: запуск скриптов средствами npm
.
npm-run-all
Предыдущий пример получился очень многословным (гораздо более коротким чем в случае с грантом и галпом, но тем-не-менее) и сложночитаемым.
npm-run-all существенно все упрощает. С его помощью, код делающий тоже самое будет выглядеть так:
{
"lint": "npm-run-all lint:jshint lint:eslint lint:jscs"
}
(npm-run-all
можно смело заменять на более короткое название: run-s
, крайние версии npm-run-all
это поддерживают)
И делать примерно то же самое: поочередно запускать скрипты.
npm-run-all
очень не плохо себя показывает в плане удобства, но по принципу действия он не сильно уходить от npm run
: каждая команда вызывается отдельно, что значительно замедляет весь процесс.
Быстрые npm-скрипты
Я долгое время использовал npm-run-all
, но в какой-то момент осознал, что запуск скриптов может быть более оптимальным, быстрым и при этом обладать простой реализацией. Первые результаты показали себя очень хорошо в плане скорости и эффективности, при этом не теряя в удобстве.
Redrun
Принцип действия redrun значительно отличается от аналогов: вместо того, что бы запускать каждую команду отдельно, он объединяет все вложенные команды в одну большую, и уже ее передает на выполнение системному шеллу. Благодаря такой оптимизации, скорость выполнения npm-скриптов
существенно возрастает, при этом остается возможность запуска скриптов параллельно и последовательно. Пример со скриптом lint
, после парсинга будет выглядеть таким образом:
jshint lib test && jscs lib test && eslint lib test
Что может redrun?
Кроме возможности запуска скриптов последовательно, параллельно, а так же в смешанном режиме, redrun парсит все вхождения скриптов, начинающиеся с npm run
, а так же npm test
(включая префиксные и постфиксные скрипты), оставляет возможность использования секции конфигов в package.json, а еще парсит все вхождения redrun
, которыми могут запускаться скрипты, что делает его уникальным инструментом для написания лаконичных и понятных скриптов, ибо в конечном счете все превратится в одну большую команду. Стоит еще отметить совместимость большей части функционала с рассмотренным выше npm-run-all
, а так же npm
(той его части, которая касается непосредственно запуска скриптов).
Взаимодействие
Рассмотрим пример использования redrun
. Для начала установка (ничего особенного):
$ npm i redrun -g
Дальше создаем package.json
с помощью npm init -y
:
$ mkdir example
$ cd example
$ npm init -y
Wrote to /home/coderaiser/example/package.json:
{
"name": "example",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "coderaiser"
"license": "MIT",
"description": ""
}
Установим tape
для тестов, и запишем простейший тест, который проходит.
$ npm i tape -D
$ cat > test.js
'use strict';
const test = require('tape');
test('some test', (t) => {
t.pass();
t.end();
});
^C
Теперь в секцию scripts
файла package.json
добавим несколько разделов:
{
"test": "tape test.js",
"watch:test": "npm run watcher -- npm test",
"watcher": "nodemon -w test -w lib --exec"
}
После чего запустим тест с помощью npm
:
$ time npm test
reel 0m6.617s
user 0m1.262s
sys 0m1.778s
А теперь то же самое, только с помощью redrun
:
$ time redrun test
real 0m2.389s
user 0m0.495s
sys 0m0.544s
Даже на таком простом примере видно, что скорость выполнения почти в 3 раза выше.
Теперь попробуем тоже самое со скриптом npm watch:test
:
$ time redrun watch:test
> nodemon -w test -w lib --exec tape test.js
/bin/sh: 1: nodemon: not found
Command failed: nodemon -w test -w lib --exec tape test.js
real 0m1.211s
user 0m0.208s
sys 0m0.332s
Ага, nodemon
мы не установили, и нам потребовалась всего 1 секунда, что бы это узнать. Хочу обратить внимания читателя, на то, что команда полностью развернулась, и nodemon
(после установки) будет перезапускать непосредственно tape
, а не npm test
.
Попробуем тоже-самое выполнить с помощью npm
:
> article@1.0.0 watch:test /home/coderaiser/article
> npm run watcher -- npm test
> article@1.0.0 watcher /home/coderaiser/article
> nodemon -w test -w lib --exec "npm" "test"
sh: 1: nodemon: not found
npm ERR! syscall spawn
npm ERR! spawn ENOENT
npm ERR! article@1.0.0 watch:test: `npm run watcher -- npm test`
npm ERR! Exit status 1
real 0m11.594s
user 0m2.181s
sys 0m2.849s
С помощью npm
нам потребовалось 11 секунд, для того, что бы узнать, что nodemon
не установлен.
Установим nodemon
:
$ npm i nodemon -D
Попробуем снова:
$ redrun watch:test
> nodemon -w test -w lib --exec tape test.js
[nodemon] 1.10.2
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: test lib
[nodemon] starting `tape test.js`
TAP version 13
# some test
ok 1 (unnamed assert)
1..1
# tests 1
# pass 1
# ok
[nodemon] clean exit - waiting for changes before restart
Теперь все работает: запускается наблюдатель, который смотрит за изменениями в папках test
и lib
и перезапускает tape test.js
— именно то, что нам нужно. При этом остается возможность использовать компактный синтаксис скриптов в package.json.
Процесс разработки
Redrun
пишется по TDD, по этому вносить изменения: добавлять фичи, или фиксить баги — очень просто. Если читатель заметит не очевидное и не документированное расхождение в работе redrun
по сравнению с аналогичными инструментами: создавайте ишью, присылайте пул реквесты — будем исправлять :).
Вывод
npm
хороший инструмент, который исправно выполняет то, для чего предназначен. Экосистема node.js
очень стремительно эволюционирует и не последнее место в этом процессе отыгрывает npm
(а одно из первых). Я думаю, придет момент, когда redrun
станет не нужен в силу того, что npm
и так все быстро будет делать. Но пока этот момент не настал, и для того, что бы его приблизить, создан помощник npm
в таком непростом, но важном деле как запуск скриптов.