Ускоряем 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


    Принцип действия 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:


    $ time npm run watch:test
    > 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 в таком непростом, но важном деле как запуск скриптов.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 17

      0
      https://github.com/sanex3339/javascript-obfuscator/blob/master/scripts/test-full
      Вот эта вот команда с npm выполняется 17.5 сек, с redrun 16.5 сек. Т.е. в моей ситуации прирост есть, но незначительный.
        +1

        Дело, в том, что redrun разворачивает лишь те скрипты, которые расписаны в package.json.
        Для того, что бы ощутить результат в полной мере, необходимы небольшие изменения:


        {
           "test:full:speed": "redrun test:compile test:full:test test:full:cover test:removeTmpDir",
           "test:full:test": "node test-tmp/test/dev/test.js",
           "test:full:cover": "$(npm bin)/istanbul cover $(npm bin)/_mocha -- test-tmp/test/index.spec.js",
           "test:removeTmpDir": "scripts/test-remove-tmp-dir"
        }

        В моем случае результаты получились немного другие:


        coderaiser@cloudcmd:~/javascript-obfuscator$ time npm run test:full
        
        real    3m46.539s
        user    0m45.062s
        sys     0m50.060s
        
        coderaiser@cloudcmd:~/javascript-obfuscator$ time redrun test:full:speed
        > scripts/test-compile && node test-tmp/test/dev/test.js && $(npm bin)/istanbul cover $(npm bin)/_mocha -- test-tmp/test/index.spec.js && scripts/test-remove-tmp-dir
        
        real    3m8.401s
        user    0m39.272s
        sys     0m44.236s

        А с такими скриптами, будет еще быстрее.


        {
           "test:full:speed:max": "redrun test:compile test:full:test test:full:cover:speed test:removeTmpDir",
           "test:full:test": "node test-tmp/test/dev/test.js",
           "test:full:cover:speed": "istanbul cover $(npm bin)/_mocha -- test-tmp/test/index.spec.js",
           "test:removeTmpDir": "scripts/test-remove-tmp-dir"
        }

        coderaiser@cloudcmd:~/javascript-obfuscator$ time redrun test:full:speed:max
        
        real    2m58.866s
        user    0m37.000s
        sys     0m42.692s

        Чем больше npm-скриптов — тем выше скорость.

          0
          Благодарю за пояснение и пример!
          0
          Спасибо огромное за статью и redrun! Отличная вещь с одним маленьким, но досадным минусом – zsh подсказывает мне доступные npm-скрипты, чего не делает с redrun.
            +1

            Действительно, этого не хватает. Нужно будет подумать об этом.

              0
              И еще, похоже у вас не работают встроенные переменные npm'a. То есть, у меня такой скрипт:
              "docker": "docker push registry.docker.com/myapp:v$npm_package_version"
              

              выполняется, но вместо переменной пустота.
                +1

                Спасибо, поддержка $npm_package_version добавлена в v5.4.0.

                +1

                В redrun v5.5.0 добавлена поддержка автодополнения названий скриптов по табу аналогичную той, что используется в npm. Для установки достаточно выполнить:


                redrun-completion >> ~/.bashrc
                redrun-completion >> ~/.zshrc
              0
              Не понимаю, что такого крутого делает redrun, чего не делают другие сборщики. Мне даже кажется, что в данном случае make будет лучше, чем redurn, потому что самому make на линуксах ничего устанавливать не надо, он там и так стоит, а рэдран надо.
              Просто конфиги свои берёт не из специального файла, а из package.json, который по идее должен быть в node-проектах. Да, умно брать конфиги от туда, эта идея хороша, но не нова.

              Я хочу попросить сравнить скорость redrun и make, если не затруднит, сделайте пожалуйста.
                0
                Мне даже кажется, что в данном случае make будет лучше, чем redurn, потому что самому make на линуксах ничего устанавливать не надо, он там и так стоит, а рэдран надо.

                Make есть на линуксах, но не на Windows.


                Просто конфиги свои берёт не из специального файла, а из package.json, который по идее должен быть в node-проектах. Да, умно брать конфиги от туда, эта идея хороша, но не нова.

                Не свои конфиги, а конфиги npm. Redrun в этом плане совместим с npm, и ничего нового не привносит.


                Я хочу попросить сравнить скорость redrun и make, если не затруднит, сделайте пожалуйста.

                У redrun и make разные цели. Redrun разворачивает npm-скрипты, и запускает получившуюся команду. Make автоматизирует процесс преобразования файлов из одной формы в другую, у него свой формат, и возможности гораздо шире, чем у redrun, но в его штатную поставку не входит чтение и исполнение npm-скриптов, скорее он предлагает писать скрипты в Makefile. Этот подход не хуже, он может быть быстрее, но он кардинально отличается от используемого в redrun а именно: использование существующих скриптов, а не написание новых.


                Я не владею make и мне не близка его философия, поэтому я не смогу сделать такое сравнение.

                  +1
                  Вы сейчас сравниваете make и npm run. Redrun делает то же, что и npm, только значительно быстрее. Зачем нужен npm run, когда есть make – абсолютно другой вопрос, который не относится ни к рэдрану, ни к этому посту.
                    0
                    Вот этим вопросом и стоило бы задаться. Все сложные скрипты удобнее делать через Makefile, банально синтаксис лучше, чем shell команды экранированные в JSON, например какой-нибудь хитрый запуск тестов:

                    ./node_modules/.bin/istanbul cover -x tests.js --dir=./build/coverage --report=lcov --hook-run-in-context ./node_modules/mocha/bin/_mocha -- --reporter spec tests.js
                    

                    Согласитесь, длинно… В Makefile хотя бы на новые строки можно перенести.

                    Я лично использую NPM скрипты только если команды очень простые и короткие или если достоверно известно, что будут юзеры на винде.
                      0

                      Действительно очень длинно, такую команду в любом случае стоит упростить. Сделать это можно добавив пару секций в package.json:


                      {
                        "config": {
                          "mocha": "--reporter spec test",
                          "istanbul": "-x test --dir=./build/coverage --report=lcov --hook-in-context",
                        },
                        "scripts": {
                          "coverage" : "istanbul cover $npm_config_istanbul _mocha -- $npm_config_mocha"
                        }
                      }

                      После чего скрипт можно запускать с помощью:


                      redrun coverage

                      либо


                      npm run coverage
                        0
                        Но это же ужас дикий ))) и запись у вас не полная — пути до istanbul и mocha потерялись. Это затыкание дырок, а не способ, увы. А вот нормальная запись:

                        ./node_modules/.bin/istanbul \
                            cover \
                            -x tests.js \
                            --dir=./build/coverage \
                            --report=lcov \
                            --hook-run-in-context \
                            ./node_modules/mocha/bin/_mocha \
                            -- \
                            --reporter spec \
                            tests.js
                        
                –1

                Я бы сильно поспорил про порог вхождения для gulp. Проблема в том, что устаревают не только плагины для gulp, но и все остальное без исключения, поэтому хотелось бы, чтобы авторы думали об обратной совместимости почаще. Redrun решает только частный случай быстрого запуска, который не всех беспокоит.

                  0

                  Redrun разворачивает секцию скриптов, за счет чего увеличивается скорость по сравнению с npm и npm-run-all. Этот частный случай интересует пользователей npm-скриптов, для которых важны: скорость, удобство и совместимость с существующими решениями — то о чем автор думает достаточно часто.

                Only users with full accounts can post comments. Log in, please.