javascript

Будущее с Proxy

  • воскресенье, 5 ноября 2017 г. в 03:13:25
https://habrahabr.ru/post/341622/
  • ReactJS
  • JavaScript
  • AngularJS


image

Мы, программисты — мечтатели. Идем на поводу у хайпа, мечтая о новой серебряной пуле, которая решит все наши проблемы. А также, мы любим писать новые велосипеды, тем самым не решая проблемы, а создавая новые. Давайте в этой статье немного помечтаем об архитектуре, разрабатывая «Псевдо-новый» велосипед.

Попробуем разработать свое архитектурное решение, используя современные подходы, с оглядкой на старших братьев Redux, Mobx и т.д.

Находясь в замечательном времени, в котором никому ранее не нужный JS, способный всего лишь анимировать DropDown, стал неожиданно мощнейшим языком! С кучей интересных, новых возможностей и переосмысленных «старых» особенностей. Все чаще и чаще, мы не идем в лоб, а проектируем нашу архитектуру, применяем патеры и решения, способные облегчить разработку и создать масштабируемый и легко усвояемый код.

Итак, начнем!

image

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

Будущее уже наступило! У нас есть ES2015+! С ним мы можем творить чудеса! Но, постойте… А что он нам дает? Чтоб вот так взять и написать наш новый супер-велосипед, который непременно должен решить все наши проблемы?

Давайте посмотрим на Proxy! Proxy, это способ отслеживать операции над объектом, такие как удаление, изменение и прочее. Сейчас Proxy поддерживается везде, кроме IE11, Vue3 под капотом будет использовать ее же. Ну а чем мы хуже, нечего плодить legacy, пусть IE11 останется за бортом нашей галеры, мы поплывем в светлое будущее. Короче, для велосипедостроения первый ингредиент будет — Proxy.

Но Proxy не решает всех проблем, да и не должен решать, всего лишь способ отследить взаимодействия с объектом. Давайте посмотрим на лучшие практики наших любимых решений.

image

Redux, что мы в нем любим? Единое место хранение данных для нашего приложения, способно хендлить изменения в одном месте, не размазывая логику по всем частям. Это плюс, берем! Middlewares, они способны расширять и дополнять функционал, организовать логирование, проксирование и т.д. Замечательно! Но самый главный плюс Redux’a, как по мне, это простота. Вот и нам, в нашем велосипеде, надо сделать все максимально просто.

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

Mobx — отличное решение, позволяющее отслеживать изменения. Этим у нас будет заниматься Proxy. Единственное, в дальнейшем, научим Proxy работать со вложенными структурами.

Типизация. Было бы неплохо использовать эту популярную нынче тему и даже дополнить её. Сделаем в нашем супер решении типизацию и валидацию в рантайме, с учетом вложенности.

RxJS, люблю его за то, что во многих случаях он способен за меня решать сложнейшие задачи, но нам не зачем его имплементить. Постараемся не усложнять наше решение, а использовать обычные объекты JS и возможно Rx будет работать с нашим велосипедом как нужно.

React, здесь я бы хотел остановиться поподробнее. React, это переломный фреймверк. До React’a у нас был Ember и Angular 1, дающий первичное представление о компонентом подходе. Мы писали много компонент и директив, дополняя основную логику, которая была сосредоточена в контроллерах, сервисах и т.д. Но с появлением React’a, мы стали рассматривать наше приложение более декларативно, не убегая от HTML, в том же контексте и можно сказать синтаксисе.

Декларативность компонентного подхода, это то что нам нужно достичь, чтобы все было максимально прозрачно и понятно. Модные сейчас render-props дают отличную возможность не выбиваться из потока и писать логику еще более декларативно, чем HOC’и, так что и их возьмем.

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

image

Каждый из современных фреймверков хорош по-своему. Мы сейчас решаем проблему хранения состояния, по сути мы должны его использовать где угодно. В данной статье, для примера, я выбрал React, так как лучше его знаю. Но в идеале, это должно быть универсально.

Создадим компонент StateManager. Это основной компонент, хранящий все состояния приложения, как Redux. По желанию мы можем добавить валидацию (например, это решение) и типизацию (TypeScript/Flow). А также, написать по желанию функционал Middlewares, которые будут отрабатывать изменения до, после, во время (поместив их вызов в Proxy).

компонент StateManager
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
<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, в котором данные приходят асинхронно, и мы можем управлять выводом элементов

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>


image

Данным примером я хотел поделиться с вами своими мыслями о использовании Proxy и нескольких хороших практик. Надеюсь, что использование новых стандартов вдохновит Вас на создание полноценных решений, которые достойно займут свое место в нашем зоопарке JS. Всем спасибо, удачи в велосипедостроении!

Ссылки:

Исходный код
To-do list поклацать