Elm для интерфейсных разработчиков - как мы погрузились в Elm, чтобы перестроить часть нашего сайта.

Почему вяз?

Преимущества Вяза (отсутствие ошибок времени выполнения, набора текста) - это то, что делает его доступным для наших back-end разработчиков. Они привыкли к функциональному стилю Scala и в целом к ​​рабочему процессу с компилятором.

С другой стороны, этот рабочий процесс также делает кривую обучения чрезвычайно крутой для интерфейсных разработчиков; Если вы привыкли к интерфейсному рабочему процессу, работа с компилятором будет совсем другой. Back-end разработчики (например, Scala) выполняют итерацию, написав код и проверяя, компилируется ли он. Из-за (непредсказуемых) ошибок времени выполнения и непрозрачного состояния в обычных интерфейсных проектах у интерфейсных разработчиков есть другой вид итеративного рабочего процесса: напишите небольшой фрагмент кода и посмотрите, какой результат будет в браузере. А это означает выход из среды IDE, навигацию по приложению и открытие инструментов разработчика в браузере.

Популяризация live reloaders и упор на быструю цепочку сборки (линтеры, прекомпиляторы) проистекают из этого рабочего процесса. Хотя в конечном итоге разработка на строго типизированном и скомпилированном языке будет иметь меньше ошибок, для этого потребуется другой стиль программирования.

Помимо адаптации к этому рабочему процессу, функционально чистый стиль кодирования печально известен непосвященным (см. Функциональное программирование - это сложно, вот почему оно хорошее, а также просто Google Функциональное программирование - сложно). Вы думаете, что вам удобны функциональные концепции (например, неизменяемое состояние, чистые функции, функции высшего порядка, каррирование) и операторы (например, карта, фильтр, сокращение) в JavaScript? В Elm это будет очень мало помощи, потому что невозможно объединить функциональный стиль с OO-стилем (или вернуться к OO-стилю, когда вы полностью потерялись).

Объекты не имеют значения в Elm, Elm - настоящий функциональный язык, как Haskell и Scheme. С другой стороны, JavaScript является гибким, мультипарадигмальным (императивным, функциональным, основанным на прототипах) и масштабируется от разработки абсолютными новичками до уровня предприятия.

Неизменность

Возьмем, к примеру, неизменность. В JavaScript (без библиотек) значение неизменяемости ограничено ключевым словом const, которое не позволяет вам переназначить объявленные переменные.

const x = {val: 'foo'};
x = {}; // prevented

Тем не менее, можно изменить содержимое константы с помощью переназначения свойств.

x.val = 'bar' // value of x is now {val: 'bar'}

Неизменяемость в Elm гораздо агрессивнее: здесь нет переменных, только константы. Назначение может быть написано встроенным или в цепочечном стиле для ясности:

-- inline
myVar : String
myVar = "Hello, " ++ toString(add 5 ((\x -> x + 1) 4))
-- chained
myVar : String
myVar = 4
    |> (\x -> x + 1)
    |> add 5
    |> toString
    |> String.append "Hello, "

Обратите внимание, что порядок обратный к тому, который вы можете ожидать в этой цепной нотации, он читается снизу вверх.

Это похоже на объединение (функциональных) операторов без сохранения промежуточного состояния в JavaScript, например:

const retweetAuthors = listOfTweets
    .filter(tweet => tweet.isRetweet)
    .map(tweet => tweet.author)

Хотя порядок здесь сверху вниз.

Если значение может быть изменено (например, путем ввода данных пользователем или обновлений сервера), вам необходимо использовать функции «обновления» в Elm. Они обновят полное или частичное состояние приложения, создав новое состояние и обновив ту часть модели, которая имеет отношение к этому событию. Пример:

updateModel : Msg -> Model -> Model
updateModel msg model =
   case msg of
       Name name ->
           { model | name = name }

Это означает, что в случае сообщения «Имя» создайте новую «модель», в которой для параметра model.name установлено указанное имя.

Преимущество состоит в том, что в процессе использования приложения записывается список состояний, что позволяет выполнять отладку в путешествиях во времени (как в this GIF).

Это может показаться знакомым, если вы использовали Redux. Redux частично вдохновлен Elm, и вы можете узнать цикл [отправка, действие] в том, как Elm обновляет модель. В этом отношении он также похож на государственные магазины в RxJS:

const increase = Rx.Observable.fromEvent(increaseButton, 'click')
    // We map to a function that will increase the count
    .map(() => state => Object.assign({}, state, {count: state.count + 1}));

Здесь вы можете видеть, что создается новый объект состояния, и обновляется только свойство count.

