Что-то что-то старая собака новые уловки

Если вы хоть раз заглянули в Интернет за последний год, то наверняка слышали о React и Redux. React был подтвержден как самый сексуальный фреймворк 2016 года, а Redux сочетается с React как арахисовое масло и желе.

У меня был большой опыт создания приложений с помощью React и Redux. К сожалению, использование последних достижений в проектах не всегда возможно. Моя команда уже знакома с Knockout.js, и у нас есть множество приложений Knockout, которые нам нужно будет поддерживать в обозримом будущем. Переход на React / Redux означал бы обучение всей команды, а также новых сотрудников по React, Redux, и Knockout. Чтобы оправдать переход, новый фреймворк должен решить больше проблем, чем он вызвал.

Имея это в виду, я решил сделать создание приложений Knockout более похожими на создание приложений React / Redux. В частности, я хотел:

1. Создавайте приложения из независимых модульных компонентов (например, React).
2. Отделяйте состояние приложения от уровня представления (например, Redux).

В конце концов, мы любим не сами инструменты; инструмент - это просто реализация решения. Нам нравятся решения, лежащие в основе инструментов… не так ли?

Компоненты

Одна из моих любимых частей использования React - разделение моих приложений на небольшие повторно используемые компоненты. Knockout может безболезненно воспроизвести это с помощью компонентов и нестандартных элементов.

Просто зарегистрируйте компоненты с помощью Knockout:

function viewModel(params) {
    const self = this;
    self.count = ko.observable(params.initialCount);
    self.increment = () => self.count(self.count() + 1);
}
const template = 
`<div>
    <span data-bind="text: count"></span>
    <button type="button" data-bind="click: increment">
        Increment
    </button>
</div>`;
ko.components.register('counter', { viewModel, template }); 

Затем используйте их в разметке:

<body>
    <h1>Counter starting at 1:</h1>
    <counter params="initialCount: 1"></counter>
</body>

Компоненты также могут быть вложенными и поддерживать передачу данных от родителя к потомку через объект params. Единственная проблема в том, что мы должны определять наши шаблоны как строки. Фу! Если вы уже используете какой-то процесс сборки для своих файлов, эту проблему легко решить, загрузив .html файлы в виде строк. Если вы используете webpack, то html-loader как раз и нужен. Затем мы можем переместить нашу разметку в отдельный файл:

<!-- counter.template.html -->
<div>
    <span data-bind="text: count"></span>
    <button type="button" data-bind="click: increment">
        Increment
    </button>
</div>

И измените нашу регистрацию на это:

import template from './counter.template.html';
function viewModel(params) {
    const self = this;
    self.count = ko.observable(params.initialCount);
    self.increment = () => self.count(self.count() + 1);
}
ko.components.register('counter', { viewModel, template });

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

import template from './counter.template.html';
import './counter.styles.css';
function viewModel(params) {
    const self = this;
    self.count = ko.observable(params.initialCount);
    self.increment = () => self.count(self.count() + 1);
}
ko.components.register('counter', { viewModel, template });

Теперь поведение, разметка и стиль для каждого компонента могут существовать вместе, независимо от самих приложений. Это делает наши компоненты модульными и многоразовыми, как и в случае с React.

Государственное управление

Создав приложение с компонентами Knockout, я понял, почему React наиболее эффективен с каким-то инструментом управления состоянием, таким как Redux. Разделение вашего приложения на более мелкие компоненты упрощает их обслуживание и повторное использование, но неизбежно появятся некоторые данные, которые необходимо разделить между несколькими разными компонентами. Эти данные и есть «состояние» вашего приложения.

Когда вы отделяете состояние приложения (Redux) от уровня представления (React), вы можете вносить изменения в одно, не мешая другому. Вместо того, чтобы вручную обеспечивать синхронизацию всех частей вашего представления друг с другом, мы можем просто убедиться, что каждая часть синхронизирована с состоянием, что намного проще сделать.

Изначально я намеревался использовать Redux с Knockout и создать пакет knockout-redux, похожий на react-redux. Однако я быстро понял, что Knockout и Redux не совсем подходят друг другу.

Redux - это однонаправленный поток данных. Слой представления подписывается на хранилище состояния и повторно отображает его при обновлении состояния, но представление никогда не изменяет состояние напрямую.

С другой стороны, Knockout больше касается двусторонней привязки данных с наблюдаемыми. Observables уже созданы для подписки. Это означает, что состояние нашего приложения Knockout может быть просто составлено из наблюдаемых, а Knockout будет обрабатывать обновление зависимостей состояния за нас.

Я построил knockout-store как замену использованию чего-то вроде knockout-redux. Вместо того, чтобы внедрять Redux в приложения Knockout, knockout-store использует инструменты, уже предоставляемые Knockout (а именно наблюдаемые). Он предлагает методы для получения и установки состояния вашего приложения и подключения компонентов к состоянию.

