
Я не начинал писать Looking Glass Engine как самостоятельный проект; скорее меня завербовали в Сопровождение Фректал, в которой было больше всего того, что мне нравилось в госуправлении. Однако, когда Looking Glass разрабатывалась, она превзошла Freactal, и в хорошем смысле.
Вот проблемы, которые я видел в состоянии, основываясь на своем опыте работы с React, Saga и Freactal:
- React ОЧЕНЬ многословен и препятствует быстрой разработке приложений.
- Создание свойств и методов их обновления было утомительным. Не существует однострочного метода для определения значения свойства по умолчанию, имени и метода обновления, а также нет проверки типа свойств.
- Асинхронные действия требуют обширного ритуала в saga и thunk, рассеивающего поток кода, который должен быть базовым и понятным локально.
- Системы текущего состояния предполагают, что состояние представляет собой глобальную нисходящую механику.
- Системы в текущем состоянии очень сложно тестировать как единый элемент, потому что они разработаны с учетом симбиотических отношений с элементами React, а не как автономные системы.
- Локальное состояние (которое я считаю изначально плохим) — это механика толчка, затрудняющая написание подпрограмм последовательного изменения.
Итак, когда я проектировал Looking Glass Engine, у меня было несколько требований, чтобы обойти это:
- Уметь быстро определять имя свойства, значение по умолчанию и (необязательно) тип, а также неявно создавать метод «set».
- Уметь писать асинхронные действия, не полагаясь на сложные артефакты.
- Уметь использовать систему как в качестве глобально доступного хранилища, такого как коллекции Redux или Backbone, так и для тактического управления сложным состоянием в компоненте.
- Иметь возможность тестировать взаимодействие состояний и действий, не требуя компонентов DOM или представления.
В результате Looking Glass Engine определяет объекты хранилища, которые являются независимыми экземплярами класса, на которые можно подписаться BehaviorSubjects. Они присоединяются к компонентам React с помощью подписки RxJS. *
Вот пример глобального хранилища для управления пользователями