Еще один знакомый аспект Elm - это использование Virtual DOM. Вместо прямого управления DOM изменяется представление пользовательского интерфейса в памяти (виртуальный DOM), которое синхронизируется с реальной DOM. Это значительно улучшает производительность и является одним из аргументов в пользу React.

С учетом всего этого мы задумали переписать сайт нашей компании на Elm. Текущая реализация сайта устарела, и весь контент хранится локально. Мы хотим перейти на подход JAMstack с контентом, хранящимся в облачных сервисах (Youtube, Twitter, а фотографии в Cloudinary), и получать данные через соответствующие API с помощью Elm. Идея состоит в том, что Elm должен быть более доступным для наших разработчиков Scala, при этом обеспечивая зрелую среду для наших интерфейсных разработчиков, так что обслуживание может выполняться любым из них.

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

Цепочка инструментов / рабочий процесс

Независимо от того, является ли это Elm, Polymer или любое другое новое решение, которое не использует модули ES6, это означает, что вы не можете использовать инструменты, к которым вы, вероятно, привыкли как фронтенд-разработчик, такие как ESLint. Теперь вы во власти зрелости экосистемы Нового решения. К счастью, похоже, что у Elm очень активное сообщество, и он, например, освещает интеграцию Webpack прямо в документации.

Вяз-реактор и внешний УСБ

Начиная с Elm, вы обычно используете elm -actor для компиляции и обслуживания файлов Elm. Это предлагает веб-интерфейс, который позволяет вам перемещаться по файлам Elm и при их открытии использовать их как HTML. Довольно скоро вы обнаружите, что это создает проблему, когда вы хотите добавить внешние глобальные ресурсы, такие как CSS. Приложение Elm, обслуживаемое elm -actor, скомпилировано в JavaScript и обернуто в HTML. Но вы не можете указать, как должен выглядеть этот HTML-код, поэтому не можете добавить, например,

<link rel="stylesheet" href="style.css" />

Вы можете (и в конечном итоге будете) создать свой собственный HTML и построить, например, webpack, но есть промежуточное решение, если вы хотите сначала лучше познакомиться с Elm, сохраняя при этом преимущества elm -actor, такие как перезагрузка в реальном времени и отладчик путешествия во времени. Это не очень хорошо документировано, но вы можете создать файл HTML, который напрямую загружает файл Elm. Элм-реактор автоматически скомпилирует и обновит его в реальном времени. HTML будет содержать:

<script type="text/javascript" 
    src="/_compile/app/src/Main.elm"></script>
<script type="text/javascript">
    runElmProgram();
</script>

Мы заметили, что локальные пользовательские шрифты, которые были добавлены во время обслуживания таким образом, не загружались. Пользовательские шрифты из CDN, вероятно, подойдут.

Webpack

Сначала я настроил конфигурацию Webpack в соответствии с этим руководством. Раньше я загружал изображение в свой код Elm, но теперь это изображение дает 404, когда я использую webpack-dev-server. Он также не копируется в / dist при сборке. Webpack, похоже, не рассматривает изображение как актив, что является известной проблемой. Есть обходные пути, такие как установка изображения в качестве фона CSS и разрешение зависимости изображения с помощью css-loader, но это не соответствует строгим рекомендациям для информативных изображений.

Предлагаемое решение - использовать elm-assets-loader. Дополнение к инструкции:

  • Мне пришлось добавить ../ в качестве префикса к пути к изображению
  • Было полезно запустить webpack вместо webpack-dev-server, это давало более информативные ошибки
  • Мне пришлось удалить эту строку из конфигурации webpack: noParse: /\.elm$/,, потому что она не удалась при импорте SVG без ошибки компиляции, но с ошибкой времени выполнения: 'Uncaught ReferenceError: require is not defined'

Эта последняя проблема возникла из-за того, что в elm-webpack-loader предлагается установить noParse: /\.elm$/, конфликт с elm-css-webpack-loader, который я добавил позже.

Причина ошибки и обходной путь для установки вместо noParse: /^((?!Stylesheet).)*\.elm.*$/, объясняется в elm-css-webpack-loader.

Возвращение отладчика

При использовании Webpack вы не используете вяза-реактор, поэтому у вас нет отладчика путешествия во времени. Это кажется очевидным: кода Elm больше нет, он скомпилирован на JS. Очень сложно найти документацию, в которой это упоминается. Однако при использовании elm-webpack-starter он фактически работает с отладчиком!

Кажется, это действительно вариант для elm-webpack-loader в webpack.config.js. Сначала мы использовали это:

