Статьи

Управление государством в Аурелии: как использовать Аурелию с Redux

Монитор с двумя динозаврами, быстро приближающимся метеором и кнопками отмены / повтора

Эта статья была рецензирована Морицем Крёгером и Джеддом Ахенгом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше! Спасибо также Саймону Кодрингтону за создание демо.

В настоящее время при разработке веб-приложения большое внимание уделяется контейнерам состояний, особенно всем видам шаблонов Flux . Одной из самых известных реализаций Flux является Redux . Для тех из вас, кто еще не успел заполучить шумиху, Redux — это библиотека, которая помогает вам сохранять предсказуемость мутаций состояния. Он хранит все состояние вашего приложения в одном дереве объектов.

В этой статье мы рассмотрим основы использования Redux с Aurelia — клиентской платформой JavaScript с открытым исходным кодом следующего поколения. Но вместо того, чтобы строить еще один контрпример, мы собираемся сделать что-то более интересное. Мы собираемся создать простой редактор уценки с функциями отмены и повтора. Код для этого урока доступен на GitHub, и здесь есть демонстрация готового проекта .

Примечание . Когда я узнаю что-то новое, я предпочитаю вернуться к источнику, а в случае с Redux есть замечательная серия видео Egghead от создателя Redux (Дана Абрамова). Поскольку мы не будем вдаваться в подробности о том, как работает Redux, если вам требуется переподготовка и у вас есть пара свободных часов, я настоятельно рекомендую дать серию шанс.

Как устроен этот учебник

В этой статье я собираюсь создать три версии одного и того же компонента.

Первая версия будет использовать чистый подход Aurelia. Здесь вы узнаете, как настроить приложение Aurelia, настроить зависимости и создать необходимые View и ViewModel. Мы рассмотрим пример построения классического способа Aurelia с использованием двусторонней привязки данных.

Вторая версия представит Redux для обработки состояния приложения. Мы будем использовать ванильный подход, что означает отсутствие дополнительного плагина для обработки взаимодействия. Таким образом, вы узнаете, как использовать готовые функции Aurelia для адаптации к процессу разработки Redux.

В окончательной версии будет реализована функция отмены / возврата. Любой, кто создал такую ​​функциональность с нуля, знает, что довольно легко начать работу, но все может быстро выйти из-под контроля. Вот почему мы будем использовать плагин redux-undo, чтобы справиться с этим для нас.

На протяжении всей статьи вы увидите несколько ссылок на официальные документы Aurelia, которые помогут вам найти дополнительную информацию. Все списки кода также ссылаются на свои исходные файлы.

Итак, без лишних слов, давайте начнем.

Строительные леса нового приложения Aurelia

Поскольку мы концентрируемся на взаимодействии с Aurelia, пример основан на новом предпочтительном способе Aurelia создания приложений, Aurelia CLI.

Следуя шагам, описанным в Документах CLI , мы устанавливаем CLI глобально с помощью следующей команды:

npm install aurelia-cli -g 

Далее мы создадим новое приложение, используя:

 au new aurelia-redux 

Откроется диалоговое окно с вопросом, хотите ли вы использовать настройки по умолчанию или настроить свой выбор. Выберите значение по умолчанию (ESNext) и выберите создание проекта и установку зависимостей. Затем измените каталог в папку вашего нового проекта (используя cd aurelia-redux ) и запустите сервер разработки с:

 au run --watch 

Если все идет по плану, запускается экземпляр сервера разработки BrowserSync , прослушивающий по умолчанию порт 9000. Кроме того, он будет отслеживать изменения, внесенные в ваше приложение, и обновлять при необходимости.

Добавление зависимостей в Bundler

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

 npm install --save marked redux redux-undo 

Хорошо, давайте пройдемся по каждому из них. Marked — это полнофункциональный, простой в использовании анализатор и компилятор уценки, который мы будем использовать для… ну, именно для того, что написано на банке. Redux — это пакет для самой библиотеки, а redux-undo — простой плагин для добавления функций отмены / повтора для контейнера состояния нашего приложения.

Под капотом Aurelia CLI используется RequireJS, поэтому все зависимости указываются в формате определения асинхронного модуля (AMD). Теперь осталось сообщить приложению Aurelia, как и где оно может найти эти зависимости.

Для этого откройте файл aurelia.json который находится в aurelia.json aurelia-project вашего приложения. Если вы прокрутите вниз до раздела bundles вы увидите два объекта. Один для app-bundle , содержащий ваш собственный код приложения, за которым следует vendor-bundle используемый для объединения всех зависимостей вашего приложения в отдельный файл пакета. Этот объект содержит свойство под названием dependencies и вы уже догадались, это место, где мы собираемся добавить наши дополнительные.

