Pull to refresh

Генератор гамм на Reactjs

Reading time 6 min
Views 12K
Здравствуйте, хабражители!

Не так давно на просторах интернета появилась javascript библиотека для построения пользовательских интерфейсов от facebook — Reactjs. Данная библиотека идеально подходит для создания простых и сложных javascript приложений. Позволяет организовывать ваш клиент-сайд в виде независимых компонентов. Берет на себя всю заботу по модификации DOM структуры дерева. И делает это весьма эффективно и разумно.

В общем, в результате небольшого знакомства с reactjs появилось такое приложение — demo. Цель данного поста поделиться впечатлениями от работы с reactjs + gruntjs + browserify.

Ниже будет изложено:

  • Основные моменты создания приложения и личные впечатления (симбиоз reactjs + browserify + gruntjs + coffeescript).
  • Серверный пререндериг reactjs компонентов для статических страниц.
  • Подход к сборке reactjs приложения с помощью gruntjs и деплой на gh-pages одной командой.


Тех кого заинтересовал прошу под кат…


Идея приложения

Не буду кривить душой, идея online генератора гамм не нова, и свое вдохновение я почерпнул из этого ресурсика, добавив несколько плюшек от себя. Гаммы, в первую очередь, полезны для людей, которые пытаются освоить импровизацию. И так же полезны как упражнение для усовершенствования техники игры. Каждая гамма, по сути, состоит из определенных нот, которые вычисляются по определенной формуле. Мне, как любителю гитары, доставляет удовольствие угадывать тональность и пытаться подбирать соло под любимую музыку. И данное приложение очень хорошо в этом помогает.

Что было реализовано


  1. Выбор тональности.
  2. Выбор гаммы (минор, мажор, блюз, пентатоника).
  3. Выбор гитарного строя.
  4. Поочередное воспроизведение нот для выбранного участка грифа гитары.
  5. Смена направления воспроизведения нот (сверху вниз/снизу вверх).
  6. Возоможность циклического воспроизведения (для заучивания).
  7. Автоматическая смена направления воспроизведения нот.


Организация приложения

Для хорошей кармы, да и просто люблю с этим повозиться — весь javascript был оформлен в виде commonjs модулей, склеивается в 1 файл и минифицируется а reactjs-компоненты пререндерятся во время сборки и вставляются в html странички.
Данный эффект достигался при помощи таких инструментов:
  • gruntjs — сборка и деплой проэкта
  • browserify — для работы commonjs модулей в браузере
  • grunt-react-prerender — пререндеринг компонентов на сервере

Формат commonjs очень удобен и позволяет использовать один и тот же javascript код как на сервере так и на клиенте.

Серверный пререндеринг

Одной из наиболее привлекательных возможностей в reactjs для меня является возможность серверного пререндеринга компонентов. Ведь прекрасно же иметь возможность инициализировать начальное состояние вашего приложения и отрисовывать копоненты до того как они попадают в браузер. Прелесть reacjs в том, что он сам понимает когда компонент уже отрисован, и не пытается сделать это снова. Если просмотреть код странички из demo, то видно что главный компонент страници уже был отрисован до отдачи контента в браузер. Главным требованием для серверного рендеринга является возможность сделать require компонента на сервере. Поскольку процедура пререндеринга является необходимой только при деплое — решил сделать под нее отдельный grunt plugin, который будет обрабатывать html файлы перед загрузкой на gh-pages — grunt-react-render.
Плагин достаточно прост и действует по такому алгоритму:

  1. Читаем файл для обработки.
  2. Парсим html структуру, находим теги с атрибутом data-rcomp.
  3. Считываем относительный путь к компоненту из атрибута data-rcomp.
  4. Делаем require компонента и вызываем метод react.renderComponentToString().
  5. Вставляем внутрь тега с атрибутом data-rcomp.
  6. Сохраняем файл.


Вот участок html кода главной страници, которая обрабатывается этим плагином:
<div class="container">
            <h1>Scales generator</h1>
            <div data-rcomp="./lib-js/pages/scales_page_component" id="container">
            </div>
        </div>



Так выглядит grunt конфигурация плагина:

    react_render:
        index:
          options:
            src: '.dist/index.html'


Впечатления о Reactjs

Reactjs подталкивает к тому, чтобы формировать логику приложения в виде отдельных конфигурируемых компонентов, которые по возможности, можно повторно использовать. Философия reactjs говорит о том, что компоненты должны хранить по минимуму состояния, так как хранимое состояние в разных местах — это источник магии и side-эффектов. Поэтому, лучше сохранять и изменять state в компонентах верхнего уровня и передавать внутренним компонентам через свойства (props).

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

