Как стать автором
Обновить

Пишем генератор для Yeoman.io

Время на прочтение 7 мин
Количество просмотров 10K
image
Доброго времени суток, хабрасообщество! В этой статье я хочу описать опыт создания генератора для scaffolding системы Yeoman. Прежде всего, я был немного удивлён тем, что данная система и работа с ней не были описаны на хабре, разве что одно маленькое упоминание из далекого 2012 года: Yeoman.io. Как я уже написал выше, в данной статье я буду рассматривать поэтапное создание yeoman-генератора для ваших проектов.

Yeoman-генератор(далее просто генератор) представляет собой npm-пакет, с помощью директив которого yeoman собирает каркас приложения. В этой статье я рассмотрю пример создания генератора для скаффолдинга архитектуры, которую я использую на своих проектах(marionette, coffee, sass+compass, require).

Начальные данные

Нам потребуется: машина с установленным nodejs, npm, yeoman и npm-пакетом generator-generator.
Далее, мы должны создать директорию, в которой будет располагаться наш генератор(я назвал свой generator-puppeteer). Очень важно, чтобы ваша папка начиналась с префикса generator-, т.к. в противном случае, при начале работы, yeoman создаст папку, софрмированную по принципу generator-<имя генератора>.

Шаг 1 — Скаффолдинг генератора



# Создадим директорию для нашего генератора
$ mkdir generator-puppeteer && cd $_

# Развернем каркас нашего генератора
$ yo generator


После двух вопросов про имя пользователя на github и названия генератора, yeoman разворачивает скелет нашего будующего генератора.
Посмотрим, что нам сгенерировал yeoman:

Директории:

app — директория, в которой будут находиться все наши файлы, связанные контентом проекта, например: bower.json, package.json, шаблоны всех наших страниц и т.п.
node_modules — директория с зависимостями генератора, продиктованными package.json, например chalk или mocha.
test — тут будут лежать все тесты для нашего генератора.


Файлы:

.editorconfig — конфиг для текстового редактора
.gitattributes — спецефические настройки директорий или файлов для git'а
.gitignore — список файлов и директорий, которые не будут проиндексированы git
.jshintrc — конфиг jshint
package.json — файл зависимостей генератора
README.md — файл описания проекта для github'а
.travis.yml — указание платформы для CI


Итак, скелет нашего генератора развёрнут.

Шаг 2 — Редактирование запускаемого файла


Лично у меня, когда я вижу незнакомую архитектуру, возникает закономерный вопрос: где находится точка входа в проект. В нашем случае — это файл index.js, находящийся в директории app. Рабоатет он следующим образом: сначало мы получаем доступ к файлу package.json и подписываемся на событие окончания инициализации. Если небыл передан флаг --skip-install, то после инифиализации будут установлены зависимости, прописанные в package.json и bower.json. Ничего сложного, верно? Теперь давайте попробуем закастомизировать стандартный UI скафолдера. Для этого нам придется изменить метод askFor — именно он вызывается первый после инициализации и отвечает за поллинг необходимой информации у пользователя(а так же рисует довольно симпатичный ASCII-арт). В данном методе используется имплементация библиотеки Inquirer, позволяющая создавать вопросы и получать информацию от пользователя. Давайте попробуем узнать у пользователя что-нибудь интересное, например название его приложения:

Исходный код:

var prompts = [{
  type: 'confirm',
  name: 'someOption',
  message: 'Would you like to enable this option?',
  default: true
}];

this.prompt(prompts, function (props) {
  this.someOption = props.someOption;

  done();
}.bind(this));

Отредактированный код:

var prompts = [{
  type: 'prompt',
  name: 'appName',
  message: 'Could you tell me the name of your new project?',
}];

this.prompt(prompts, function (answers) {
  this.appName = answers.appName;

  done();
}.bind(this));


