[VueJS, TypeScript] vuex + typescript = vuexok. Велосипед, который поехал и обогнал всех
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Доброго времени суток.
Как и многие разработчики, я в свободное от работы время пишу свой относительно небольшой проект. Раньше писал на react, а на работе используется vue. Ну и что бы прокачаться во vue начал пилить свой проект на нем. Сначала всё было хорошо, прямо-таки радужно, пока я не решил, что надо бы еще прокачаться и в typescript. Так в моем проекте появился typescript. И если с компонентами всё было неплохо, то с vuex всё оказалось печально. Так мне пришлось пройти все 5 стадий принятия проблемы, ну почти все.
Отрицание
Основные требования для стора:
- В модулях должны работать типы typescript
- Модули должно быть легко использовать в компонентах, должны работать типы для стейта, экшенов, мутаций и геттеров
- Не придумывать новое api для vuex, надо сделать так, чтобы как-то типы typescript заработали с модулями vuex, чтобы не приходилось разом переписывать всё приложение
- Вызов мутаций и экшенов должен быть максимально простым и понятным
- Пакет должен быть как можно меньше
- Не хочу хранить константы с именами мутаций и экшенов
- Оно должно работать (А как же без этого)
Не может быть что у такого уже зрелого проекта как vuex не было нормальной поддержки typescript. Ну-с, открываем Google Yandex и погнали. Я был уверен на 100500% что с typescript всё должно быть отлично (как же я ошибался). Есть куча разных попыток подружить vuex и typescript. Приведу несколько примеров, которые запомнились, без кода чтобы не раздувать статью. Всё есть в документации по ссылкам ниже.
vuex-smart-module
github.com/ktsn/vuex-smart-module
Добротно, даже очень. Всё при себе, но лично мне не понравилось то, что для экшенов, мутаций, стейта, геттеров надо создавать отдельные классы. Это, конечно, вкусовщина, но это я и мой проект) И в целом вопрос типизации решен не до конца (ветка комментариев с объяснением почему).
Vuex Typescript Support
Хорошая попытка, но что-то много переписывать, да и вообще не принялось сообществом.
vuex-module-decorators
Казалось, что это идеальный способ подружить vuex и typescript. Похоже на vue-property-decorator, который я использую в разработке, работать с модулем можно как с классом, в общем супер, но…
Но наследования нет. Классы модулей не корректно наследуются и issue на эту проблему висят уже очень давно! А без наследования будет очень много дублирования кода. Блин…
Гнев
Дальше было совсем уже не очень, ну или ± так же — идеального решения нет. Это тот самый момент, когда говоришь себе: Ну зачем я начал писать проект на vue? Ну ты же знаешь react, ну писал бы на react, там бы таких проблем не было! На основной работе проект на vue и тебе надо в нем прокачаться — зашибись аргумент. А оно стоит потраченных нервов и бессонных ночей? Сиди как все, пиши компонентики, нет, тебе больше всех надо! Бросай этот vue! Пиши на react, прокачивайся в нем, за него и платят больше!
В тот момент я был готов хейтить vue как никто другой, но это были эмоции, и интеллект всё же был выше этого. Vue имеет (на мой субъективный взгляд) много преимуществ над react, но совершенства не бывает, как и победителей на поле сражений. И vue, и react по-своему хороши, а так как уже значительная часть проекта написана на vue, то было бы максимально глупо сейчас переходить на react. Надо было решить, что же делать с vuex.
Торг
Ну что же, дела обстоят не очень хорошо. Может тогда vuex-smart-module? Этот пакет вроде хорош, да, надо создавать много классов, но работает отлично же. Или может попробовать прописывать типы для мутаций и экшенов руками в компонентах и использовать чистый vuex? Там и vue3 c vuex4 на подходе, может у них дела с typescript обстоят лучше. Так что давай попробуем чистый vuex. И вообще на работу проекта это не влияет, всё же работает, типов нет, но вы держитесь. И держимся же)
Сначала так и начал делать, но код получается монструозный…
Депрессия
Надо было двигаться дальше. Но куда — неизвестно. Это был совсем отчаянный шаг. Решил сделать контейнер состояния с нуля. Код был набросан за пару часов. И получилось даже хорошо. Типы работают, состояние реактивно и даже наследование есть. Но вскоре агония отчаяния стала отступать, а здравый смысл — возвращаться. В общем, эта идея отправилась на свалку. По большому счету это был паттерн глобальной шины событий. А он хорош только для не больших приложений. И вообще писать свой vuex — всё же совсем перебор (по крайней мере в моей ситуации). Тут я уже догадывался, что совсем загнался. Но отступать было уже поздно.
Но если кому интересно, то код тут: (Наверное зря добавил этот фрагмент, но путь будет)
Слабонервным не смотреть
SPL
const getModule = <T>(name:string, module:T) => {
const $$state = {}
const computed: Record<string, () => any> = {}
Object.keys(module).forEach(key => {
const descriptor = Object.getOwnPropertyDescriptor(
module,
key,
);
if (!descriptor) {
return
}
if (descriptor.get) {
const get = descriptor.get
computed[key] = () => {
return get.call(module)
}
} else if (typeof descriptor.value === 'function') {
// @ts-ignore
module[key] = module[key].bind(module)
} else {
// @ts-ignore
$$state[key] = module[key]
}
})
const _vm = new Vue({
data: {
$$state,
},
computed
})
Object.keys(computed).forEach((computedName) => {
var propDescription = Object.getOwnPropertyDescriptor(_vm, computedName);
if (!propDescription) {
throw new Error()
}
propDescription.enumerable = true
Object.defineProperty(module, computedName, {
get() { return _vm[computedName as keyof typeof _vm]},
// @ts-ignore
set(val) { _vm[computedName] = val}
})
})
Object.keys($$state).forEach(name => {
var propDescription = Object.getOwnPropertyDescriptor($$state,name);
if (!propDescription) {
throw new Error()
}
Object.defineProperty(module, name, propDescription)
})
return module
}
function createModule<
S extends {[key:string]: any},
M,
P extends Chain<M, S>
>(state:S, name:string, payload:P) {
Object.getOwnPropertyNames(payload).forEach(function(prop) {
const descriptor = Object.getOwnPropertyDescriptor(payload, prop)
if (!descriptor) {
throw new Error()
}
Object.defineProperty(
state,
prop,
descriptor,
);
});
const module = state as S & P
return {
module,
getModule() {
return getModule(name, module)
},
extends<E>(payload:Chain<E, typeof module>) {
return createModule(module, name, payload)
}
}
}
export default function SimpleStore<T>(name:string, payload:T) {
return createModule({}, name, payload)
}
type NonUndefined<A> = A extends undefined ? never : A;
type Chain<T extends {[key:string]: any}, THIS extends {[key:string]: any}> = {
[K in keyof T]: (
NonUndefined<T[K]> extends Function
? (this:THIS & T, ...p:Parameters<T[K]>) => ReturnType<T[K]>
: T[K]
)
}
Принятие Рождение велосипеда который обогнал всех. vuexok
Для нетерпеливых код тут, краткая документация тут.
В конце концов, написал крохотную библиотечку, которая закрывает все хотелки и даже чуть-чуть больше чем от нее требовалось. Но обо всём по порядку.
Простейший модуль с vuexok выглядит так:
import { createModule } from 'vuexok'
import store from '@/store'
export const counterModule = createModule(store, 'counterModule', {
state: {
count: 0,
},
actions: {
async increment() {
counterModule.mutations.plus(1)
},
},
mutations: {
plus(state, payload:number) {
state.count += payload
},
setNumber(state, payload:number) {
state.count = payload
},
},
getters: {
x2(state) {
return state.count * 2
},
},
})
Ну вроде почти как vuex, хотя… что там на 10й строке?
counterModule.mutations.plus(1)
Воу! А это легально? Ну с vuexok — да, легально) Метод createModule возвращает объект, который в точности повторяет структуру объекта модуля vuex, только без свойства namespaced, и мы можем использовать его для вызова мутаций и экшенов или для получения стейта и геттеров, причем все типы сохраняются. Причем из любого места, где его можно импортировать.
А что там с компонентами?
А с ними все отлично, так как фактически это vuex, то в принципе ничего не поменялось, commit, dispatch, mapState и т.д. работают как и раньше.
Но теперь можно сделать так, чтобы типы из модуля работали в компонентах:
import Vue from 'vue'
import { counterModule } from '@/store/modules/counterModule'
import Component from 'vue-class-component'
@Component({
template: '<div>{{ count }}</div>'
})
export default class MyComponent extends Vue {
private get count() {
return counterModule.state.count // type number
}
}
Свойство state в модуле реактивно, как и в store.state, так что чтобы использовать состояние модуля в компонентах Vue достаточно просто вернуть часть состояния модуля в вычисляемом свойстве. Есть только одна оговорка. Я намеренно сделал стейт Readonly типом, не хорошо так стейт vuex изменять.
Вызов экшенов и мутаций прост до безобразия и тоже сохраняются типы входных параметров
private async doSomething() {
counterModule.mutations.setNumber(10)
// Аналогично вызову this.$store.commit('counterModule/setNumber', 10)
await counterModule.actions.increment()
// Аналогично вызову await this.$store.dispatch('counterModule/increment')
}
Вот такая красота получилась. Чуть позже понадобилось еще реагировать на изменение jwt, который тоже хранится в сторе. И тогда появился у модулей метод watch. Вотчеры модулей работают также как store.watch. Единственная разница заключается в том, что в качестве параметров функции-гетера передаются стейт и гетеры модуля
const unwatch = jwtModule.watch(
(state) => state.jwt,
(jwt) => console.log(`New token: ${jwt}`),
{ immediate: true },
)
Итак, что мы имеем:
- типизированный стор — есть
- типы работают в компонентах — есть
- апи как у vuex и всё что было до этого на чистом vuex не ломается — есть
- декларативная работа со стором — есть
- маленький размер пакета (~400 байт gzip) — есть
- не иметь необходимости хранить в константах названия экшенов и мутаций — есть
- оно должно работать — есть
Вообще странно что такой прекрасной фичи нет во vuex из коробки, это же офигеть как удобно!
Что касается поддержки vuex4 и vue3 — не проверял, но судя по докам должно быть совместимо.
Так же решены проблемы представленные в этих статьях:
Vuex – решаем старый спор новыми методами
Vuex нарушает инкапсуляцию
Влажные мечты:
Было бы здорово сделать так что бы в контексте экшенов были доступны мутации и другие экшены.
Как это сделать в контексте типов typescript — фиг его знает. Но если бы можно было делать так:
{
actions: {
one(injectee) {
injectee.actions.two()
},
two() {
console.log('tada!')
}
}
То радости моей не было бы предела. Но жизнь, впрочем как и typescript, суровая штука.
Вот такое приключение с vuex и typescript. Ну, вроде выговорился. Спасибо за внимание.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, JavaScript, Программирование, VueJS] Vue 3 на Typescript
- [JavaScript, VueJS] Организация типовых модулей во Vuex
- [Habr, ReactJS, TypeScript] Мама, я сделал Хабр
- [Программирование, Функциональное программирование, TypeScript] Функциональное программирование на TypeScript: полиморфизм родов высших порядков
- [JavaScript, Angular, ReactJS, VueJS, TypeScript] Создание микросервисной архитектуры с использованием single-spa (миграция существующего проекта)
- [Разработка веб-сайтов, JavaScript, Проектирование и рефакторинг, Системы сборки] Как привести проект в чувство
- [Разработка веб-сайтов, JavaScript, Angular, TypeScript] Давайте сделаем переиспользуемый компонент tree view в Angular
- [JavaScript, Разработка игр, TypeScript, Логические игры] DagazServer: Как всё устроено
- [CSS, JavaScript, Работа с векторной графикой, VueJS] Как реализовать динамическую диаграмму для Vue на основе SVG
- [JavaScript, Программирование, Java, TypeScript] TypeScript для бэкенд-разработки (перевод)
Теги для поиска: #_vuejs, #_typescript, #_frontend, #_frontend, #_frontend_razrabotka (front-end разработка), #_frontendrazrabotka (frontend-разработка), #_vue, #_vuejs, #_vue2, #_vuex, #_vuextypescript, #_typescript, #_vuejs, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:33
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Доброго времени суток. Как и многие разработчики, я в свободное от работы время пишу свой относительно небольшой проект. Раньше писал на react, а на работе используется vue. Ну и что бы прокачаться во vue начал пилить свой проект на нем. Сначала всё было хорошо, прямо-таки радужно, пока я не решил, что надо бы еще прокачаться и в typescript. Так в моем проекте появился typescript. И если с компонентами всё было неплохо, то с vuex всё оказалось печально. Так мне пришлось пройти все 5 стадий принятия проблемы, ну почти все. Отрицание Основные требования для стора:
Не может быть что у такого уже зрелого проекта как vuex не было нормальной поддержки typescript. Ну-с, открываем Google Yandex и погнали. Я был уверен на 100500% что с typescript всё должно быть отлично (как же я ошибался). Есть куча разных попыток подружить vuex и typescript. Приведу несколько примеров, которые запомнились, без кода чтобы не раздувать статью. Всё есть в документации по ссылкам ниже. vuex-smart-module github.com/ktsn/vuex-smart-module Добротно, даже очень. Всё при себе, но лично мне не понравилось то, что для экшенов, мутаций, стейта, геттеров надо создавать отдельные классы. Это, конечно, вкусовщина, но это я и мой проект) И в целом вопрос типизации решен не до конца (ветка комментариев с объяснением почему). Vuex Typescript Support Хорошая попытка, но что-то много переписывать, да и вообще не принялось сообществом. vuex-module-decorators Казалось, что это идеальный способ подружить vuex и typescript. Похоже на vue-property-decorator, который я использую в разработке, работать с модулем можно как с классом, в общем супер, но… Но наследования нет. Классы модулей не корректно наследуются и issue на эту проблему висят уже очень давно! А без наследования будет очень много дублирования кода. Блин… Гнев Дальше было совсем уже не очень, ну или ± так же — идеального решения нет. Это тот самый момент, когда говоришь себе: Ну зачем я начал писать проект на vue? Ну ты же знаешь react, ну писал бы на react, там бы таких проблем не было! На основной работе проект на vue и тебе надо в нем прокачаться — зашибись аргумент. А оно стоит потраченных нервов и бессонных ночей? Сиди как все, пиши компонентики, нет, тебе больше всех надо! Бросай этот vue! Пиши на react, прокачивайся в нем, за него и платят больше! В тот момент я был готов хейтить vue как никто другой, но это были эмоции, и интеллект всё же был выше этого. Vue имеет (на мой субъективный взгляд) много преимуществ над react, но совершенства не бывает, как и победителей на поле сражений. И vue, и react по-своему хороши, а так как уже значительная часть проекта написана на vue, то было бы максимально глупо сейчас переходить на react. Надо было решить, что же делать с vuex. Торг Ну что же, дела обстоят не очень хорошо. Может тогда vuex-smart-module? Этот пакет вроде хорош, да, надо создавать много классов, но работает отлично же. Или может попробовать прописывать типы для мутаций и экшенов руками в компонентах и использовать чистый vuex? Там и vue3 c vuex4 на подходе, может у них дела с typescript обстоят лучше. Так что давай попробуем чистый vuex. И вообще на работу проекта это не влияет, всё же работает, типов нет, но вы держитесь. И держимся же) Сначала так и начал делать, но код получается монструозный… Депрессия Надо было двигаться дальше. Но куда — неизвестно. Это был совсем отчаянный шаг. Решил сделать контейнер состояния с нуля. Код был набросан за пару часов. И получилось даже хорошо. Типы работают, состояние реактивно и даже наследование есть. Но вскоре агония отчаяния стала отступать, а здравый смысл — возвращаться. В общем, эта идея отправилась на свалку. По большому счету это был паттерн глобальной шины событий. А он хорош только для не больших приложений. И вообще писать свой vuex — всё же совсем перебор (по крайней мере в моей ситуации). Тут я уже догадывался, что совсем загнался. Но отступать было уже поздно. Но если кому интересно, то код тут: (Наверное зря добавил этот фрагмент, но путь будет) Слабонервным не смотретьSPLconst getModule = <T>(name:string, module:T) => {
const $$state = {} const computed: Record<string, () => any> = {} Object.keys(module).forEach(key => { const descriptor = Object.getOwnPropertyDescriptor( module, key, ); if (!descriptor) { return } if (descriptor.get) { const get = descriptor.get computed[key] = () => { return get.call(module) } } else if (typeof descriptor.value === 'function') { // @ts-ignore module[key] = module[key].bind(module) } else { // @ts-ignore $$state[key] = module[key] } }) const _vm = new Vue({ data: { $$state, }, computed }) Object.keys(computed).forEach((computedName) => { var propDescription = Object.getOwnPropertyDescriptor(_vm, computedName); if (!propDescription) { throw new Error() } propDescription.enumerable = true Object.defineProperty(module, computedName, { get() { return _vm[computedName as keyof typeof _vm]}, // @ts-ignore set(val) { _vm[computedName] = val} }) }) Object.keys($$state).forEach(name => { var propDescription = Object.getOwnPropertyDescriptor($$state,name); if (!propDescription) { throw new Error() } Object.defineProperty(module, name, propDescription) }) return module } function createModule< S extends {[key:string]: any}, M, P extends Chain<M, S> >(state:S, name:string, payload:P) { Object.getOwnPropertyNames(payload).forEach(function(prop) { const descriptor = Object.getOwnPropertyDescriptor(payload, prop) if (!descriptor) { throw new Error() } Object.defineProperty( state, prop, descriptor, ); }); const module = state as S & P return { module, getModule() { return getModule(name, module) }, extends<E>(payload:Chain<E, typeof module>) { return createModule(module, name, payload) } } } export default function SimpleStore<T>(name:string, payload:T) { return createModule({}, name, payload) } type NonUndefined<A> = A extends undefined ? never : A; type Chain<T extends {[key:string]: any}, THIS extends {[key:string]: any}> = { [K in keyof T]: ( NonUndefined<T[K]> extends Function ? (this:THIS & T, ...p:Parameters<T[K]>) => ReturnType<T[K]> : T[K] ) } Принятие Рождение велосипеда который обогнал всех. vuexok Для нетерпеливых код тут, краткая документация тут. В конце концов, написал крохотную библиотечку, которая закрывает все хотелки и даже чуть-чуть больше чем от нее требовалось. Но обо всём по порядку. Простейший модуль с vuexok выглядит так: import { createModule } from 'vuexok'
import store from '@/store' export const counterModule = createModule(store, 'counterModule', { state: { count: 0, }, actions: { async increment() { counterModule.mutations.plus(1) }, }, mutations: { plus(state, payload:number) { state.count += payload }, setNumber(state, payload:number) { state.count = payload }, }, getters: { x2(state) { return state.count * 2 }, }, }) Ну вроде почти как vuex, хотя… что там на 10й строке? counterModule.mutations.plus(1)
Воу! А это легально? Ну с vuexok — да, легально) Метод createModule возвращает объект, который в точности повторяет структуру объекта модуля vuex, только без свойства namespaced, и мы можем использовать его для вызова мутаций и экшенов или для получения стейта и геттеров, причем все типы сохраняются. Причем из любого места, где его можно импортировать. А что там с компонентами? А с ними все отлично, так как фактически это vuex, то в принципе ничего не поменялось, commit, dispatch, mapState и т.д. работают как и раньше. Но теперь можно сделать так, чтобы типы из модуля работали в компонентах: import Vue from 'vue'
import { counterModule } from '@/store/modules/counterModule' import Component from 'vue-class-component' @Component({ template: '<div>{{ count }}</div>' }) export default class MyComponent extends Vue { private get count() { return counterModule.state.count // type number } } Свойство state в модуле реактивно, как и в store.state, так что чтобы использовать состояние модуля в компонентах Vue достаточно просто вернуть часть состояния модуля в вычисляемом свойстве. Есть только одна оговорка. Я намеренно сделал стейт Readonly типом, не хорошо так стейт vuex изменять. Вызов экшенов и мутаций прост до безобразия и тоже сохраняются типы входных параметров private async doSomething() {
counterModule.mutations.setNumber(10) // Аналогично вызову this.$store.commit('counterModule/setNumber', 10) await counterModule.actions.increment() // Аналогично вызову await this.$store.dispatch('counterModule/increment') } Вот такая красота получилась. Чуть позже понадобилось еще реагировать на изменение jwt, который тоже хранится в сторе. И тогда появился у модулей метод watch. Вотчеры модулей работают также как store.watch. Единственная разница заключается в том, что в качестве параметров функции-гетера передаются стейт и гетеры модуля const unwatch = jwtModule.watch(
(state) => state.jwt, (jwt) => console.log(`New token: ${jwt}`), { immediate: true }, ) Итак, что мы имеем:
Вообще странно что такой прекрасной фичи нет во vuex из коробки, это же офигеть как удобно! Что касается поддержки vuex4 и vue3 — не проверял, но судя по докам должно быть совместимо. Так же решены проблемы представленные в этих статьях: Vuex – решаем старый спор новыми методами Vuex нарушает инкапсуляцию Влажные мечты: Было бы здорово сделать так что бы в контексте экшенов были доступны мутации и другие экшены. Как это сделать в контексте типов typescript — фиг его знает. Но если бы можно было делать так: {
actions: { one(injectee) { injectee.actions.two() }, two() { console.log('tada!') } } То радости моей не было бы предела. Но жизнь, впрочем как и typescript, суровая штука. Вот такое приключение с vuex и typescript. Ну, вроде выговорился. Спасибо за внимание. =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:33
Часовой пояс: UTC + 5