Статьи

Дайте Grunt the Boot! Руководство по использованию npm в качестве инструмента сборки

Фронтальные инструменты для сборки и рабочих процессов доступны в изобилии: Grunt , Gulp , Broccoli и Jake и многие другие. Эти инструменты могут автоматизировать практически все, что вы неоднократно делаете в проекте, от минимизации и объединения исходных файлов до запуска тестов или компиляции кода. Но вопрос в том, нужны ли они вам? Вы действительно хотите ввести дополнительную зависимость в ваш проект? Ответ — нет!». Существует бесплатная альтернатива, которая может выполнить большинство этих задач для вас, и она поставляется в комплекте с Node.js. Конечно я говорю о нпм .

В этой статье мы обсудим, на что способен npm как инструмент сборки. Если вы хотите быстро ознакомиться с npm перед началом работы, обратитесь к руководству для начинающих по npm . Если вы хотите следовать, вы можете найти код, используемый в этой статье на GitHub .

Сценарии npm

Чтобы начать наше обсуждение, мы собираемся создать каталог для нашего нового демонстрационного проекта, который мы назовем «buildtool». После этого мы переместимся в эту папку и затем запустим команду npm init для создания файла package.json :

 $ mkdir ~/buildtool && cd ~/buildtool $ npm init 

Вам будет задано несколько вопросов. Не стесняйтесь пропустить все или часть из них, поскольку вы замените окончательное содержимое файла package.json следующим содержимым:

 { " name ": "buildtool" , " version ": "1.0.0" , " description ": "npm as a build tool" , " dependencies ": {} , " devDependencies ": {} , " scripts ": { " info ": "echo 'npm as a build tool'" } , " author ": "SitePoint" , " license ": "ISC" } 

Как видите, у нас есть объект scripts со property info . Значение info будет выполнено в оболочке как команда. Мы можем увидеть список свойств scripts (также называемых командами ) и значений, определенных в проекте, выполнив команду:

 $ npm run 

Если вы выполните предыдущую команду в нашей папке проекта, вы должны увидеть следующий результат:

 Scripts available in buildtool via `npm run-script`: info echo 'npm as a build tool' 

Если вы хотите запустить определенное свойство, вы можете запустить команду:

 $ npm run <property> 

Итак, чтобы запустить команду info мы определили в файле package.json , нам нужно написать:

 $ npm run info 

Он выдаст следующий вывод:

 $ npm run info > buildtool@ 1.0 . 0 info /home/sitepoint/buildtool > echo 'npm as a build tool' npm as a build tool 

Если вам нужен только вывод info , вы можете использовать флаг -s который отключает вывод из npm :

 $ npm run info -s npm as a build tool 

Пока мы использовали только простое echo , но это очень мощная функция. Все в командной строке доступно нам, и мы можем быть очень креативными здесь. Итак, давайте основываемся на том, что мы уже рассмотрели, и установим несколько packages чтобы создать некоторые общие рабочие процессы.

Общие рабочие процессы

Первое, что мы хотели бы реализовать, — это возможность линтинга для наших файлов JavaScript. Это включает в себя запуск программы, которая проанализирует наш код на предмет возможных ошибок. Для этого мы будем использовать JSHint , поэтому первым шагом является установка пакета через npm:

 $ npm install jshint --save-dev 

После выполнения этой команды вы увидите новую подпапку с именем node_modules . Это где JSHint был загружен. Кроме того, нам также необходимо создать следующую структуру папок для нашего проекта:

 ├── assets │ ├── css │ │ └── main .css │ └── scripts │ └── main .js ├── dist ├── package .json ├── node_modules └── test └── test .js 

В системе Unix это можно сделать с помощью следующей команды:

 $ mkdir -p assets/css assets/scripts test && touch assets/css/main.css assets/scripts/main.js test/test.js 

пыление

Теперь мы main.js некоторые синтаксические ошибки в файле main.js На данный момент файл пуст, поэтому откройте его и вставьте следующее содержимое:

 "use strict" ; var Author = new function (name) { this .name = name || "Anonymous" ; this .articles = new Array (); } Author.prototype.writeArticle = function (title) { this .articles.push(title); }; Author.prototype.listArticles = function () { return this .name + " has written: " + this .articles.join( ", " ); }; exports.Author = Author; var peter = new Author( "Peter" ); peter.writeArticle( "A Beginners Guide to npm" ); peter.writeArticle( "Using npm as a build tool" ); peter.listArticles(); 

Надеюсь, цель этого кода ясна — мы объявляем функцию конструктора, целью которой является создание новых объектов Author . Мы также прикрепляем несколько методов к свойству prototype Author что позволит нам хранить и перечислять статьи, которые автор написал. Обратите внимание на оператор exports который сделает наш код доступным вне модуля, в котором он определен. Если вы хотите узнать больше об этом, обязательно прочитайте: Понимание module.exports и export в Node.js.

Затем мы должны добавить property к нашему объекту scripts в package.json которое будет запускать jshint . Для этого мы создадим свойство lint следующим образом:

 "scripts" : { " info ": "echo 'npm as a build tool'" , " lint ": "echo '=> linting' && jshint assets/scripts/*.js" } 