Управление файлом aurelia.json вручную, в настоящее время является необходимым шагом, но он будет автоматизирован в будущих версиях.

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

 // file: aurelia_project/aurelia.json ... { "name": "text", "path": "../scripts/text" }, // START OF NEW DEPENDENCIES, DON'T COPY THIS LINE { "name": "marked", "path": "../node_modules/marked", "main": "marked.min" }, { "name": "redux", "path": "../node_modules/redux/dist", "main": "redux.min" }, { "name": "redux-undo", "path": "../node_modules/redux-undo/lib", "main": "index" }, // END OF NEW DEPENDENCIES, DON'T COPY THIS LINE { "name": "aurelia-templating-resources", "path": "../node_modules/aurelia-templating-resources/dist/amd", "main": "aurelia-templating-resources" }, ... 

Подключение зависимости приложения

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

 au run --watch 

Вот и все, теперь мы готовы испачкать руки кодом.

Добавление стиля

Ни один редактор уценок не будет полным без приличного стиля. Мы начнем с включения стильного шрифта в index.html в корневой папке.

 <head> <title>Aurelia MarkDown Editor</title> <link href="https://fonts.googleapis.com/css?family=Passion+One:400,700|Roboto:300,400,500,700" rel="stylesheet" type="text/css"> </head> 

После этого мы добавим несколько стилей в /src/styles.css . Вместо того, чтобы перечислять все CSS здесь, я бы посоветовал вам взглянуть на файл CSS на GitHub и использовать эти стили в своем собственном проекте.

Делая это Aurelia Way

Мы начнем с создания нового пользовательского элемента с именем <markdown-aurelia> будет действовать как наш логический контейнер. Мы делаем это, следуя соглашениям Aurelia по умолчанию о создании ViewModel markdown-aurelia.js и View markdown-aurelia.html внутри папки src .

Соглашения являются мощными, но иногда могут не подходить для вашего приложения. Обратите внимание, что вы всегда можете переопределить их при необходимости, следуя этим инструкциям

Теперь давайте посмотрим на представление для нашего нового компонента. Компоненты Aurelia Views заключены в <template> , поэтому вся наша разметка должна быть вложена в него.

Начнем с того, что нам нужен наш файл CSS. Затем, после заголовка, мы используем <div> для размещения <textarea> , который будет служить нашей панелью редактора, и второго <div> , который будет отображать скомпилированные результаты. Эти элементы имеют свои свойства value и innerHTML связанные с двумя свойствами в ViewModel с помощью команды Aurelia bind .

Для панели редактора мы связываемся со свойством raw в ViewModel. По умолчанию Aurelia будет использовать двустороннее связывание, так как это элемент управления формой.

Для предварительного просмотра <div> мы привязываем к свойству innerHTML . Мы делаем это (вместо простой ${html} интерполяции), чтобы результирующий HTML отображался как HTML, а не как строка. В этом случае Aurelia решит использовать одностороннюю привязку, поскольку она не видит атрибут contenteditable для элемента и, следовательно, не ожидает ввода данных пользователем.

 // file: src/markdown-aurelia.html <template> <require from="./styles.css"></require> <h1>Aurelia Markdown Redux</h1> <div class="markdown-editor"> <textarea class="editor" value.bind="raw"></textarea> <div class="preview" innerHTML.bind="html"></div> </div> </template> 

Markdown View, Aurelia Way

Ничего себе … нет Меньше / Sass / Compass / что угодно … конечно есть много способов стилизовать компоненты в Aurelia. Посмотрите здесь, чтобы увидеть, какие варианты в вашем распоряжении.

На самом деле в этом нет ничего большего, поэтому давайте посмотрим на ViewModel, которая, если честно, такая же короткая. Здесь мы начнем с импорта marked зависимости. Вы помните процесс подключения с aurelia.json мы делали раньше? Все это было сделано, чтобы разрешить импорт внешних модулей в стиле ES6. Кроме того, мы импортируем bindable декоратор.

В соответствии с соглашением Аурелии, ViewModel — это простой класс ES6, названный с использованием версии имени файла в UpperCamelCased. Теперь мы собираемся объявить одно из свойств этого класса ( raw ) как связываемое с помощью декоратора в стиле ES7. Мы должны сделать это, так как мы используем это свойство для передачи информации в компонент (через <textarea> ).