Сначала мы устанавливаем состояние всего нашего приложения:

import { setState } from 'knockout-store';
setState({ count: ko.observable(1) });

Затем мы можем связать наши модели представления с состоянием с помощью метода connect (который был явно украден из react-redux):

import { connect } from 'knockout-store';
import template from './counter.template.html';
import './counter.styles.css';
function viewModel(params) {
    const self = this;
    self.count = params.count;
    self.increment = () => self.count(self.count() + 1);
}
function mapStateToParams({ count }) {
    return { count };
}
const ConnectedViewModel = connect(mapStateToParams)(viewModel);
ko.components.register('counter', {
    template,
    viewModel: ConnectedViewModel
});

Если вы использовали response-redux, это, вероятно, покажется вам до жути знакомым. Мы определяем функцию mapStateToParams вместо mapStateToProps. Эта функция получит объект, переданный в setState, и любые свойства, возвращаемые ею, будут прикреплены к объекту params нашей модели представления. mapStateToParams передается в connect, который возвращает другую функцию, которая обертывает модель представления, с которой мы ее вызываем. Конечным результатом является наша исходная модель представления с некоторыми дополнительными свойствами из состояния, прикрепленного к объекту params. Подробнее о мотивации нокаут-магазина можно прочитать в его вики.

В нашем небольшом примере приложения результат таков, что каждый компонент счетчика, отображаемый на странице, будет отображать одно и то же количество. Мы можем щелкнуть любую из кнопок, чтобы обновить счетчик для каждого компонента; это часть состояния приложения. Поскольку count является наблюдаемым, мы также можем подписаться на него или использовать его в вычисляемых функциях, а Knockout будет синхронизировать все для нас. Если бы мы захотели, мы могли бы даже разделить наш счетчик на презентационные и контейнерные компоненты, как приложение React / Redux.

Отличия от Redux и React-Redux

Хотя knockout-store был вдохновлен Redux и react-redux, реализации довольно разные.

Я уже упоминал, что knockout-store не поддерживает однонаправленный поток данных. Модели просмотра могут изменять состояние напрямую; на самом деле это просто объект, к которому у всех есть доступ. Knockout будет синхронизировать все, пока свойства состояния наблюдаемы, но, по общему признанию, это более беспорядочно из-за природы двусторонних привязок данных.

Когда состояние изменяется в приложении Redux, сам объект состояния заменяется новым. В knockout-store объект состояния всегда тот же объект, что изначально установлен с помощью setState. К сожалению, это означает, что вы не можете так просто отменить / повторить историю, как с приложением Redux.

Наконец, в react-redux есть <Provider> component, используемый для предоставления состояния компонентам с помощью connect. Первоначально я пытался воспроизвести это, но обнаружил, что в любом случае не рекомендуется использовать несколько магазинов для приложения Redux. Из-за этого я полностью удалил любую концепцию <Provider>; это означает, что у вас может быть только один объект состояния приложения для каждого приложения, и connect автоматически получает к нему доступ.

Различий больше, но сейчас я могу вспомнить самые большие.

Что мы узнали

После создания примера todo-приложения с использованием компонентов и knockout-store я очень доволен опытом разработки. Мне нравилось создавать приложение по многим из тех же причин, по которым мне нравится создавать приложения React / Redux. Мне нравилось строить его из более мелких, многоразовых компонентов и держать компоненты отделенными друг от друга, отделяя состояние приложения от уровня представления. Хотел бы я по-прежнему писать приложения на React / Redux? Совершенно верно, но теперь я все еще могу отлично проводить время, когда этого просто нет в картах.

Оказывается, дело не столько в инструментах, которые мы используем, сколько в проблемах, которые они решают. Через несколько лет React станет старой новостью, а новый фреймворк станет новой модой (Vue?). Нам всем нужно будет сразу же покинуть корабль?

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

Я также научился не недооценивать старые инструменты. Нокаут-магазин смехотворно мал. Knockout предоставил мне отличную песочницу для игры. Knockout исполнилось семь лет, а это примерно пятьсот лет по программному обеспечению. Тем не менее, немного подумав, мы можем применить к нему современные идеи и продолжать создавать отличные приложения. Если моя команда все же решит начать создавать приложения React / Redux в будущем, у нас уже будут некоторые концепции.

Мы всегда называем новые инструменты «блестящими». Может быть, нашим старым просто нужно немного отполировать.

Привет, я Алекс. Я делаю генеративную музыку на Generative.fm. Многие разработчики (в том числе и я) считают, что он идеально подходит для прослушивания кода. Это тоже открытый код!