Pull to refresh

Vibe.js — попытка сделать state management без боли

Reading time4 min
Views11K

Всем йо, хабражители.


В общем, так вышло, что я пишу на JavaScript уже довольно долго, и одной из самых главных задач всегда была организация состояния приложения.
TL; DR;
Нет ничего привычнее на свете,
Чем писать колесо на велосипеда


Что-то хочется кешировать, что-то обновлять, причем обновлять везде, а не только в локальном компоненте, не хочется перерисовывать весь компонент если поменялся весь Store (shout out to Vuex), а хочется подписываться на то, что используешь (shout out to MobX).


В Redux мне очень не нравились несколько аспектов:


1) Слишком много boilerplate кода. Конечно, есть много способов и подходов сделать мутации приятнее для программистов, но тем не менее, все равно эта часть перегружена имхо.


2) Разрозненность сущностей. В свое время, когда я писал мобильные приложения на ReactNative, мы работали с JSON API сервером, то есть он возвращал ответ в формате json api спецификации, включая сущности и отношения этих сущностей. Мне спецификация очень понравилась, хоть сначала я не вдуплил. И сразу пример проблемы: у нас список диалогов, мы зашли в диалог — там пользователь онлайн. Вернулись в список диалогов — пользователь оффлайн. Думаю, знакомо юзерам ВК.


В Vuex мне в принципе, все нравится, но там не решена проблема разрозненности сущностей и есть нюансы.


В чем идея Vibe.js


Когда я писал proof of concept, я отталкивался от следующих идей:


1) Я хочу, чтобы сущность была в одном месте. Как в базе данных: 1 id = 1 сущность.
2) Я хочу, чтобы я мог подписываться только на нужные сущности
3) В то же время я хочу комбинировать различные сущности и атрибуты, чтобы не воротить каждый раз кучу подписок на нужные сущности.
4) Я хочу иметь возможность напрямую реактивно обновлять состояние — entity.name = "Vasiliy", но в то же время иметь возможность делать мутации с payload и как-то дебажить мутации, как минимум, например, добавляя к ним текстовый message.


Что получилось


Сейчас в Vibe.js есть следующие концепты:


Model, EntitySubject


Класс, который позволяет определить модель сущности.
Пример использования:


const User = new Model('User', {
    structure: {
        name: types.Attribute,
        bestFriend: types.Reference('User'),
        additionalInfo: {
            age: types.Attribute
        }
    },
    computed: {
        bestFriendsName(){
            return this.bestFriend && this.bestFriend.name || "No best friend :C"
        }
    },
    mutations: {
        setName(newName){
            this.mutate({
                name: newName
            }, "User.setName")
        }
    }
});

Конструктор позволяет описать структуру сущности, вычисляемые значения, а так же мутации.
Структура может быть описана с помощью Атрибутов, Ссылок или вложенных объектов.


Замечу, что имя пользователя можно изменить и напрямую: someUser.name = "New name",
но мутации — более стандартизированный подход.


Сам экземпляр модели практически ничего не может — он только хранит структуры из конструктора.


Если мы хотим добавить сущность:


User.insertEntity(1, {
    name: "Yura",
    bestFriend: 1, // sad when the best friend of yourself is you
    additionalInfo: {
        age: 17
    }
});

Если какие-то значения не будут указаны, будет использоваться дефолтный null. Чтобы теперь пользоваться этой сущностью, вызовем метод observe.


const entity = User.observe(1);
const user = entity.interface;
console.log(user.name) // -> "Yura"

Есть нюанс, да? Слишком много чего нужно написать, чтобы работать с сущностью. По строчкам.


1) entity = Экземпляр EntitySubject. Он подписывается на изменения сущности и обновляет interface. На него так же можно подписаться.
2) interface = Реактивный интерфейс для работы с сущностью. У него доступны значения состояния сущности, computed значения и мутации. Нужно заметить, что если сущность еще не существует в EntityStore, то entity.interface будет `null.


EntityStore


Это, как понятно из названия, хранилище сущностей. В нем хранятся все состояния, все observable, модели и содержит методы, которыми пользуются Model или Subject.


const User = new Model('User', {
    structure: {
        name: types.Attribute,
        bestFriend: types.Reference('User'),
        additionalInfo: {
            age: types.Attribute
        }
    },
    computed: {
        bestFriendsName(){
            return this.bestFriend && this.bestFriend.name || "No best friend :C"
        }
    },
    mutations: {
        setName(newName){
            this.mutate({
                name: newName
            }, "User.setName")
        }
    }
});
const Store = new EntityStore([User]);

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


Directory, DirectorySubject


Директории похожи на сущности — синглтоны. У них не индентификаторов, они статичны. Также их не нужно указывать при инициализации EntityStore, потому что сущности не могут на них подписаться. По сути директории — это "каталоги", которые имеют какое-то локальное состояние в виде атрибутов и ссылки на сущности.


К примеру, если мы смотрим книги в интернет магазине, то такая директория могла быть использована:


const Store = new EntityStore([Book]);

const BooksList = new Directory('BooksList', {
    structure: {
        page: types.Attribute,
        searchWord: types.Attribute,
        fetchedBooks: types.Array(types.Reference(Book.name))
    }
}, Store);

Директории так же поддерживают computed значения и мутации, и на них так же можно подписаться.


А что насчет подписок?


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


Ну и все это протестировано


Или не все. Я люблю писать тесты и хочу написать их побольше, потому что мне кажется, что их недостаточно. В том плане, они не охватывают все, что может пойти не так.


Ссылки


Github репозиторий библиотеки


NPM модуль


Репозиторий с примером Todo list на react


Github pages с генерированной документацией ESDOC

Only registered users can participate in poll. Log in, please.
Какой библиотекой state management'a Вы пользуетесь?
41.11% Redux74
39.44% Vuex71
11.11% MobX20
0.56% Relay1
0% Alt JS0
1.11% Другой Flux-like solution2
6.67% Еще что-то другое, напишу какое12
180 users voted. 64 users abstained.
Tags:
Hubs:
+14
Comments32

Articles

Change theme settings