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

в 19:52, , рубрики: javascript, React, reactive programming, ReactJS, redux, state management, Программирование, Разработка веб-сайтов

Всем йо, читатели.

В общем, так вышло, что я пишу на JavaScript уже довольно долго, и одной из самых главных задач всегда была организация состояния приложения.
Что-то хочется кешировать, что-то обновлять, причем обновлять везде, а не только в локальном компоненте, не хочется перерисовывать весь компонент если поменялся весь 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

Автор: Юрий Степин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js