И был удивлен насколько это просто с reactjs. По сути, компонент становится очень прост, когда весь рендеринг берет на себя reactjs, а тебе остается только задать то, как нужно отрисовать компонент при разном состоянии и правильно его переключать.

код выпадающего списка
React = require 'react'
{ul, li, span, div, a} = React.DOM

SimpleDropdown = React.createClass
    displayName: "SimpleDropdown"
    getDefaultProps: -> onChange: ->

    getInitialState: ->
        isOpen: false
        value: @props.value or ""

    toggle: -> @setState {isOpen: !@state.isOpen}

    itemClick: (ev) ->
        ev.preventDefault()
        value = (ev.target.getAttribute "value")
        @setState {value, isOpen: false}
        @props.onChange value

    render: ->
        self = @
        options = @props.options.map ([value, text]) ->
            (li {key: "opt_#{value}"},
                (a {value, href: "#", onClick: self.itemClick}, text))

        openCls = if @state.isOpen then "open" else ""

        currentOption = (@props.options.filter ([value, text]) =>
            value.toString() is @state.value.toString())?[0]

        (div
            className: "btn-group #{openCls}"
            (button
                className: "btn btn-default"
                onClick: @toggle
                (span {className: "glyphicon"}, "")
                currentOption?[1]
                (span {className: "caret"}, ""))
            (ul
                className: "dropdown-menu"
                ref: "select"
                options))


Coffeescript оказался очень удобен для описания html структуры компонентов. Особенно полезно его реструктурирующие присваивание.

{div, ul, span, li} = React.DOM

(div {className: "div"}
    (span {className: "span", "span Text")
    (ul
        (li "option 1")
        (li "option 2")
)


Как по мне, вполне достаточно чтобы не использовать jsx. Так же пришлось повозиться с bootstrap кнопками и draggable поведением компонентов.

Бизнес-логика

Так как приложение создавалось не только в целях освоить реакт, а и упростить себе жизнь и другим в изучении гамм на гитаре — всю логику старался делать как можно проще для того, чтобы при возможности, кто-то мог без особых усилий добавить другую гамму или строй гитары. Так вот, например, выглядит описание гамм в коде:
SCALES =
    Minor:
        desc: "Minor scale"
        size: [STEP, hSTEP, STEP, STEP, hSTEP, STEP, STEP]
        get_notes: (Tonica) -> generate_scale Tonica, SCALES.Minor

    Major:
        desc: "Major scale"
        size: [STEP, STEP, hSTEP, STEP, STEP, STEP, hSTEP]
        get_notes: (Tonica) -> generate_scale Tonica, SCALES.Major
...

Каждая гамма имеет свою формулу(size), функцию для генерации нот и описание. Показав этот проект одному другу, он имея более глубокие познания в музыке, без особого труда добавил несколько других гамм.

Так же просто выглядят данные для гитарных строев:
TUNINGS =
    "Standart":
        name: "Standart E"
        notes:  [E, B, G, D, A, E]
        offset: [0, 0, 0, 0, 0, 0]

    "DropD":
        name: "Dropped D"
        notes:  [E, B, G, D, A,  D]
        offset: [0, 0, 0, 0, 0, -2]
...


Поочередное воспроизведение звуков реализовано с помощью библиотеки howler.js.
Если у кого-то из тех кто дочитал до этого места появится желание добавить и усовершенствовать что-то в проекте — буду этому безумно рад.

Сборка и деплой 1й командой

Многие совсем не заморачиваются над сборкой проекта и организацией кода, может это и правильно. Лично я получаю от процесса автоматизации удовольствие, поэтому немного заморочился над этим и организовал сборку и деплой проекта на gruntjs. Есть 3 основных команды:
  • grunt build (компиляция coffeescript, обертка в commonjs модули для браузера, склейка js и css в 1 файл)
  • grunt deploy (build + минификация js и css, копирование в дирректорию для деплоя, пререндеринг реакт компонентов)
  • grunt deploy-gh (deploy + деплой на github pages)

Подробнее о сборке проекта может рассказать Gruntfile в репозитории.

Выводы

В целом, работа с reactjs оставила позитивное впечатление, в будущем хочу попробовать его вместе с какой-то FRP библиотекой(RxJs или bacon.js). Так же, планирую добавлять другой полезный функционал в этот проект.
Благодарю за внимание! Любая критика, пожелания и отзывы — приветствуются.

Исходный код — github
Tags:
Hubs:
+27
Comments 9
Comments Comments 9

Articles