После этого мы определяем свойство html для хранения скомпилированной уценки. Наконец, мы определяем функцию rawChanged , которая будет rawChanged всякий раз, когда изменяется значение необязательной привязки. Он принимает newValue в качестве аргумента, который можно использовать в качестве входных данных для ранее импортированной marked функции. Возвращаемое значение этой функции присваивается свойству html компонента.

 // file: src/markdown-aurelia.js import marked from 'marked'; import { bindable } from 'aurelia-framework'; export class MarkdownAurelia { @bindable raw; html = ''; rawChanged(newValue) { this.html = marked(newValue); } } 

Markdown ViewModel, путь Аурелия

Единственное, что осталось сделать, прежде чем мы сможем использовать наш новый компонент, — это сделать его где-нибудь. Мы сделаем это внутри root компонента приложения, поэтому откройте файл src/app.html и замените содержимое следующим:

 // file: src/app.html <template> <require from="./markdown-aurelia"></require> <markdown-aurelia raw.bind="data"></markdown-aurelia> </template> 

Использование компонента Markdown

Здесь мы импортируем компонент в представление с помощью <require> . Атрибут from указывает, где Aurelia должна искать компонент.

После этого мы <markdown-aurelia> компонент <markdown-aurelia> и привязываем свойство data к нашему raw свойству, которое будет действовать как начальное значение для компонента.

Мы определяем это свойство data в файле app.js , соответствующем ViewModel представлению компонента App .

 // file: src/app.js export class App { constructor() { this.data = 'Hello World!'; } } 

Настройка данных уценки по умолчанию

И вуаля! У нас есть рабочий редактор уценки!

Представляем Redux в стек

Redux можно описать тремя основными принципами . Первый принцип — единственный источник правды . Это все о наличии одного места для хранения состояния вашего приложения, а именно одного объекта JavaScript (также называемого деревом состояний). Второй принцип заключается в том, что состояние только для чтения . Это гарантирует, что само государство не может быть изменено, но должно быть полностью заменено. Третий принцип заключается в том, что эти изменения должны быть сделаны с использованием чистых функций . Это означает отсутствие побочных эффектов, и мы всегда должны быть в состоянии воссоздать состояние одинаковым образом.

В каждом приложении Redux также есть три основных объекта: действия , редукторы и хранилище . Действие — это то, что вы отправляете в любое время, когда хотите изменить состояние. Это простой объект JavaScript, описывающий изменение в минимально возможных терминах. Редукторы — это чистые функции, которые принимают состояние приложения и отправляемое действие и возвращают следующее состояние приложения. Наконец, хранилище содержит объект состояния, оно позволяет вам отправлять действия. Когда вы создаете его, вам нужно передать ему редуктор, который определяет, как должно обновляться состояние.

Это столько резюме, сколько я хотел бы дать. Если вам нужна переподготовка, обратитесь к официальным документам Redux или видеокурсу Дана Абрамова на egghead.io. Я также могу порекомендовать Морица Крегера « Мой опыт работы с Redux и Vanilla JavaScript» здесь, на SitePoint.

Теперь, без лишних слов, давайте посмотрим на Markdown ViewModel в стиле Redux.

Путь Redux

Давайте начнем с создания новых файлов markdown-redux.html и markdown-redux.js в нашей папке src . В обоих этих файлах мы можем просто скопировать существующий код Aurelia и на следующих шагах добавить к ним дополнительные части Redux.

Начиная с ViewModel, мы сначала импортируем функцию createStore , которую затем используем внутри нашего объявления класса, чтобы инициализировать хранилище. Мы передаем хранилищу ссылку на нашу функцию-редуктор ( textUpdater ) и присваиваем ее свойству store нашего класса. Обратите внимание, что для простоты в этом примере создатель редуктора и действия хранится в том же файле, что и ViewModel.

Следующее изменение происходит внутри конструктора, где мы используем функцию subscribe для регистрации обратного вызова update который хранилище Redux будет вызывать каждый раз, когда отправляется действие. Вы можете видеть, что мы использовали метод bind для передачи правильного контекста выполнения обратному вызову. Этот обратный вызов позаботится о рендеринге всех будущих состояний.

Сам метод update просто запрашивает последнее состояние из хранилища, используя метод Redux getState и присваивает результирующие значения нашим html и raw свойствам.

Чтобы ответить на ввод пользователя, мы создаем метод keyupHandler который принимает newValue в качестве единственного аргумента. Здесь мы подошли к важнейшей части философии Redux — единственный способ вызвать изменение состояния — отправить действие. Таким образом, это единственное, что сделает наш обработчик: отправит новое действие updateText которое получает newValue в качестве аргумента.