Больше информации вы сможете найти в их репозитории, на странице примеров. Использование данной библиотеки здесь будем максимально полезно, если вы решите предоставить пользователю возможность выбора дополнительных технологий, которые он возможно захочет включить в проект, например, можно спросить у пользователя, хочет ли он, чтобы в проект была включена возможность использовать bootstrap «из коробки». Как вы заметили, все переменные записываются как свойства экземпляра генератора — позднее, мы будем использовать их внутри шаблонов.

Шаг 3 — Написание директив для скаффолдинга структуры приложения


Теперь давайте рассмотрим функцию app — сердце нашего генератора. Именно тут мы собираем каркас нашего приложения. Что же происходит в теле этой функции:

app: function () {
  this.mkdir('app');
  this.mkdir('app/templates');

  this.copy('_package.json', 'package.json');
  this.copy('_bower.json', 'bower.json');
}

Как мы видим, по умолчанию здесь всё весьма пресно: мы просто создаем 2 каталога и копируем 2 шаблона в директорию нашего проекта. Функция copy принимает всего два параметра: исходный файл из sourceRoot и имя файла, который будет создан в targetRoot. Давайте лучше напишем код, который будет создавать нам файл index.html. Но, вероятно, я хочу изменять содержимое индекса в зависимости от опций, которые я могу выбрать перед установкой. Например, я хочу устанавливать в тег название своего проекта — тут простым this.copy не обойтись, здесь нам поможет this.template. Давайте остановимся на этих функциях немного подробнее. Обе функции являются частью примеси actions/actions, и выполняются для перемещения файлов из директории шаблонов в директорию приложения, за одним исключением: функция templateумеет работать с шаблонами, т.е. с её помощью мы сможем скопировать файл из sourceRoot, вставить в него данные и отправить в targetRoot. Давайте попробуем сделать это на описанном выше примере. Создадим файл _index.html в sourceRoot директории проекта(по умолчанию app/templates). В качестве примера, можно использовать этот gist. Теперь немножко допишем функцию app, чтобы получить нечто следующее:

app: function () {
  this.mkdir('app');
  this.mkdir('app/templates');

  this.template('_index.html', 'index.html');
  this.copy('_package.json', 'package.json');
  this.copy('_bower.json', 'bower.json');
}

Итак, откуда же мы возьмём данные для нашего шаблона? По умолчанию, если данные не переданы явно третим атрибутом, то шаблонизатор использует scope генератора в качестве хэша с данными, т.е. когда мы сохранили appName, введенный через prompt в this.appName, мы автоматически сделали его доступным во всех наших шаблонах(где напрямую не указан хэш с данными). Отлично, теперь мы умеем параметризировать наши файлы. Следующий шаг — проектирование архитектуры. Так как я пишу генератор под архитектуру своего проекта, то и в этой статье я буду опираться на его архитектуру, а именно:

app — корень приложения
app/templates — шаблоны
app/core — базовые классы
app/common — разные примеси и пр.
app/static — статика(изображения, шрифты)
app/components — компоненты
app/modules — модули
app/stylesheets — стили
app/libs — сторонние библиотеки

На этом архитектурная составляющая завершена, осталось только настроить библиотеки, которые мы хотим использовать по умолчанию. Но вот вопрос: мы пишем генератор, которым будут пользоваться разные люди, разделяющие наш взгляд на архитектурные решения приложения, но будут ли они все использовать одинаковый tool-chain? Вряд ли. Мы, как порядочные разработчики, разумеется, должны предусмотреть такое момент и добавить хотя бы минимальный выбор технологий, которые планируется поддерживать нашим генератором «из коробки». В моем случае это будет RequireJS, CoffeeScript и SASS+Compass, и при каждом использовании моего генератора, пользователю будет задан вопрос, какие технологии из представленных он хочет добавить в проект. И не забудем добавить Gruntfile! С учетом этих дополнений, код нашего метода app будет следующим:

app: function () {
  // Core application folder
  this.mkdir('app');
  // Templates application folder
  this.mkdir('app/templates');
  // Folder for base classes
  this.mkdir('app/core');
  // Common project files
  this.mkdir('app/common');
  // Static content, like images or fonts
  this.mkdir('app/static');
  // Logic components for the project
  this.mkdir('app/components');
  // Modules of the project
  this.mkdir('app/modules');
  // Stylesheets directory
  this.mkdir('app/stylesheets');
  // 3-rd party members libs
  this.mkdir('app/libs'); 

  this.template('Gruntfile.js', 'Gruntfile.js');
  this.template('_index.html', 'index.html');

  // RequireJS config & App
  this.copy('js/app.js', 'app/app.js');
  this.copy('js/main.js', 'app/main.js');

  this.copy('_package.json', 'package.json');
  this.copy('_bower.json', 'bower.json');
  this.copy('_.bowerrc', '.bowerrc');
}


Обратите внимание, я добавляю в конце файл .bowerrc, в нём я указываю, что зависимости должны складироваться в директорию app/libs.

Шаг 4 — Создание саб-генератора


Итак, пускай и не очень углубляясь, мы смогли написать простенький генератор для структуры проекта и index.html, который будет являться точкой входа в наш проект. Вроде бы неплохо, да? Но Yeoman может больше! Давайте попробуем выжать из него ещё чуть-чуть!
Исходя из того, что мы уже написали, yeoman используется у нас только для разворачивания архитектуры на начальном этапе, но теперь мы будем использовать его для создания шаблонов компонентов нашего приложения. Как я уже писал выше, в нашем проекте(по крайней мере в моей архитектуре нашего проекта), я добавил папку app/components, в которую собираюсь складывать какие-то абстрактные компоненты; теперь немного подробнее: под компонентом я понимаю некую организацию кода наподобе MVC, которая позволяет упростить работу с логическими сущностями. Так, например, на нескольких страницах нашего приложения должен находиться блок с комментариями. Дабы не копипастить код из модуля и держать его всегда в консистентном состоянии, мы создаем CommentComponent, который вызываем из разных модулей через его API, например:

var _this = this;
var commentComponent = new CommentComponent;
commentComponent.getUserComments({user_id: 1}).done(function(commentsView) {
  _this.layout.comments.show(commentsView);
});

Соответственно, мне не помешало бы, если бы я смог создавать такие компоненты максимально быстро(ведь никто не любит создавать кучу файлов и папок?). Как вам, скажем, если наш компонент будет создаваться удобной командой

# Создаем компонент CommentsComponent
$ yo puppeteer:component 'Comments'

Итак, давайте определимся, что данная команда должна уметь? Например, создавать MVC архитектуру в папке app/components/comments, а так же генерировать необходимый минимальный набор файлов:
models/comment.js
collections/comments.js
views/comments.js
views/comment.js
controller.js


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

# Создаем саб-генератор "component"
$ yo generator:subgenerator 'component'

Итак, посмотрим, что он нам сгенерировал:
app/component
app/component/index.js
app/component/templates/somefile.js

По своей сути, саб-генератор представляет из себя тот-же обычный генератор, имеет такой же API и почти такую же структуру, как и его старший брат. Итак, что же мы видим, когда открываем index.js: наш компонент наследуется от NamedBase и имеет 2 предустановленых метода: init и files. Как несложно догадаться, в init у нас просто выводится greetings-msg для вызывающего саб-генератор, а в методе files мы описываем, непосредственно, всю логику работы генератора. Я не буду заострять на этом внимание, т.к. ничего нового тут нет. Мой пример index.js вы можете посмотреть в моём gist'е.

Далее создаем шаблоны самих файлов. Тоже ничего нового, мы это уже делали выше. По обыкновению, мою версию вы сможете найти тут.

Шаг 5 — Запуск нашего генератора


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

$ npm link

Теперь, когда линк создан, мы можем создать тестовую директорию и пощупать то, что у нас получилось:

$ mkdir TestProject && cd $_ && yo puppeteer
Теги:
Хабы:
+8
Комментарии 4
Комментарии Комментарии 4

Публикации

Истории

Работа

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн