При использовании Wix, при работе с Corvid вам не нужно иметь дело с HTML / CSS при разработке пользовательского интерфейса. Вместо этого вы получаете полноценный редактор WYSIWYG, в котором вы можете создать пользовательский интерфейс для своего приложения. Затем все, что осталось сделать — это написать логику приложения, на которой мы действительно хотим сосредоточиться при разработке приложений.
Большинство примеров, которые вы увидите сегодня, связывающих логику приложения с представлением, аналогичны приложениям jQuery старого стиля. Вы связываете обработчик событий с элементами пользовательского интерфейса, и в ответ на эти события вы запускаете некоторую логику, которая обновляет другие элементы пользовательского интерфейса с результатом.
Вам также могут понравиться:
Создание, управление, развертывание и масштабирование вашего следующего веб-проекта с помощью Corvid .
Стиль jQuery
Давайте посмотрим на пример. Скажем, у вас есть представление с текстовым элементом ( #counter
), который отображает счетчик и два элемента кнопки ( #increment
и #decrement
), которые увеличивают или уменьшают этот счетчик.
Код Corvid, который реализует эту логику, будет выглядеть примерно так:
JavaScript
1
let counter = 0;
2
$w.onReady(function() {
3
$w('#counter').text = `${counter}`;
4
$w('#increment').onClick(function() {
5
counter++;
6
$w('#counter').text = `${counter}`;
7
});
8
$w('#decrement').onClick(function() {
9
counter--;
10
$w('#counter').text = `${counter}`;
11
});
12
});
Это стиль кодирования, который используют профессиональные разработчики внешнего интерфейса, поскольку он напоминает им о старых временах jQuery, когда мы явно обновляли пользовательский интерфейс с эффектом каждого взаимодействия.
Чтобы понять, почему это плохая модель, давайте попробуем немного усложнить пример. Допустим, у нас также есть еще один текстовый элемент ( #counter2
) со своими собственными кнопками увеличения / уменьшения. На этот раз мы также воспользуемся возможностью сделать небольшой рефакторинг. Реализация логики для этого будет выглядеть примерно так:
JavaScript
xxxxxxxxxx
1
let counter = 0, counter2 = 0;
2
function renderCounter() {
3
$w('#counter').text = `${counter}`;
4
}
5
function renderCounter2() {
6
$w('#counter2').text = `${counter2}`;
7
}
8
$w.onReady(function() {
9
renderCounter();
10
renderCounter2();
11
$w('#increment').onClick(function() {
12
counter++;
13
renderCounter();
14
});
15
$w('#decrement').onClick(function() {
16
counter--;
17
renderCounter();
18
});
19
$w('#increment2').onClick(function() {
20
counter2++;
21
renderCounter2();
22
});
23
$w('#decrement2').onClick(function() {
24
counter2--;
25
renderCounter2();
26
});
27
});
Как вы можете видеть, в этом шаблоне после обновления какого-либо состояния ( counter
/ counter2
), а затем мы идем и обновляем соответствующие компоненты UI, на которые должно повлиять это изменение состояния. Так, если, например, мы добавим дополнительный текстовый элемент, вычисляющий сумму двух счетчиков, мы обновим его во всех местах:
JavaScript
xxxxxxxxxx
1
let counter = 0, counter2 = 0;
2
function renderCounter() {
3
$w('#counter').text = `${counter}`;
4
}
5
function renderCounter2() {
6
$w('#counter2').text = `${counter2}`;
7
}
8
function renderSum() {
9
$w('#sum').text = `${counter + counter2}`;
10
}
11
$w.onReady(function() {
12
renderCounter();
13
renderCounter2();
14
renderSum();
15
$w('#increment').onClick(function() {
16
counter++;
17
renderCounter();
18
renderSum();
19
});
20
$w('#decrement').onClick(function() {
21
counter--;
22
renderCounter();
23
renderSum();
24
});
25
$w('#increment2').onClick(function() {
26
counter2++;
27
renderCounter2();
28
});
29
$w('#decrement2').onClick(function() {
30
counter2--;
31
renderCounter2();
32
renderSum();
33
});
34
});
Упс! Вы видели ошибку? Я забыл позвонить , renderSum()
когда #increment2
в ы щелкнули. Ну, это то, что происходит, когда вы подключаете пользовательский интерфейс и вводите данные вручную. Это только ухудшается по мере того, как приложение усложняется, а также по мере изменения поведения приложения по мере добавления новых функций.
Например, посмотрите это приложение со списком задач, написанное на Corvid с использованием шаблона jQuery: https://shahartalmi36.wixsite.com/corvid-jquery .
У нас есть две ошибки:
- Если вы установите флажок рядом с элементом todo, который помечает его как выполненный, описание todo получает зачеркивание, что является ожидаемым поведением. Но если вы установите верхний флажок, который помечает все элементы задачи как выполненные, мы забыли обновить описание элемента задачи с зачеркиванием.
- Если вы установите флажок рядом с элементом todo, который помечает его как выполненный, счетчик «оставленные элементы» в левом нижнем углу обновляется с количеством оставшихся элементов. Но если мы удаляем незавершенный элемент todo с помощью кнопки удаления, мы забываем обновить счетчик «оставленных элементов».
Это раздражает и трудно ловить ошибки. Каждое приложение jQuery было заполнено ими, и это делало поддержание базы кода приложений jQuery полным адом.
Вы также можете играть с реальным приложением. Просто откройте пример corvid-jquery в редакторе Wix (включите режим разработки из верхнего меню, чтобы увидеть код).
И тогда пришла привязка данных
Привязка данных — это довольно аккуратная концепция, когда вам больше не нужно явно обновлять пользовательский интерфейс при изменении состояния. Вместо этого вы определяете, какая часть состояния входит в каждый элемент пользовательского интерфейса, используя некоторую платформу. Затем, когда вы обновляете состояние в этой платформе, он автоматически обновляет интерфейс с соответствующими изменениями. Поскольку фреймворк знает, какой элемент пользовательского интерфейса нуждается в каком состоянии, он может обновлять только те элементы пользовательского интерфейса, которые заботятся об обновленной части состояния.
Так что же это за рамки? На самом деле их много. Первым широко используемым фреймворком был фактически Angular, но Angular был гораздо большим, чем просто фреймворк для привязки данных или управления состоянием. Это было связано со многими другими проблемами, наиболее важными с тем, как отображается пользовательский интерфейс, что, очевидно, является проблемой для нас, поскольку мы хотим отображать пользовательский интерфейс с помощью Corvid.
Но затем произошло нечто хорошее. React вышел и поставил свой флаг только для того, чтобы высказывать свое мнение о том, как отображается пользовательский интерфейс; остальное было открыто для расширения. Вскоре появилось новое поколение структур управления состоянием, которое дало вам только метод управления вашим состоянием и требовало лишь небольших адаптеров для привязки состояния к представлению приложения на основе React. Более поздние версии Angular также позволяли пользователям легко использовать такое управление состоянием вместо встроенного управления состоянием, которое поставлялось с Angular.
Это было здорово, поскольку, по сути, вы можете написать почти всю логику своего приложения, не заботясь о том, какую инфраструктуру вы будете использовать для визуализации интерфейса. Это означает, что если бы вы использовали такую инфраструктуру управления состоянием, вы могли бы довольно легко переместить логику вашего приложения из React в Angular или даже какую-нибудь другую будущую инфраструктуру, которая может появиться (например, Corvid!), И единственное, что могло бы измениться, — это подключение состояния для пользовательского интерфейса.
В этой статье мы рассмотрим две мои любимые среды управления состояниями ( Redux и MobX ) и увидим, как вы можете легко подключить их к приложению Corvid. Это также означает, что вы можете легко взять любое приложение на основе Redux или MobX и перенести его из React в Corvid!
Чтобы использовать Redux и MobX, вам нужно установить эти внешние библиотеки и дополнительную библиотеку, называемую corvid-redux
в вашем приложении Corvid.
Redux
В Redux основная концепция заключается в том, что ваше состояние управляется редуктором и обновляется посредством диспетчеризации действий. По сути, это означает, что каждый раз, когда вы хотите обновить состояние, вы отправляете действие с необходимым обновлением. Затем редуктор (который в основном является просто функцией с двумя аргументами) вызывается с текущим состоянием, а действие и должно возвращать новое состояние.
Это звучит сложно, но это действительно просто. Давайте посмотрим на контрпример:
JavaScript
xxxxxxxxxx
1
import { createStore } from 'redux';
2
const initialState = { counter: 0 };
4
function reducer(state = initialState, action) {
6
switch (action.type) {
7
case 'INCREMENT':
8
return { counter: state.counter + 1 };
9
case 'DECREMENT':
10
return { counter: state.counter - 1 };
11
default:
12
return state;
13
}
14
}
15
const store = createStore(reducer);
17
$w.onReady(function() {
19
store.subscribe(() => $w('#counter').text = `${store.getState().counter}`);
20
$w('#increment').onClick(() => store.dispatch({ type: 'INCREMENT'}));
22
$w('#decrement').onClick(() => store.dispatch({ type: 'DECREMENT'}));
23
});
Все, что здесь изменилось, это то, что вместо того, чтобы увеличивать или уменьшать счетчик самостоятельно при нажатии кнопок, мы отправляем действие INCREMENT
или DECREMENT
. Затем Redux вызывает редуктор с текущим состоянием, а отправленное действие и редуктор возвращают новое состояние с увеличенным или уменьшенным счетчиком в зависимости от того, какое действие было обработано.
Наиболее интересная линия, на которой стоит сосредоточиться сейчас:
store.subscribe(() => $w('#counter').text = `${store.getState().counter}`);
В этом мы подписываемся на изменения в магазине, и когда состояние обновляется, Redux вызывает наш обратный вызов, и у нас будет возможность обновить представление новым состоянием.
Это хорошо, но недостаточно детально. В настоящее время у нас есть только один счетчик, но в предыдущем примере у нас было два счетчика и сумма, которая будет выглядеть примерно так:
JavaScript
xxxxxxxxxx
1
import { createStore } from 'redux';
2
const initialState = { counter: 0, counter2: 0 };
4
function reducer(state = initialState, action) {
6
switch (action.type) {
7
case 'INCREMENT':
8
return { state, counter: state.counter + 1 };
9
case 'DECREMENT':
10
return { state, counter: state.counter - 1 };
11
case 'INCREMENT2':
12
return { state, counter2: state.counter2 + 1 };
13
case 'DECREMENT2':
14
return { state, counter2: state.counter2 - 1 };
15
default:
16
return state;
17
}
18
}
19
const store = createStore(reducer);
21
$w.onReady(function() {
23
store.subscribe(() => {
24
$w('#counter').text = `${store.getState().counter}`;
25
$w('#counter2').text = `${store.getState().counter2}`;
26
$w('#sum').text = `${store.getState().counter + store.getState().counter2}`;
27
});
28
$w('#increment').onClick(() => store.dispatch({ type: 'INCREMENT'}));
30
$w('#decrement').onClick(() => store.dispatch({ type: 'DECREMENT'}));
31
$w('#increment2').onClick(() => store.dispatch({ type: 'INCREMENT2'}));
32
$w('#decrement2').onClick(() => store.dispatch({ type: 'DECREMENT2'}));
33
});
По сути, в обратном вызове подписки мы обновляем все элементы пользовательского интерфейса с новым состоянием. Это не очень эффективно, так как нет причины обновлять первый счетчик, если второй счетчик - тот, который был увеличен. Для этого мы должны добавить привязку corvid-redux , которая гарантирует это, делая привязки данных более декларативными:
JavaScript
xxxxxxxxxx
1
import { createStore } from 'redux';
2
import { createConnect } from 'corvid-redux';
3
const initialState = { counter: 0, counter2: 0 };
5
function reducer(state = initialState, action) {
7
switch (action.type) {
8
case 'INCREMENT':
9
return { state, counter: state.counter + 1 };
10
case 'DECREMENT':
11
return { state, counter: state.counter - 1 };
12
case 'INCREMENT2':
13
return { state, counter2: state.counter2 + 1 };
14
case 'DECREMENT2':
15
return { state, counter2: state.counter2 - 1 };
16
default:
17
return state;
18
}
19
}
20
const store = createStore(reducer);
22
const {connect, pageConnect} = createConnect(store);
23
pageConnect(() => {
25
connect(state => ({ text: `${state.counter}` }))($w('#counter'));
26
connect(state => ({ text: `${state.counter2}` }))($w('#counter2'));
27
connect(state => ({ text: `${state.counter + state.counter2}` }))($w('#sum'));
28
$w('#increment').onClick(() => store.dispatch({ type: 'INCREMENT'}));
30
$w('#decrement').onClick(() => store.dispatch({ type: 'DECREMENT'}));
31
$w('#increment2').onClick(() => store.dispatch({ type: 'INCREMENT2'}));
32
$w('#decrement2').onClick(() => store.dispatch({ type: 'DECREMENT2'}));
33
});
Здесь мы в основном говорим, что мы связываем text
свойство соответствующего элемента пользовательского интерфейса со значением счетчика / суммы. Теперь вместо обновления всего пользовательского интерфейса при обновлении состояния пользовательский интерфейс будет обновляться только в случае изменения связанного значения. Чтобы сделать эту работу, все, что нам нужно сделать, это использовать createConnect
метод из corvid-redux , который дает нам два помощника: это pageConnect
, по сути, небольшая оболочка поверх $w.onReady
, которую мы использовали до сих пор, и connect
которую мы используем привязать к свойствам элемента.
Довольно просто, правда? Давайте немного усложним это. Давайте составим упрощенный список задач. Теперь у нас есть текстовый ввод ( #input
) и кнопка добавления ( #add
), и у нас есть повторитель ( #repeater
), который будет отображать элементы, которые мы добавили. Для каждого элемента в повторителе у нас будет текстовый элемент, отображающий описание ( #description
) и кнопку удаления, чтобы удалить его из списка ( #remove
).
Это будет выглядеть примерно так:
JavaScript
xxxxxxxxxx
1
import { createStore } from 'redux';
2
import { createConnect } from 'corvid-redux';
3
let nextId = 0;
5
const initialState = [
7
{ _id: `${++nextId}`, description: 'Task 1' },
8
{ _id: `${++nextId}`, description: 'Task 2' },
9
{ _id: `${++nextId}`, description: 'Task 3' }
10
];
11
function reducer(state = initialState, action) {
13
switch (action.type) {
14
case 'ADD_TODO':
15
return [state, {_id: action.id, description: action.description}];
16
case 'REMOVE_TODO':
17
return state.filter(todo => todo._id !== action.id);
18
default:
19
return state;
20
}
21
}
22
const store = createStore(reducer);
24
const {connect, pageConnect, repeaterConnect} = createConnect(store);
25
pageConnect(() => {
27
connect(state => ({data: state}))($w('#repeater'));
28
$w('#add').onClick(() => {
30
store.dispatch({type: 'ADD_TODO', id: `${++nextId}`, description: $w('#input').value});
31
$w('#input').value = '';
32
});
33
repeaterConnect($w('#repeater'), ($item, _id) => {
35
connect(state => ({text: state.find(todo => todo._id === _id).description}))($item('#description'));
36
$item('#remove').onClick(() => store.dispatch({type: 'REMOVE_TODO', id: _id}));
38
});
39
});
Теперь это интересно. Здесь мы привязываем массив к data
свойству элемента ретранслятора ( #repeater
). Как видите, начальное состояние этого массива состоит из трех элементов todo; у каждого элемента есть свойство и свойство. Это уникальный идентификатор, обязательный для любого элемента повторителя, как описано в ... Вторая интересная вещь здесь - это то , что мы используем для привязки элементов внутри повторителя к нашему состоянию. _id
description
_id
repeaterConnect
repeaterConnect
получает два параметра: элемент повторителя, с которым мы хотим связать, и обратный вызов. Этот обратный вызов вызывается для каждого нового элемента в повторителе (включая, конечно, три начальных элемента), чтобы позволить ему связать внутренние элементы этого элемента повторителя с состоянием. Вам не нужно беспокоиться об отмене привязки при удалении элементов, так как corvid-redux позаботится об этом автоматически.
Как вы можете видеть, обратный вызов просто получает $item
функцию селектора, которую вы можете использовать для выбора внутренних элементов элемента, и функцию _id
, которую вы можете использовать для доступа к соответствующему элементу в состоянии. Связывание внутри repeaterConnect
выполняется так же connect
, как если бы вы выполняли связывание вне повторителя.
Обратите внимание, что, как всегда в Redux, мы должны помнить, что состояние является неизменным, что означает, что мы не можем сохранять ссылки на элемент внутри массива, так как эта ссылка никогда не будет содержать более поздних изменений состояния. Вместо этого мы должны найти правильный элемент в массиве внутри connect
обратного вызова следующим образом:
JavaScript
xxxxxxxxxx
1
connect(state => ({
2
text: state.find(todo => todo._id === _id).description
3
}))
4
($item('#description'));
Так как обычно у нас будет несколько таких вызовов подключения внутри repeaterConnect, вероятно, мы можем сделать небольшой рефакторинг и извлечь часть поиска для функции, и тогда часть подключения будет несколько короче:
const find = state => state.find(todo => todo._id === _id); connect(state =>
({ text: find(state).description}))($item('#description'))
JavaScript
xxxxxxxxxx
1
const find = state => state.find(todo => todo._id === _id);
2
connect(state => ({
3
text: find(state).description
4
}))
5
($item('#description'));
В общем, как мы узнали здесь, держать всю логику состояния вдали от кода привязки - это всегда хорошая практика, поскольку она позволяет вам в будущем заменить каркасы представления небольшими изменениями в вашем логическом коде. Я настоятельно рекомендую прочитать документы Redux о производных данных , чтобы узнать больше о том, как извлечь данные из состояния.
Это на самом деле все, что нам нужно знать, чтобы использовать Redux в Corvid. Есть много интересных вещей, которые нужно узнать о Redux, например, как добавить промежуточное ПО в ваш магазин и как обрабатывать асинхронные операции, но приятно то, что это всего лишь Redux и не зависит от используемой вами технологии представления, которая в нашем случае это просто Corvid. Вы можете прочитать все о тех, кто в документах Redux, о продвинутых темах и попробовать применить их в своем приложении Corvid.
Для живого примера более сложного списка задач откройте пример corvid-redux в редакторе Wix (включите режим разработки из верхнего меню, чтобы увидеть код).
MobX
Если честно, я не большой поклонник Redux. Образец, представленный некоторыми шаблонами, иногда слишком велик, и в целом я думаю, что для большинства случаев использования недостатки неизменяемости больше, чем преимущества. Это не значит, что я никогда не использую Redux; иногда это может быть очень полезно. Давайте рассмотрим альтернативное решение для управления состоянием, которое я в основном предпочитаю использовать - MobX.
В MobX основная концепция заключается в том, что вы привязываетесь к некоторому производному состоянию, и MobX знает, как автоматически идентифицировать ваши зависимости состояния и автоматически создавать наблюдаемое для него. Затем он снова запускает привязку, только когда состояние, от которого вы зависите, меняется. Давайте посмотрим на MobX в действии с нашими двумя счетчиками и примером суммы:
JavaScript
xxxxxxxxxx
1
import { autorun, observable } from 'mobx';
2
const state = observable({
4
counter: 0,
5
counter2: 0,
6
get sum() {
7
return this.counter + this.counter2;
8
}
9
});
10
$w.onReady(() => {
12
autorun(() => $w('#counter').text = `${state.counter}`);
13
autorun(() => $w('#counter2').text = `${state.counter2}`);
14
autorun(() => $w('#sum').text = `${state.sum}`);
15
$w('#increment').onClick(() => state.counter++);
17
$w('#decrement').onClick(() => state.counter--);
18
$w('#increment2').onClick(() => state.counter2++);
19
$w('#decrement2').onClick(() => state.counter2--);
20
});
Вот и все. Крутая вещь в MobX состоит в том, что он будет запускать autorun
методы только тогда, когда состояние, которое они используют, изменилось. Итак, когда мы обновляем counter
, вторая autorun
не запускается, а когда мы обновляем counter2
, первая autorun
не запускается. Третий autorun
будет работать в обоих случаях, так как он зависит от sum
, который зависит и от counter
и counter2
.
Обратите внимание, что вместо того, чтобы использовать эту sum
функцию получения в наблюдаемой, мы могли бы сделать что-то вроде:
JavaScript
xxxxxxxxxx
1
autorun(() => $w('#sum').text = `${state.counter+state.counter2}`);
Это сработало бы точно так же, и нам бы не понадобился добытчик. Однако, как я упоминал ранее, разумно отделить логику от представления, потому что тогда вы можете легко повторно использовать свое состояние при изменении технологии представления. Я рекомендую всегда выполнять все вычисления в наблюдаемом состоянии с использованием геттеров и оставлять вид как можно более простым.
Давайте попробуем посмотреть, как работает MobX, когда попробуем пример упрощенного списка задач. Напоминаю: у нас есть текстовый ввод ( #input
), кнопка добавления ( #add
) и ретранслятор ( #repeater
), который будет отображать элементы, которые мы добавили. Для каждого элемента в повторителе у нас будет текстовый элемент, отображающий описание ( #description
) и кнопку удаления, чтобы удалить его из списка ( #remove
).
JavaScript
xxxxxxxxxx
1
import { autorun, observable } from 'mobx';
2
let nextId = 0;
4
const state = observable({
6
tasks: [
7
{ _id: `${++nextId}`, description: 'Task 1', completed: false },
8
{ _id: `${++nextId}`, description: 'Task 2', completed: true },
9
{ _id: `${++nextId}`, description: 'Task 3', completed: false }
10
],
11
addTodo(description) {
12
this.tasks.push({_id: `${++nextId}`, description, completed: false})
13
},
14
removeTodo(id) {
15
this.tasks.remove(this.tasks.find(todo => todo._id === id));
16
}
17
});
18
$w.onReady(() => {
20
autorun(() => $w('#repeater').data = state.tasks);
21
$w('#add').onClick(() => {
23
state.addTodo($w('#input').value);
24
$w('#input').value = '';
25
});
26
const destroyers = {};
28
$w('#repeater').onItemReady(($item, {_id}) => {
29
destroyers[_id] = [
30
autorun(() => $item('#description').value = state.tasks.find(todo => todo._id === _id))
31
];
32
$item('#remove').onClick(() => state.removeTodo(_id));
34
});
35
$w('#repeater').onItemRemoved(({_id}) => destroyers[_id].forEach(f => f()));
36
});
Все, что нам нужно было сделать, как и в Redux, - это привязать массив к data
свойству элемента ретранслятора ( #repeater
). Теперь onItemReady
будет вызываться для любого нового элемента и onItemRemoved
для любого удаленного элемента. Внутри onItemReady
, мы делаем все связывание необходимо с помощью $item
переключателя.
Обратите внимание на одну особую вещь, которую очень важно понимать: возвращаемые значения из всех вызовов autorun
должны быть сохранены в виде массива на destroyers
карте, который впоследствии вызывается для каждого возвращаемого значения при удалении элемента. Причина этого заключается в том, что возвращаемое значение autorun
вызова на самом деле является методом отказа от подписки для этого конкретного autorun
. В corvid-redux это делается для нас автоматически, но в MobX мы должны вызывать эти методы отказа от подписки при удалении элемента.
Одно из мест, где MobX намного удобнее, - это когда вы хотите создать побочный эффект при привязке какого-либо значения (например, скрыть и показать элемент). Допустим, у вас есть логическое состояние, которое указывает, должен ли какой-либо элемент быть видимым или нет. Поскольку видимость в Corvid контролируется с помощью функций show / hide, в Redux это немного сложно. В MobX вы бы просто сделали:
JavaScript
xxxxxxxxxx
1
autorun(() => state.shouldShow ? $w('#element').show() : $w('#element').hide());
В то время как в Redux corvid-redux должен предоставить вам магическое видимое свойство, которое за сценой вызывает show / hide:
Для живого примера более сложного списка задач откройте пример corvid-mobx в редакторе Wix (включите режим разработки из верхнего меню, чтобы увидеть код).
JavaScript
xxxxxxxxxx
1
connect(state => ({visible: state.shouldShow}))($w('#element'));
Этот пост изначально появился на Medium .
Дальнейшее чтение
- Основы Redux .
- Как структурировать приложение MobX для реального мира .
- Создание чата на основе React с Redux, часть 2: React с Redux и привязками .