'elm-webpack-loader?verbose=true&warn=true'

Что эквивалентно:

{
   loader: 'elm-webpack-loader',
   options: {
       verbose: true,
       warn: true
   }
}

И тут можно добавить опцию «отладка»:

{
   loader: 'elm-webpack-loader',
   options: {
       verbose: true,
       warn: true,
       debug: true
   }
}

У меня возникли проблемы с поиском этой опции, потому что я просмотрел readme-файл elm-webpack-loader, но я искал отладка в readme, и на самом деле там есть только ссылка на все параметры в источнике.

Модульные тесты

Мы настроили модульные тесты (см. Test / Assets.elm) и изменили задачи npm таким образом, чтобы:

  • Postinstall запускает установку пакета elm для /, а также для / tests /
  • Новая задача npm format-validate проверяет / app / src и /tests/*.elm (как быстрый способ исключения / tests / elm-stuff)
  • npm test работает format-validate и elm-test
  • npm run build сначала запускает npm test, а затем запускает webpack-dev-server

Ускорение сборки Трэвиса

После добавления модульного теста (например, elm-test) скорость сборки на Travis CI превышает 45 минут. Одно из предложений - кэшировать артефакты вяза.

Добавьте это в travis.yml:

cache:
  directories:
    - elm-stuff/build-artifacts
    - tests/elm-stuff/build-artifacts

Первая сборка после добавления этого должна быть все еще медленной, потому что артефакты сборки нужно будет кэшировать, но вторая сборка должна быть быстрее:

  • сборка № 23 (первая сборка с включенным кешем): 46 минут 48 секунд
  • сборка # 24: 5 минут 53 секунды

Так что это того стоит!

Можно было бы кэшировать elm-stuff (где установлены зависимости Elm) вместо elm-stuff/build-artifacts, но для этого потребуется очистить кеш вручную при добавлении новых зависимостей Elm.

Объединение нескольких компонентов в одном приложении Elm

Мы начали с нескольких проверок концепции, поэтому у нас есть отдельное приложение TwitterFeed и приложение DiceRoller, которые необходимо интегрировать в приложение сайта. Я следил за своей попыткой рефакторинга (для разделения одного огромного файла Elm), которую я сделал в моем первом эксперименте с Elm, который помог интегрировать модели и функции обновления.

TwitterFeed имеет начальный Cmd. Это должно каким-то образом вызываться начальным Cmd приложения на главной странице.

Это инициализация модели и Cmd приложения на главной странице. Модель TwitterFeed была добавлена, но исходный Cmd по-прежнему «none»:

init : ( Model, Cmd Msg )
init =
   ( Model "Elm" Material.model initialDiceRoller TwitterFeed.State.initialModel, Cmd.none )

Я хочу, чтобы вызывался пользовательский init cmd:

init : ( Model, Cmd Msg )
init =
   ( Model "Elm" Material.model initialDiceRoller TwitterFeed.State.initialModel, initCmd )

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

initTwitterFeedCmd : Cmd TwitterFeedMsg
initTwitterFeedCmd =
   Task.attempt NewTweets fetchTweets

Невозможно создать initCmd в приложении главной страницы, которое напрямую вызывает initTwitterFeedCmd:

initCmd : Cmd Msg
initCmd =
   initTwitterFeedCmd

Поскольку initTwitterFeedCmd имеет тип Cmd TwitterFeedMsg. Однако после добавления TwitterFeedMsg к типу объединения для Msg, то есть:

type Msg
   = Name String
   | Mdl (Material.Msg Msg)
   | MsgForDiceRoller DiceRollerMsg
   | MsgForTwitterFeed TwitterFeedMsg

Затем можно отобразить из Cmd TwitterFeedMsg в Cmd Msg:

initCmd : Cmd Msg
initCmd =
   initTwitterFeedCmd
       |> Cmd.map MsgForTwitterFeed

Вывод

В конце концов, у нас просто нет достаточно веского варианта использования Elm. Слишком мало состояний для управления и взаимодействия между компонентами. Компонент TwitterFeed, например, имеет состояние, но он настолько прост, что Elm не предлагает никаких преимуществ перед альтернативами JavaScript, такими как хранилища состояний RxJS или Redux. В этом смысле Elm хорошо подходит для расширенного веб-приложения или компонента с элементами управления пользовательским интерфейсом или сложной логикой.

Также остаётся крутая кривая обучения для большинства разработчиков, и хотя это было ожидаемо для фронтенд-разработчиков, для разработчиков Scala это оказалось более сложной задачей, чем мы думали вначале.