Мы, программисты — мечтатели. Идем на поводу у хайпа, мечтая о новой серебряной пуле, которая решит все наши проблемы. А также, мы любим писать новые велосипеды, тем самым не решая проблемы, а создавая новые. Давайте в этой статье немного помечтаем об архитектуре, разрабатывая «Псевдо-новый» велосипед.
Мы, программисты — мечтатели. Идем на поводу у хайпа, мечтая о новой серебряной пуле, которая решит все наши проблемы. А также, мы любим писать новые велосипеды, тем самым не решая проблемы, а создавая новые. Давайте в этой статье немного помечтаем об архитектуре, разрабатывая «Псевдо-новый» велосипед.
Попробуем разработать свое архитектурное решение, используя современные подходы, с оглядкой на старших братьев Redux, Mobx и т.д.
Находясь в замечательном времени, в котором никому ранее не нужный JS, способный всего лишь анимировать DropDown, стал неожиданно мощнейшим языком! С кучей интересных, новых возможностей и переосмысленных «старых» особенностей. Все чаще и чаще, мы не идем в лоб, а проектируем нашу архитектуру, применяем патеры и решения, способные облегчить разработку и создать масштабируемый и легко усвояемый код.
Итак, начнем!
Сразу оговорюсь, что в нашем велосипеде мы будем решать вопрос не отрисовки View, а хранения данных и реакций на их изменения.
Будущее уже наступило! У нас есть ES2015+! С ним мы можем творить чудеса! Но, постойте… А что он нам дает? Чтоб вот так взять и написать наш новый супер-велосипед, который непременно должен решить все наши проблемы?
Давайте посмотрим на Proxy! Proxy, это способ отслеживать операции над объектом, такие как удаление, изменение и прочее. Сейчас Proxy поддерживается везде, кроме IE11, Vue3 под капотом будет использовать ее же. Ну а чем мы хуже, нечего плодить legacy, пусть IE11 останется за бортом нашей галеры, мы поплывем в светлое будущее. Короче, для велосипедостроения первый ингредиент будет — Proxy.
Но Proxy не решает всех проблем, да и не должен решать, всего лишь способ отследить взаимодействия с объектом. Давайте посмотрим на лучшие практики наших любимых решений.
Redux, что мы в нем любим? Единое место хранение данных для нашего приложения, способно хендлить изменения в одном месте, не размазывая логику по всем частям. Это плюс, берем! Middlewares, они способны расширять и дополнять функционал, организовать логирование, проксирование и т.д. Замечательно! Но самый главный плюс Redux’a, как по мне, это простота. Вот и нам, в нашем велосипеде, надо сделать все максимально просто.
Из минусов, это работа с асинхронностью. Много лишнего кода, который приходится писать… Надо как-то этого избежать.
Mobx — отличное решение, позволяющее отслеживать изменения. Этим у нас будет заниматься Proxy. Единственное, в дальнейшем, научим Proxy работать со вложенными структурами.
Типизация. Было бы неплохо использовать эту популярную нынче тему и даже дополнить её. Сделаем в нашем супер решении типизацию и валидацию в рантайме, с учетом вложенности.
RxJS, люблю его за то, что во многих случаях он способен за меня решать сложнейшие задачи, но нам не зачем его имплементить. Постараемся не усложнять наше решение, а использовать обычные объекты JS и возможно Rx будет работать с нашим велосипедом как нужно.
React, здесь я бы хотел остановиться поподробнее. React, это переломный фреймверк. До React’a у нас был Ember и Angular 1, дающий первичное представление о компонентом подходе. Мы писали много компонент и директив, дополняя основную логику, которая была сосредоточена в контроллерах, сервисах и т.д. Но с появлением React’a, мы стали рассматривать наше приложение более декларативно, не убегая от HTML, в том же контексте и можно сказать синтаксисе.
Декларативность компонентного подхода, это то что нам нужно достичь, чтобы все было максимально прозрачно и понятно. Модные сейчас render-props дают отличную возможность не выбиваться из потока и писать логику еще более декларативно, чем HOC’и, так что и их возьмем.
После того, как мы примерно определили, как это все должно выглядеть, давайте начнем писать код
Каждый из современных фреймверков хорош по-своему. Мы сейчас решаем проблему хранения состояния, по сути мы должны его использовать где угодно. В данной статье, для примера, я выбрал React, так как лучше его знаю. Но в идеале, это должно быть универсально.
Создадим компонент StateManager. Это основной компонент, хранящий все состояния приложения, как Redux. По желанию мы можем добавить валидацию (например, это решение) и типизацию (TypeScript/Flow). А также, написать по желанию функционал Middlewares, которые будут отрабатывать изменения до, после, во время (поместив их вызов в Proxy).
class StateManager extends Component {
componentWillMount() {
this.data = this.props.data || {};
this.proxify({props: this, val: 'data'});
}
proxify = ({props, val}) => {
Object.keys(props[val])
.filter(prop => typeof props[val][prop] === 'object' || Array.isArray(props[val][prop]))
.map(prop => ({props: props[val], val: prop}))
.forEach(item => this.proxify(item));
props[val] = this._makeProxy(props[val]);
};
_makeProxy = props => {
return new Proxy(props, {
set: (target, key, args) => {
if (typeof args === 'object' || Array.isArray(args)) {
target[key] = args;
this.proxify({props: target, val: key});
}
else {
target[key] = args;
}
setTimeout(this.forceUpdate.bind(this));
return true;
},
deleteProperty: (oTarget, key) => {
setTimeout(this.forceUpdate.bind(this));
return true;
}
});
};
render() {
return this.props.children(this.data);
}
}
StateManager.propTypes = {
data: PropTypes.object.isRequired
};
По порядку, что тут происходит. Извне, мы пробрасываем данные нашего приложения, либо пустой объект (схему), который будем заполнять в процессе. Заворачиваем наше приложение
<StateManager
data={{
state: {}
}}>
{store =>
...НАШЕ ПРИЛОЖЕНИЕ…
}
</StateManager>
метод proxify рекурсивно трансформирует в Proxy все объекты / массивы, делая их восприимчивыми на изменения, через метод _makeProxy.
В самом прокси объекте я добавил 2 хука — set, на любое изменение, deleteProperty — на любое удаление. В set мы смотрим, если у нас пришел объект, массив, или глубокие данные, мы их снова прогоняем через proxify, после чего вызываем forceUpdate, что приведет к перерисовке всех дочерних элементов с обновлённым состоянием нашего приложения.
Пробрасывая измененное состояние через render-prop, мы автоматически сможем его разбросать в любые части приложения.
Так как, по сути, это обычный объект, мы можем к нему прикрутить тот же Rx, или что угодно, мутировать когда угодно, и вообще извращаться сколько влезет.
По скорости Проксируемый объект существенно отличается от обычного присваивания. Провел замеры на создание и присваивание строки в объект с глубиной вложенности 2:
для обычного объекта
~ 0.006ms
Proxy
~ 0.05ms
Это обычное присваивание без реакта и прочего. Будем надеяться, что это оптимизируют.
С этим примитивным подходом, написанным за несколько часов «на коленке», можно создать такой to-do list, в котором данные приходят асинхронно, и мы можем управлять выводом элементов
<StateManager
data={{
state: {}
}}>
{store => <div>
<button onClick={() => store.state = {
todo: {
items: [
{name: 'Get data', state: 'in-progress'},
{name: 'Bind proxy to all', state: 'in-progress'},
{name: 'Force update root component', state: 'in-progress'},
{name: 'Update data in children', state: 'in-progress'}
],
filter: ''
}}
}>Load todolist</button>
<hr/>
<div>
{
store.state.todo &&
store.state.todo.items &&
[
<ul key="todolist">
{
store.state.todo.items
.map((item, index) =>
<li
hidden={!(store.state.todo.filter === '' || item.state === store.state.todo.filter)}
style={item.state === 'done' ? {
textDecoration: 'line-through'
} : {}}
key={index}>
{item.name}
{item.state === 'done' ?
<button onClick={() => store.state.todo.items[index].state = 'in-progress'}>Set to in progress</button> :
<button onClick={() => store.state.todo.items[index].state = 'done'}>Set to done</button>}
<button onClick={() => store.state.todo.items.splice(index, 1)}>Remove</button>
</li>
)
}
</ul>,
<div key="add-new">
<input type="text" ref={c => this.input = c} />
<button onClick={() => {
if (this.input && !!this.input.value) {
store.state.todo.items.push({name: this.input.value, state: 'in-progress'});
this.input.value = '';
}
}}>Add new item</button>
</div>,
<select key="filter" onChange={e => store.state.todo.filter = e.target.value}>
<option key="all" value="">All</option>
<option key="in-progress" value="in-progress">In progress</option>
<option key="done" value="done">Done</option>
</select>
]
}
</div>
<br/>
<br/>
<br/>
<hr/>
<h3>State:</h3>
<pre>{JSON.stringify(store, true , 2)}</pre>
</div>
}
</StateManager>
Данным примером я хотел поделиться с вами своими мыслями о использовании Proxy и нескольких хороших практик. Надеюсь, что использование новых стандартов вдохновит Вас на создание полноценных решений, которые достойно займут свое место в нашем зоопарке JS. Всем спасибо, удачи в велосипедостроении!
Ссылки:
→ Исходный код
→ To-do list поклацать
Автор: AlexSergey