Все идет нормально? Мы почти там. Но так как компонент будет инициализирован с некоторым текстом по умолчанию — помните свойство raw? — нам также нужно убедиться, что начальное значение будет отображено. Для этого мы можем использовать хук жизненного цикла Aurelia, прикрепленный для вызова keyupHandler , после того, как компонент был подключен к DOM.

 // file: src/markdown-redux.js import marked from 'marked'; import { bindable } from 'aurelia-framework'; import { createStore } from 'redux'; export class MarkdownRedux { @bindable raw; html = ''; store = createStore(textUpdater); constructor() { this.store.subscribe(this.update.bind(this)); } update() { const state = this.store.getState(); this.html = state.html; this.raw = state.raw; } keyupHandler(newValue) { this.store.dispatch(updateText(newValue)); } attached() { this.keyupHandler(this.raw); } } 

Компонент разметки Redux Way — ViewModel

Добавление создателя и редуктора действий

В дополнение к обновлениям ViewModel нам также необходимо взглянуть на действие и редуктор. Помните, что Redux — это не что иное, как набор функций, и поэтому наше единственное действие будет создано функцией updateText . Это допускает преобразование text в HTML, который, в соответствии с философией Redux, инкапсулирует внутри объекта свойство типа TEXT_UPDATE . Свойство text указывается с использованием синтаксиса сокращенного имени свойства ES6.

Поскольку в нашем примере требуется один редуктор, textUpdater действует как корневой редуктор. Состояние по умолчанию, если оно не указано, — это объект с пустыми raw свойствами и html , указанными с использованием синтаксиса значения по умолчанию ES6 . Затем редуктор проверяет тип action и либо, в качестве хорошей практики, возвращает состояние, если совпадений не найдено, или возвращает новое состояние.

 // file: src/markdown-redux.js const TEXT_UPDATE = 'UPDATE'; // action creator const updateText = (text) => { return { type: TEXT_UPDATE, text }; }; // reducer function textUpdater(state = { raw: '', html: '' }, action) { switch (action.type) { case TEXT_UPDATE: return { raw: action.text, html: marked(action.text) }; default: return state; } } 

Компонент уценки Redux Way — Действие / Редуктор

Обновление вида

Теперь, если мы посмотрим на то, что мы достигли с изменениями ViewModel, мы заметим, что обновления компонента ограничены либо инициализатором (компонентом App который предоставляет начальное значение для raw свойства), либо методом update . Это противоречит двустороннему связыванию Aurelia, которое позволяет декларативно изменять значение из разметки.

Вот как мы можем изменить вид, чтобы он соответствовал новой парадигме. Вместо использования ключевого слова Aurelia bind мы будем использовать one-way привязку для атрибута value текстовой области. Таким образом, мы переопределяем поведение двусторонней привязки по умолчанию и инициируем однонаправленный процесс обновления из ViewModel в View.

Чтобы захватить ввод пользователя, нам также нужно подключить событие keyup , что мы можем сделать с привязкой trigger . При каждом нажатии клавиши keyupHandler и передается значение <textarea> . Мы используем специальное свойство $event для доступа к собственному DOM-событию и оттуда к значению target . И последнее, но не менее важное: мы не хотим повторять визуализацию при каждом нажатии клавиши, а после того, как пользователь прекратил печатать. Мы можем сделать это, используя поведение привязки debounce Aurelia.

Вместо trigger мы могли бы также использовать delegate . Хотите понять разницу? Посмотрите здесь

 // file: src/markdown-redux.html <template> <require from="./styles.css"></require> <h1>Aurelia Markdown Redux</h1> <div class="markdown-editor cf"> <textarea class="editor" keyup.trigger="keyupHandler($event.target.value) & debounce" value.one-way="raw"></textarea> <div class="preview" innerHTML.bind="html"></div> </div> </template> 

Компонент уценки Redux Way — Просмотр

Наконец, не забудьте обновить app.html чтобы создать новый компонент

 // file: src/app.html <template> <require from="./markdown-redux"></require> <markdown-redux raw.bind="data"></markdown-redux> </template> 

Обновление App.html для визуализации Redux-Component

Реализация Отменить / Повторить

До сих пор мы только что адаптировали наш оригинальный компонент Aurelia для использования рабочего процесса Redux. Честно говоря, пока не так много пользы. Почему мы сделали все это? Наличие единой точки, где происходят обновления, могло бы быть сделано и с помощью подхода чистой Aurelia. Оказывается, еще раз, все дело в функциях, которые делают этот подход значимым. На следующем шаге мы увидим, как мы можем добавить функции отмены и возврата в наш компонент, чтобы обрабатывать изменения состояния во времени и перемещаться между ними.