import {Store} from '@wonderlandlabs/looking-glass-engine';
import tryToLogUserIn from './user/tryToLogIn';
const userStore = new Store({
actions: {
loadUserFromLocalStorage(store) {
/** ... omitted for brevity (**) */
},
saveUserToLocalStorage(store) {
/** ... omitted for brevity ... (**) */
},
clearUserFromLocalStorage(store) {
/** ... omitted for brevity ... (**) */
},
logout(store) {
store.actions.clearUserFromLocalStorage();
store.actions.setUser(null);
},
async login(store, username, password) {
store.actions.setLoginError(false);
try {
let user = await tryToLogIn(username, password);
store.actions.setUser(user);
store.actions.saveUserToLocalStorage(user);
} catch (err) {
this.setLoginError(err);
}
}
}
})
.addProp('loginError')
.addProp('user', storedUser || null, 'object');
userStore.actions.loadUserFromLocalStorage();
export default userStore;
Чтобы использовать это хранилище в компоненте входа:
import userStore from './userStore';
import React, {Component} from 'react';
export default class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {...userStore.state, username: '', password: ''};
this.doLogin = this.doLogin.bind(this);
}
componentDidMount() {
this.sub = userStore.subscribe(({state}) => {
this.setState(state);
});
}
componentWillUnmount() {
if (this.sub) {
this.sub.unsubscribe();
}
}
doLogin() {
const {username, password} = this.state;
userStore.actions.login(username, password);
}
render() {
const {username, password, loginError} = this.state;
return (
<div className="login-form">
{loginError ? <p>{loginError.message}</p> : ''}
<div className="form-row">
<label>Username</label>
<input type="text" value={username}
onChange={({target}) => {
this.setState({username: target.value})
}}/>
</div>
<div className="form-row">
<label>Password</label>
<input type="password" value={password}
onChange={({target}) => {
this.setState({password: target.value})
}}/>
</div>
<div className="form-row">
<button onClick={this.doLogin}>Log In</button>
</div>
</div>
)
}
}
Или в пользовательской панели.
export default class UserPanel extends Component {
constructor(props) {
super(props);
this.state = {user: userStore.state.user};
this.doLogin = this.doLogin.bind(this);
}
componentDidMount() {
this.sub = userStore.subscribe(({state}) => {
this.setState({user: state.user});
});
}
componentWillUnmount() {
if (this.sub) {
this.sub.unsubscribe();
}
}
render() {
const {user} = this.state;
return (
<section className="user-panel">
{user ? <span>
{`welcome ${user.username}`}
<button onClick={() => {
userStore.actions.logout();
document.location = '/'; // or history.push('/') if react-router used
}}>Log Out</button>
</span>
: <a href="/login">Log In</a>
}
</section>
)
}
}
Действия
Действия просты в использовании и мощны в использовании.
Действия могут принимать аргументы, как и в случае с описанным выше методом входа в систему. Ваши аргументы будут переданы действию после внутреннего свойства хранилища, которое всегда будет первым. (вам не нужно проходить магазин самостоятельно.)
- Действия неразрывно связаны с контекстом хранилища.
- Если действие возвращает значение, это значение теперь является состоянием (например, действие Redux).
- Если действие не имеет возвращаемого значения, то никаких изменений не предполагается.
- Первым параметром действия является сам объект хранилища, поэтому вы можете вызывать другие действия из этого хранилища, включая свойства установки для свойств (см. ниже).
- Если вы меняете состояние внутри действия, свойство store.state обновляется синхронно.
- После каждого действия (включая вызовы установщика свойств) само хранилище передается подписчикам с текущим состоянием в качестве подсвойства.
Эти правила означают, что вы можете писать простые синхронные действия, которые изменяют несколько свойств — либо одним махом, возвращая обновленную версию состояния (стиль Redux), либо одно за другим, вызывая действия установки свойств внутри вашего действия.
И если вы хотите/нужно использовать асинхронные действия и внешние асинхронные системы, вы можете смешивать их без каких-либо специальных инструментов.
Внутри вашего действия вы всегда можете рассчитывать на актуальность свойства состояния параметров (при условии, что вы не деструктурируете его).
И если ваш магазин является глобальным (или вы предоставляете его как свойство для подпредставления), вы можете вызывать действия напрямую — вам не нужно смешивать их с реквизитами или состоянием компонентов React.
Асинхронные процессы в действиях
Действия являются синхронными, когда они могут быть, и асинхронными, когда они должны быть.
- Если действие возвращает Promise, это обещание раскручивается, и правила действий продолжаются с него асинхронно.
- Если действие возвращает функцию, эта функция вызывается, и ее результат обрабатывается по правилам действий.
- Действия возвращают обещания. это не означает, что действие является асинхронным, обязательно — действия настройки свойств и действия, которые не являются асинхронными и не возвращают промисы, выполняются напрямую. На самом деле это означает, что в таких случаях вам гарантируется, что все асинхронные действия будут развернуты и завершены после завершения действия return Promise.
В результате, если вы объявите действие асинхронным, вы сможете кодировать, используя все обычные асинхронные инструменты внутри своего действия.
Определение свойства
«Свойство» — это поле в состоянии магазина. Это утилита определения, которая одним махом добавляет начальное значение в состояние и метод установки. Определение свойства — одно из самых важных средств LGE, позволяющих сэкономить время. Хотя вы можете определить состояние как вход в конструктор (const myStore = new Store({state: {a: 1, b:2}, action: {…}), на практике определение свойств по одному быстрее и более Все это допустимые способы определения свойств:
const ShoppingCart = new Store ({
actions: {},
props: {
total: 0,
coupons: {
start: () => ([]),
type: 'array'
}
}
})
.addProp('cartState', 'shopping', 'string')
.addProp('cartError', null)
.addProp('cartId');
Эта корзина будет иметь синхронные действия setCartState(value), setCart
Если тип определен (либо как третье свойство addProp, либо как свойство типа объекта описания), то вызов set[propName]()
Другие преимущества использования LGE
Хранилища LGE чрезвычайно легко тестируются, не требуя каких-либо представлений или интеграции реакции в модульные тесты. Изменения данных можно наблюдать, подписавшись на магазин.
Магазины LGE можно объединять для разделения связанных функций.
Магазины LGE могут прослушивать друг друга — если ваш магазин является глобальным, вы можете создавать подписки на один магазин и при обнаружении определенных изменений вызывать действия в другом магазине.
Действия LGE Stores перечислены в свойстве, поэтому вам не нужно гадать, что это за действия (в отличие от Redux).
Магазины LGE не предназначены для какой-либо конкретной версии React; их можно использовать везде, где есть наблюдаемые объекты RxJS, включая Angular, серверную часть и т. д.
Действия в LGE Store можно регулировать с помощью транзакций, чтобы синхронизировать все обновления действия (в том числе в поддействиях) в одно изменение по завершении действия (или выполнить откат в случае ошибок).
Ошибки LGE Store попадают в обработчик ошибок подписки, поэтому все ошибки можно наблюдать из центрального места.
Действия LGE Stores можно добавлять после создания с помощью методов addAction() и addProp(), поэтому вы можете писать функции для внешнего изменения хранилищ.
Магазины LGE имеют необязательный поток отладчика, который предоставляет подробную информацию о каждом обновлении магазина, если вам нужно проанализировать деятельность вашего магазина.
BehaviorSubject LGE Stores отображается в свойстве .stream магазина, поэтому, если вы хотите воспользоваться преимуществами регулирования или постобработки RxJS, вы можете передать поток из .stream в любые фильтры или управляющие механизмы RxJS, которые вы хотите.
Я обнаружил, что LGE значительно повышает мою производительность и помогает мне избежать многословных джунглей управления состоянием семейства Redux и любопытных задержек состояния компонентов. Я надеюсь, что вы найдете его полезным при разработке собственных приложений.
(*) Ранее я разработал HOC для Looking Glass Engine; но, наблюдая за тем, как люди взаимодействуют с ними, я заметил, что, не участвуя в процессе привязки, они зацикливались на своем «правильном» способе присоединения состояния к компонентам, вместо того чтобы осознавать, что у них есть варианты, как объединить состояние и действия в компонент.
(**) полный код с сохранением в магазине:
const userStore = new Store({
actions: {
loadUserFromLocalStorage(store) {
let userString = localStorage.getItem('user');
try {
store.actions.setUser(JSON.parse(userString));
} catch (err) {
store.actions.clearUserFromLocalStorage();
}
},
saveUserToLocalStorage(store) {
/** ... omitted for brevity ... (**) */
const userObject = store.actions.user;
if (!userObject) {
localStorage.removeItem('user');
return;
}
try {
localStorage.setItem('user', userString);
} catch (err) {
store.actions.clearUserFromLocalStorage();
}
},
clearUserFromLocalStorage(store) {
localStorage.removeItem('user');
store.actions.setUser(null);
},
logout(store) {
store.actions.clearUserFromLocalStorage();
},
async login(store, username, password) {
store.actions.setLoginError(false);
try {
let user = await tryToLogIn(username, password);
store.actions.setUser(user);
store.actions.saveUserToLocalStorage(user);
} catch (err) {
this.setLoginError(err);
}
}
}
})
.addProp('loginError')
.addProp('user', storedUser || null, 'object');
userStore.actions.loadUserFromLocalStorage();
export default userStore;