Здесь мы используем оператор && для объединения команд и файловых глобусов (звездочка), которые обрабатываются как подстановочный знак, в этом случае сопоставляя любой файл с .js в каталоге script .

Примечание : командная строка Windows не поддерживает globs, но когда ей передается аргумент командной строки, такой как *.js , Windows передает его дословно вызывающему приложению. Это означает, что поставщики могут устанавливать библиотеки совместимости для обеспечения функциональности, подобной глобальному Windows. Для этого JSHint использует библиотеку minimatch .

Теперь давайте скопируем код:

 npm run lint -s 

Это дает следующий вывод:

 => linting assets/scripts/main.js: line 1 , col 1 , Use the function form of "use strict" . assets/scripts/main.js: line 5 , col 28 , The array literal notation [] is preferable. assets/scripts/main.js: line 3 , col 14 , Weird construction. Is 'new' necessary? assets/scripts/main.js: line 6 , col 1 , Missing '()' invoking a constructor. assets/scripts/main.js: line 6 , col 2 , Missing semicolon. assets/scripts/main.js: line 16 , col 1 , 'exports' is not defined. 6 errors 

Оно работает. Давайте исправим эти ошибки, перезапустим линтер, чтобы убедиться, а затем перейдем к тестированию:

 ( function () {  "use strict" ; var Author = function (name) { this .name = name || "Anonymous" ; this .articles = []; }; Author.prototype.writeArticle = function (title) { this .articles.push(title); }; Author.prototype.listArticles = function () { return this .name + " has written: " + this .articles.join( ", " ); }; exports.Author = Author; var peter = new Author( "Peter" ); peter.writeArticle( "A Beginners Guide to npm" ); peter.writeArticle( "Using npm as a build tool" ); peter.listArticles(); })(); 

Обратите внимание, как мы завернули все в немедленно вызванное выражение функции .

 npm run lint -s => linting 

Нет ошибок Были хороши!

тестирование

Для начала нам нужно установить пакет mocha . Mocha — это простая, но гибкая среда тестирования JavaScript для Node.js и браузера. Если вы хотите больше узнать об этом, эта статья — отличное место для начала: базовое фронтальное тестирование с Mocha & Chai

 npm install mocha --save-dev 

Далее мы собираемся создать несколько простых тестов для проверки методов, которые мы написали ранее. Откройте test.js и добавьте следующий контент (обратите внимание на оператор require который делает наш код доступным для mocha):

 var assert = require ( "assert" ); var Author = require ( "../assets/scripts/main.js" ).Author; describe( "Author" , function () { describe( "constructor" , function () { it( "should have a default name" , function () { var author = new Author(); assert.equal( "Anonymous" , author.name); }); }); describe( "#writeArticle" , function () { it( "should store articles" , function () { var author = new Author(); assert.equal( 0 , author.articles.length); author.writeArticle( "test article" ); assert.equal( 1 , author.articles.length); }); }); describe( "#listArticles" , function () { it( "should list articles" , function () { var author = new Author( "Jim" ); author.writeArticle( "a great article" ); assert.equal( "Jim has written: a great article" , author.listArticles()); }); }); }); 

Теперь давайте добавим test задание в package.json :

 "scripts": { "info": "echo 'npm as a build tool'", "lint": "echo '=> linting' && jshint assets/scripts/*.js", "test": "echo '=> testing' && mocha test/" } 

У npm есть несколько удобных ярлыков, а именно: npm test npm start npm stop . Это псевдонимы для их эквивалентов выполнения, а это значит, что нам просто нужно запустить npm test чтобы запустить npm test в действие:

 $ npm test -s => testing Author constructor ✓ should have a default name #writeArticle ✓ should store articles #listArticles ✓ should list articles 3 passing ( 5 ms) 

До и после крючки

Это не было бы очень эффективно, если бы мы запустили наш набор тестов, и он сразу же вышел из-за синтаксической ошибки. К счастью, npm предоставляет нам pre и post хуки, поэтому, если вы запустите npm run test он сначала выполнит npm run pretest и npm run posttest после его завершения. В этом случае мы хотим запустить скрипт lint перед test скриптом. Следующий сценарий pretest делает это возможным.

 "scripts" : { " info ": "echo 'npm as a build tool'" , " lint ": "echo '=> linting' && jshint assets/scripts/*.js" , " test ": "echo '=> testing' && mocha test/" , " pretest ": "npm run lint -s" } 

Представьте, что мы ранее не исправляли синтаксические ошибки в нашем скрипте. В этом случае приведенный выше сценарий pretest test завершится неудачно с ненулевым кодом завершения, и test сценарий не запустится. Это именно то поведение, которое мы хотим.

 $ npm test -s => linting assets/scripts/main.js: line 1 , col 1 , Use the function form of "use strict" . ... 6 errors 

С исправленным кодом в main.js :

 => linting => testing Author constructor ✓ should have a default name #writeArticle ✓ should store articles #listArticles ✓ should list articles 3 passing ( 6 ms) 