Начнем с создания новых файлов markdown.html и markdown.js в нашей папке src . Опять же, в обоих этих файлах мы можем просто скопировать существующий код Aurelia и на следующих шагах добавить к ним дополнительный код.

На этот раз мы сделаем это наоборот и сначала посмотрим на представление. Здесь мы добавляем новый элемент <div> над разделом markdown-editor . Внутри этого элемента мы размещаем две кнопки, которые будут действовать как триггеры отмены и повтора. Мы также хотели бы отобразить количество предыдущих состояний ( pastCount ) и будущих ( futureCount ) внутри соответствующих кнопок. Мы сделаем это с помощью простой интерполяции.

 // file: src/markdown.html <template> <require from="./styles.css"></require> <h1>Aurelia Markdown Redux</h1> <div class="toolbar"> <button click.trigger="undo()">(${pastCount}) Undo</button> <button click.trigger="redo()">Redo (${futureCount})</button> </div> <div class="markdown-editor cf"> ... </div> </template> 

Компонент уценки с отменой / возвратом — просмотр

Теперь пришло время взглянуть на изменения в ViewModel. Создатель действия и редуктор остаются прежними, но новшеством является импорт функции ActionCreators функции ActionCreators из модуля ActionCreators -undo. Обратите внимание, что undoable функция экспортируется по умолчанию, поэтому мы можем избавиться от фигурных скобок. Мы используем эту функцию, чтобы обернуть нашу textUpdater редуктора textUpdater , которую мы передаем createStore . Это все, что нужно для того, чтобы наш магазин мог обрабатывать и отменять функциональность.

В дополнение к этому мы futureCount свойства pastCount и futureCount , которые мы инициализируем нулем. Рассматривая метод update мы теперь видим, что метод getState по умолчанию вместо возврата состояния возвращает объект с getState , past и future состояниями. Мы используем present состояние, чтобы присвоить новые значения нашим html и raw свойствам. Поскольку past и future — это массивы состояний, мы можем просто использовать их свойство length для обновления наших значений. Наконец, что не менее redo методы undo и undo теперь отправляют новые действия, автоматически ActionCreators объектом ActionCreators .

 // file: src/markdown.js import marked from 'marked'; import { bindable } from 'aurelia-framework'; import { createStore } from 'redux'; import undoable from 'redux-undo'; import { ActionCreators } from 'redux-undo'; export class Markdown { @bindable raw; html = ''; store = createStore(undoable(textUpdater)); pastCount = 0; futureCount = 0; constructor() { ... } update() { const state = this.store.getState().present; this.html = state.html; this.raw = state.raw; this.pastCount = this.store.getState().past.length; this.futureCount = this.store.getState().future.length; } keyupHandler(newValue) { ... } undo() { this.store.dispatch(ActionCreators.undo()); } redo() { this.store.dispatch(ActionCreators.redo()); } attached() { ... } } 

Компонент уценки с отменой / повтором — ViewModel

Снова обновите app.html чтобы создать окончательную версию компонента.

 // file: src/app.html <template> <require from="./markdown"></require> <markdown raw.bind="data"></markdown> </template> 

Обновление App.html для визуализации Redux-Component

И это все, что нужно. Причина, по которой все это работает так легко, в том, что мы следовали стандартному рабочему процессу, который предлагает Redux.

Вывод

Архитектура Redux вращается вокруг строгого однонаправленного потока данных. Это имеет много преимуществ, но также имеет свою цену. Если вы сравните исходный путь Aurelia с первым переписыванием Redux, вы увидите, что здесь задействовано гораздо больше шаблонов. Конечно, есть абстракции и более приятные интеграции, такие как плагин aurelia-redux-plugin (который добавляет еще один классный подход с диспетчерами диспетчера и селектора), но в конце концов, это либо вопрос большего количества кода, либо больше вещей для изучения.

Я обнаружил, что при оценке новых концепций, самое главное, чтобы действительно понять, как они работают. Только тогда вы действительно сможете решить, подходит ли вам компромисс между сложностью и стабильностью. Лично мне нравится идея думать о моем приложении как о множестве состояний, и я более чем рад видеть простую интеграцию из коробки (и даже более глубокую, такую ​​как вышеупомянутый плагин) в вашем распоряжении с Aurelia ,

Я надеюсь, что вам понравился этот пример, и теперь у вас есть лучшее представление о том, как вы могли бы применить свои существующие навыки Redux к Aurelia или заимствовать идеи и применять их в своем подходе к разработке по умолчанию. Дайте нам знать об этом на официальном канале Aurelia Gitter или в комментариях ниже.