Есть много преимуществ для централизации состояния вашего приложения в магазине Vuex. Одним из преимуществ является то, что все транзакции записываются. Это позволяет использовать удобные функции, такие как отладка во времени, где вы можете переключаться между предыдущими состояниями, чтобы изолировать проблемы.
В этой статье я покажу, как создать функцию отмены / возврата с помощью Vuex , которая работает аналогично отладке во время путешествия. Эта функция может использоваться в различных сценариях от сложных форм до игр на основе браузера.
Вы можете проверить готовый код здесь, на GitHub , и попробовать демо в этом Codepen . Я также создал плагин как модуль NPM под названием vuex-undo-redo, если вы хотите использовать его в проекте.
Настройка плагина
Чтобы сделать эту функцию многократно используемой, мы создадим ее как плагин Vue. Эта функция требует от нас добавления некоторых методов и данных в экземпляр Vue, поэтому мы структурируем плагин как миксин.
plugin.js
JavaScript
1
module.exports = {
2
install(Vue) {
3
Vue.mixin({
4
// Code goes here
5
});
6
}
7
};
Чтобы использовать его в проекте, мы можем просто импортировать плагин и установить его:
app.js
JavaScript
xxxxxxxxxx
1
import VuexUndoRedo from './plugin.js';
2
Vue.use(VuexUndoRedo);
Вам также могут понравиться:
Компоненты и как они взаимодействуют в Vue и Vuex .
концепция
Функция будет работать, откатив последнюю мутацию, если пользователь хочет отменить, а затем повторно применить ее, если он хочет повторить. Как мы это осуществим?
Подход № 1
Первый возможный подход состоит в том, чтобы «снимать» состояние хранилища после каждой мутации и помещать снимок в массив. Чтобы отменить / повторить, мы можем получить правильный снимок и заменить его состоянием хранилища.
Проблема с этим подходом состоит в том, что состояние магазина является объектом JavaScript. Когда вы помещаете объект JavaScript в массив, вы просто нажимаете ссылку на объект. Наивная реализация, как и следующая, не будет работать:
JavaScript
xxxxxxxxxx
1
var state = { };
2
var snapshot = [];
3
// Push the first state
5
snapshot.push(state);
6
// Push the second state
8
state.val = "new val";
9
snapshot.push(state);
10
// Both snapshots are simply a reference to state
12
console.log(snapshot[0] === snapshot[1]); // true
Подход моментального снимка потребует, чтобы вы сначала сделали клон состояния перед нажатием. Учитывая, что состояние Vue становится реактивным благодаря автоматическому добавлению функций получения и установки, оно не очень хорошо работает с клонированием.
Подход № 2
Другой возможный подход заключается в регистрации каждой зафиксированной мутации. Чтобы отменить, мы сбрасываем хранилище в его начальное состояние, а затем снова запускаем мутации; все кроме последнего. Повторение аналогичная концепция.
Учитывая принципы Flux, повторный запуск мутаций из одного и того же начального состояния должен идеально воссоздать состояние. Поскольку это более чистый подход, чем первый, давайте продолжим.
Ведение мутаций
Vuex предлагает метод API для подписки на мутации, который мы можем использовать для их регистрации. Мы установим это на created
крючке. В обратном вызове мы просто помещаем мутацию в массив, который позже можно будет повторно запустить.
plugin.js
JavaScript
xxxxxxxxxx
1
Vue.mixin({
2
data() {
3
return {
4
done: []
5
}
6
},
7
created() {
8
this.$store.subscribe(mutation => {
9
this.done.push(mutation);
10
}
11
}
12
});
Отменить метод
Чтобы отменить мутацию, мы очистим хранилище, а затем повторно запустим все мутации, кроме последней. Вот как работает код:
- Используйте
pop
метод массива, чтобы удалить последнюю мутацию. - Очистите состояние магазина с помощью специальной мутации
EMPTY_STATE
(объясняется ниже). - Повторяйте каждую оставшуюся мутацию, фиксируя ее снова в новом магазине. Обратите внимание, что метод подписки все еще активен во время этого процесса, то есть каждая мутация будет добавляться повторно. Удалите его снова немедленно,
pop
чтобы предотвратить это.
JavaScript
xxxxxxxxxx
1
const EMPTY_STATE = 'emptyState';
2
Vue.mixin({
4
data() { },
5
created() { },
6
methods() {
7
undo() {
8
this.done.pop();
9
this.$store.commit(EMPTY_STATE);
10
this.done.forEach(mutation => {
11
this.$store.commit(`${mutation.type}`, mutation.payload);
12
this.done.pop();
13
});
14
}
15
}
16
});
Очистка магазина
Всякий раз, когда этот плагин используется, разработчик должен реализовать мутацию в своем хранилище под названием emptyState
. Задача состоит в том, чтобы вернуть магазин в исходное состояние, чтобы он был готов к восстановлению с нуля.
Разработчик должен сделать это самостоятельно, потому что плагин, который мы создаем, не имеет доступа к магазину, только к экземпляру Vue. Вот пример реализации:
store.js
JavaScript
xxxxxxxxxx
1
new Vuex.Store({
2
state: {
3
myVal: null
4
},
5
mutations: {
6
emptyState() {
7
this.replaceState({ myval: null });
8
}
9
}
10
});
Возвращаясь к нашему плагину, emptyState
мутация не должна быть добавлена в наш done
список, так как мы не хотим повторно фиксировать это в процессе отмены. Предотвратите это с помощью следующей логики:
plugin.js
JavaScript
xxxxxxxxxx
1
Vue.mixin({
2
data() { },
3
created() {
4
this.$store.subscribe(mutation => {
5
if (mutation.type !== EMPTY_STATE) {
6
this.done.push(mutation);
7
}
8
});
9
},
10
methods() { }
11
});
Повторить метод
Давайте создадим новое свойство данных, undone
которое будет массивом. Когда мы удаляем последнюю мутацию done
во время процесса отмены, мы помещаем ее в этот массив:
plugin.js
JavaScript
xxxxxxxxxx
1
Vue.mixin({
2
data() {
3
return {
4
done: [],
5
undone: []
6
}
7
},
8
methods: {
9
undo() {
10
this.undone.push(this.done.pop());
11
12
}
13
}
14
});
Теперь мы можем создать redo
метод, который будет просто брать последнюю добавленную мутацию undone
и повторно фиксировать ее.
plugins.js
JavaScript
xxxxxxxxxx
1
methods: {
2
undo() { },
3
redo() {
4
let commit = this.undone.pop();
5
this.$store.commit(`${commit.type}`, commit.payload);
6
}
7
}
Повторить недействительность
Если пользователь инициирует отмену один или несколько раз, а затем делает новый новый коммит, содержимое undone
будет признано недействительным. Если это произойдет, мы должны опустошить undone
.
Мы можем обнаружить новые коммиты из нашего обратного вызова подписки при добавлении коммита. Однако логика хитрая, так как обратный вызов не имеет никакого очевидного способа узнать, что такое новый коммит и что такое отмена / повтор.
Самый простой подход - установить флаг newMutation
в экземпляре. Это будет верно по умолчанию, но методы отмены и повтора временно установят для него значение false. Если при фиксации мутации установлено значение true, subscribe
обратный вызов очистит undone
массив.
plugin.js
JavaScript
xxxxxxxxxx
1
module.exports = {
2
install(Vue) {
3
Vue.mixin({
4
data() {
5
return {
6
done: [],
7
undone: [],
8
newMutation: true
9
};
10
},
11
created() {
12
this.$store.subscribe(mutation => {
13
if (mutation.type !== EMPTY_STATE) {
14
this.done.push(mutation);
15
}
16
if (this.newMutation) {
17
this.undone = [];
18
}
19
});
20
},
21
methods: {
22
redo() {
23
let commit = this.undone.pop();
24
this.newMutation = false;
25
this.$store.commit(`${commit.type}`, commit.payload);
26
this.newMutation = true;
27
},
28
undo() {
29
this.undone.push(this.done.pop());
30
this.newMutation = false;
31
this.$store.commit(EMPTY_STATE);
32
this.done.forEach(mutation => {
33
this.$store.commit(`${mutation.type}`, mutation.payload);
34
this.done.pop();
35
});
36
this.newMutation = true;
37
}
38
}
39
});
40
},
41
}
Основной функционал теперь завершен! Добавьте плагин в свой собственный проект или в мою демонстрацию, чтобы протестировать его.
Публичный API
В моей демонстрации вы заметите, что кнопки отмены и возврата отключены, если их функциональность в настоящее время не разрешена. Например, если еще не было коммитов, вы, очевидно, не можете отменить или повторить. Разработчик, использующий этот плагин, может захотеть реализовать подобную функциональность.
Чтобы разрешить это, плагин может предоставлять два вычисляемых свойства canUndo
и canRedo
как часть публичного API. Это тривиально для реализации:
plugin.js
JavaScript
xxxxxxxxxx
1
module.exports = {
2
install(Vue) {
3
Vue.mixin({
4
data() { },
5
created() { },
6
methods: { },
7
computed: {},
8
computed: {
9
canRedo() {
10
return this.undone.length;
11
},
12
canUndo() {
13
return this.done.length;
14
}
15
},
16
});
17
},
18
}