Мы в зеленом!

Минификация кода

Для этого раздела нам нужно добавить каталог dist в наш проект, а также несколько подкаталогов и файлов. Вот как выглядит структура папок:

  ├── dist │ └── public │ ├── css │ ├── index .html │ └── js 

Команда для воссоздания этого на Unix-машине:

 mkdir -p dist/public/css dist/public/js && touch dist/public/index.html 

Содержимое index.html просто.

 <!DOCTYPE html> < html > < head > < meta charset = "utf-8" /> < title > npm as a build tool </ title > < link href = 'css/main.min.css' rel = 'stylesheet' > </ head > < body > < h2 > npm as a build tool </ h2 > < script src = 'js/main.min.js' >  </ script > </ body > </ html > 

В настоящее время main.js не минимизирован. Так и должно быть, потому что это файл, с которым мы работаем, и мы должны иметь возможность прочитать его. Однако, прежде чем загрузить его на работающий сервер, нам нужно уменьшить его размер и поместить его в каталог dist/public/js . Для этого мы можем установить пакет uglify-js и создать новый скрипт.

 $ npm install uglify-js --save-dev 

Теперь мы можем сделать новый скрипт minify:js в package.json :

 "scripts" : { " info ": "echo 'npm as a build tool'" , " lint ": "echo '=> linting' && jshint assets/scripts/*.js" , " test ": "echo '=> testing' && mocha test/" , " minify:js ": "echo '=> minify:js' && uglifyjs assets/scripts/main.js -o dist/public/js/main.min.js" , " pretest ": "npm run lint -s" } 

Запустить его:

 $ npm run minify:js -s => minify:js 

И скрипт создает минимизированную версию нашего файла в нужном месте назначения. Мы сделаем то же самое для нашего CSS-файла, используя пакет clean-css .

 $ npm install clean-css --save-dev 

И создайте сценарий minify:css .

 "scripts" : { " info ": "echo 'npm as a build tool'" , " lint ": "echo '=> linting' && jshint assets/scripts/*.js" , " test ": "echo '=> testing' && mocha test/" , " minify:js ": "echo '=> minify:js' && uglifyjs assets/scripts/main.js -o dist/public/js/main.min.js" , " minify:css ": "echo '=> minify:css' && cleancss assets/css/main.css -o dist/public/css/main.min.css" , " pretest ": "npm run lint -s" } 

Давайте run скрипт.

 $ npm run minify:css -s => minify:css 

В ожидании перемен

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

Хорошей новостью является то, что вы можете сделать это и в npm, используя такой пакет, как watch , который призван упростить управление просмотром файлов и деревьев каталогов.

 $ npm install watch --save-dev 

Затем в файле package.json необходимо указать задачи, которые будут выполняться при обнаружении изменения. В этом случае минимизация JavaScript и CSS:

 "scripts" : { ... " watch ": "watch 'npm run minify:js && npm run minify:css' assets/scripts/ assets/css/" } 

Запустите скрипт, используя:

 $ npm run watch 

Теперь, когда любой файл в assets/scripts/ или assets/css/ изменяется, скрипты минификации будут вызываться автоматически.

Сценарий сборки

К настоящему времени у нас есть несколько сценариев, которые мы можем объединить в цепочку для build сценария build который должен выполнять следующее: linting, тестирование и минимизация В конце концов, было бы больно каждый раз запускать эти задачи по отдельности. Чтобы создать этот скрипт сборки, измените объект скрипта в package.json , таким образом:

 "scripts" : { " info ": "echo 'npm as a build tool'" , " lint ": "echo '=> linting' && jshint assets/scripts/*.js" , " test ": "echo '=> testing' && mocha test/" , " minify:js ": "echo '=> minify:js' && uglifyjs assets/scripts/main.js -o dist/public/js/jquery.min.js" , " minify:css ": "echo '=> minify:css' && cleancss assets/css/main.css -o dist/public/css/main.min.css" , " build ": "echo '=> building' && npm run test -s && npm run minify:js -s && npm run minify:css -s" , " pretest ": "npm run lint -s" } 

Запуск скрипта build дает нам следующий результат.

 $ npm run build -s => building => linting => testing Author constructor ✓ should have a default name #writeArticle ✓ should store articles #listArticles ✓ should list articles 3 passing ( 6 ms) => minify:js => minify:css 

Серверный скрипт

После того, как мы запустим наш скрипт build было бы хорошо, если бы мы могли запустить сервер для нашего контента в dist и проверить его в браузере. Мы можем сделать это с помощью пакета http-сервера .

 $ npm install http-server -save-dev 

Мы делаем server скрипт.

 "scripts" : { ... " server ": "http-server dist/public/" , } 

И теперь мы можем run наш сервер.

 $ npm run server Starting up http-server, serving dist/public/ on: http://0.0.0.0:8080 Hit CTRL-C to stop the server _ 

Конечно, server скрипт может быть добавлен в скрипт build , но я оставляю это как упражнение для читателя.

Вывод

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

Если у вас есть какие-либо вопросы или комментарии, я был бы рад услышать их в ветке ниже.