Elm’s ngReact
Иногда имеют значение небольшие и постепенные изменения.
Про Вяз было сказано много. Впечатляющее количество статей, выступлений и сообщений восхваляют Elm как будущее фронтенд-разработки (наряду с PureScript и ClojureScript).
Что действительно хорошо в Вязах, так это то, что вам не нужно переходить на полный Вяз, чтобы воспользоваться преимуществами. Преимущества включают отличный язык, инструменты, сообщество, а также отличные идеи и концепции. Особенно хорош аспект сообщества и взаимодействие внутри сообщества. Похоже, что все либо занимаются парным программированием, либо иным образом сотрудничают, по крайней мере, с точки зрения стороннего наблюдателя.
У Эльма также есть потрясающая стратегия адаптации. Эван Чаплицки написал об этом в статье Как использовать вяз на работе, где подробно описывает стратегии того, как использовать вяз на рабочем месте. Ранее упомянутый факт, что вам не нужно полностью использовать Elm, а начинать с малого, встраивая Elm в существующий проект, значительно снижает барьер для входа. Это означает, что внедрить Elm в ваш проект на основе React проще, чем вы думаете.
Мы рассмотрим различные стратегии миграции, которые включают интеграцию в проект JavaScript barebone, миграцию с React на Elm и, наконец, как даже постепенно перейти с Redux на Elm.
Встраивать Elm в какой-нибудь большой проект JavaScript не так уж и сложно. Это как три строчки JS, но такое ощущение, что о них никто не знает!
Хотя это звучит как сложное мероприятие, на самом деле часть взаимодействия - это всего лишь пара строк кода, как указано в приведенном выше комментарии. Добавляя параметр вывода к команде elm-make, мы получаем скомпилированный файл JavaScript без HTML-части.
elm-make Counter.elm --output=Counter.js
Следующий фрагмент взят из Руководство по языку Elm - раздел JavaScript Interop.
<div id="main"></div> <script src="main.js"></script> <script> var node = document.getElementById('main'); var app = Elm.Main.embed(node);. </script>
Взаимодействие между JavaScript и Elm может быть достигнуто двумя способами: через порты и через флаги. Чтобы заставить их общаться, требуется пара изменений как на стороне Elm, так и на стороне JavaScript. Включая необходимость определения модуля порта
port module Counter exposing (..)
и добавление портов для прослушивания и отправки из и в JS.
port check : Int -> Cmd msg port counter : (Int -> msg) -> Sub msg
Все коммуникации между Elm и JavaScript проходят через порт, через который мы можем взаимодействовать с помощью команд и подписок. В приведенном выше примере мы отправляем данные на сторону JS через порт проверки. Теперь у нас есть возможность отправить целое число, вызвав
check 1
Абонентская часть покрывается встречным портом. Мы подписываемся на любые изменения значения счетчика, поступающие со стороны JS.
Чтобы повторить наш предыдущий фрагмент HTML, теперь мы можем инициализировать счетчик, например. В этом очень конкретном случае мы определяем начальное значение счетчика 3, а затем подписываемся на любые изменения этого счетчика.
<script> var node = document.getElementById('counter'); var app = Elm.Counter.embed(node); app.ports.counter.send(3) app.ports.check.subscribe(function(count) { console.log('receiving data...', count); }) </script>
Мы также можем прослушать изменения и умножить текущий счетчик на 3 ф. Е.
<script> // ... app.ports.check.subscribe(function(count) { app.ports.counter.send((count*3)) }) </script>
Как видно из предыдущих примеров, мы можем легко взаимодействовать с JavaScript с точки зрения Elm и наоборот. Elm также обеспечивает предварительное отклонение недопустимых данных, предотвращая попадание неверных данных в приложение Elm.
Однако возможности взаимодействия на этом не заканчиваются. react-elm-components - это специальная библиотека, предназначенная для внедрения компонентов, написанных на Elm, в кодовую базу React.
На самом деле он просто оборачивает ранее замеченный код в компонент React, гарантируя, что компонент Elm связан с правильным узлом DOM и обрабатывает отправку, а также подписку на этот компонент. В общем, это всего лишь пара строк кода, которые позволяют нам плавно связать Elm с React. Тот же пример теперь можно записать так.
import React from 'react' import { render } from 'react-dom' import Elm from 'react-elm-components' import { Counter } from './Counter' const setupPorts = ports => { ports.check.subscribe(count => ports.counter.send((count*3))); } const CounterComponent = () => <Elm src={Counter} ports={setupPorts} /> render(<div> <CounterComponent /> </div>, document.getElementById('app'))
Для полной реализации проверьте пример react-elm-components.
Все эти примеры подчеркивают тот факт, что мы можем внедрить самый маленький компонент в существующий проект JavaScript, не выполняя сложную настройку. Добавьте к этому тот факт, что мы всегда можем вернуться назад, и включение Elm станет интересным делом. Эти функции являются мостом к внесению дополнительных изменений. Открывая путь для медленного переноса проекта на другую платформу или, в данном случае, на другой язык.
Но, как вы понимаете, на этом все не заканчивается. Кто-то из сообщества Elm подумал о том, как связать Redux с Elm. На самом деле это имеет смысл, учитывая, насколько широко Redux распространен в мире JavaScript и особенно React. Кристоф Германн написал модуль под названием redux-elm-middleware, который позволяет нам медленно переносить существующую кодовую базу redux на Elm.
Давайте создадим редуктор Counter, просто чтобы прочувствовать идею.
port module Reducer exposing (..) import Redux import Task exposing (..) import Process import Json.Encode as Json exposing ( object, int ) port increment : ({} -> msg) -> Sub msg port decrement : ({} -> msg) -> Sub msg subscriptions : Model -> Sub Msg subscriptions _ = Sub.batch [ decrement <| always Decrement , increment <| always Increment ]
Как видно из примера взаимодействия, мы определяем модуль порта, а также необходимые порты, которые нам нужны для получения уведомлений при увеличении или декременте Действие отправлено.
-- MODEL type alias Model = { count : Int } init : Int -> ( Model, Cmd Msg) init count = ( { count = count }, Cmd.none ) encodeModel : Model -> Json.Value encodeModel { count } = object [ ( "count", int count ) ]
Единственная действительно интересная часть в следующем разделе - это encodeModel, где мы сообщаем Элму, как должна выглядеть наша модель. Если переданные данные не соответствуют определенной модели, они будут немедленно отклонены и завершатся ошибкой на стороне JavaScript.
-- VIEW view : Model -> Html Msg view model = div [] [ button [ onClick Decrement ] [ text "-" ] , div [] [ text (toString model) ] , button [ onClick Increment ] [ text "+" ] ] -- ACTIONS type Msg = NoOp | Increment | Decrement -- UPDATE update : Msg -> Model -> ( Model, Cmd Msg ) update action model = case action of Increment -> ( { model | count = model.count + 1 }, Cmd.none ) Decrement -> ( { model | count = model.count - 1 }, Cmd.none ) NoOp -> ( model, Cmd.none ) main = Redux.program { init = init 0 , update = update , encode = encodeModel , subscriptions = subscriptions }
Все, что осталось сделать, это определить действия и функцию update. Эта реализация очень похожа на исходный пример Counter, никаких дополнительных знаний не требуется. Еще один интересный аспект - здесь мы используем Redux.program.
Часть JavaScript будет состоять из подключенного компонента Counter.
import React from 'react' import { render } from 'react-dom' import { applyMiddleware, createStore, combineReducers } from 'redux' import { connect, Provider } from 'react-redux' import { compose } from 'ramda' import createElmMiddleware, { reducer as elmReducer } from 'redux-elm-middleware' const reducers = combineReducers({ elm: elmReducer, }) const elmStore = window.Elm.Reducer.worker() const {run, elmMiddleware} = createElmMiddleware(elmStore) const store = createStore(reducers, {}, compose( applyMiddleware(elmMiddleware), )) run(store)
Здесь много чего происходит. Мы получаем доступ к Elm.Reducer через окно и передаем его createElmMiddleware, который возвращает нам run и elmMiddleware функции. Затем мы создаем хранилище и применяем elmMiddleware к функции Redux applyMiddleware и, наконец, вызываем run с созданным хранилищем.
Остальной код специфичен для React.
const Counter = ({ count = 0, Inc, Dec }) => ( <div> <button onClick={Inc}>+</button> <p>Current count: {count}</p> <button onClick={Dec}>-</button> </div> ) const EnhancedCounter = connect( ({elm}) => ({ count: elm.count }), dispatch => ({ Inc: () => dispatch({ type: 'INCREMENT' }), Dec: () => dispatch({ type: 'DECREMENT' }), }), )(Counter) render( <Provider store={store}> <EnhancedCounter /> </Provider>, document.getElementById('app') )
Если вам интересно, как перенести приложение Redux или React на Elm, все это уже было продумано сообществом. Самый простой способ начать - это попробовать. Взгляните на пример redux-elm-middleware для более подробной реализации.
Вам все еще может быть интересно, что мы получаем от всего этого. Короче говоря, помимо того факта, что мы можем ввести в проект функциональный язык, мы также получаем чистое состояние и обработку эффектов из коробки, при этом сохраняя при этом преимущества экосистемы Redux.
Сильная сторона Elm - возможность взаимодействовать с JavaScript, в то же время изолировав любые плохие данные от самого Elm. Очевидно, что этот подход имеет свою цену, которая включает, например, необходимость ввода сложных объектов JSON. Вы можете иметь это в виду.
Наконец, вы помните ngReact? Оглядываясь назад, это звучит банально, но ngReact решил одну проблему, перенеся существующее приложение Angular на React. react-elm-components и redux-elm- промежуточное ПО открывают удобный способ внедрения Elm в существующий проект, аналогичный ngReact.
Постройте что-нибудь маленькое. Запустите его в производство. А потом вы сможете увидеть, нравится вам это или нет.
Ричард Фельдман, ReactiveConference 2016
Особая благодарность Christoph Hermann и Oskar Maria Grande за предоставленные отзывы.
Есть вопросы или отзывы? Подключиться через Twitter