vuex + typescript = vuexok. A bicicleta que montou e ultrapassou todos

Dia bom.



Como muitos desenvolvedores, escrevo meu próprio  projeto relativamente pequeno em meu tempo livre  . Eu costumava escrever em reação, mas no trabalho uso vue. Bem, a fim de bombear em vue, comecei a ver meu projeto nisso. No começo tudo estava bem, totalmente rosado, até que decidi que ainda precisava melhorar a datilografia. É assim que o texto datilografado apareceu em meu projeto. E se todos os componentes fossem  bons , então vuex tudo era triste. Então eu tive que passar por todos os 5 estágios de aceitação do problema, bem, quase tudo.



Negação



Requisitos básicos para uma loja:



  1. Tipos de texto datilografado devem funcionar em módulos
  2. Módulos devem ser fáceis de usar em componentes, tipos de estados, ações, mutações e getters devem funcionar
  3. Não crie uma nova api para vuex, você precisa se certificar de que os tipos de texto digitados de alguma forma funcionam com módulos vuex para que você não tenha que reescrever todo o aplicativo de uma vez
  4. Chamar mutações e ações deve ser o mais simples e direto possível
  5. O pacote deve ser o menor possível
  6. Não quero armazenar constantes com nomes de mutações e ações
  7. Deve funcionar (e sem ele)


Não pode ser que um projeto tão maduro como o vuex não tivesse suporte normal de digitação. Bem, abrimos o  Google  Yandex e dirigimos. Eu estava 100500% certo de que tudo deveria estar bem com o texto datilografado (como eu estava errado). Existem muitas tentativas diferentes de fazer amigos vuex e datilografar. Vou dar alguns exemplos de que me lembro, sem o código para não estourar o artigo. Tudo está na documentação dos links abaixo.



vuex-smart-module



github.com/ktsn/vuex-smart-module

Bom, muito bom. Tudo comigo, mas pessoalmente não gostei do fato de que para ações, mutações, estados, getters, você precisa criar classes separadas. Isso, é claro, é bom gosto, mas este sou eu e meu projeto) E, em geral, o problema de digitação não foi totalmente resolvido ( tópico de comentários com uma explicação do porquê ).



Suporte para Vuex Typescript



Boa tentativa, mas muita reescrita e geralmente não aceita pela comunidade.



vuex-module-decorators



Esta parecia ser a maneira perfeita de fazer amigos vuex e datilografados. Parece que o vue-property-decorator que uso no desenvolvimento, você pode trabalhar com o módulo como com uma classe, em geral, super, mas ...



Mas não há herança. As classes de módulo não são herdadas corretamente e o problema está pendurado há muito tempo! E sem herança, haverá muita duplicação de código. Panqueca…



Raiva



Então não era muito, bom, ou ± o mesmo - não há solução ideal. Este é o momento exato em que você diz a si mesmo: Por que comecei a escrever um projeto em vue? Bem, você sabe reagir, bem, eu escreveria sobre reagir, não haveria tais problemas! No trabalho principal, o projeto está em voga e você precisa atualizá-lo - acerte o argumento. Vale a pena passar os nervos e as noites sem dormir? Sente-se como todo mundo, escreva komponentiki, não, você precisa acima de tudo! Jogue essa vue! Escreva para reagir, atualize-o e pague mais por isso!



Naquele momento, eu estava pronto para odiar vue como nenhum outro, mas era emoção, e inteligência ainda estava acima disso. Vue tem (na minha opinião subjetiva) muitas vantagens sobre reagir, mas não há perfeição, assim como vencedores no campo de batalha. Tanto a visão quanto a reação são boas em sua própria maneira, e como uma parte significativa do projeto já está escrita em vue, seria o mais tolo possível mudar para reagir agora. Eu tive que decidir o que fazer com vuex.



Pechincha



Bem, as coisas não estão indo bem. Talvez então vuex-smart-module? Este pacote parece bom, sim, você tem que criar muitas classes, mas funciona muito bem. Ou talvez ele pudesse tentar escrever tipos para mutações e ações manualmente em componentes e usar vuex puro? Lá, vue3 com vuex4 está a caminho, talvez eles estejam se saindo melhor com o texto datilografado. Então, vamos tentar o vuex puro. No geral isso não atrapalha o trabalho do projeto, ainda funciona, não tem tipos, mas você aguenta. E espere)



No começo eu comecei a fazer isso, mas o código acabou sendo monstruoso ...



Depressão



Eu tive que seguir em frente. Mas onde é desconhecido. Foi um passo completamente desesperado. Decidi fazer um  contêiner de estado do zero . O código foi elaborado em algumas horas. E acabou bem. Os tipos funcionam, o estado é reativo e até a herança existe. Mas logo a agonia do desespero começou a diminuir e o bom senso começou a voltar. Em suma, essa ideia foi para a lata de lixo. Em geral, esse era o padrão de barramento de evento global. E só é bom para pequenas aplicações. E, em geral, escrever seu próprio vuex ainda é um exagero (pelo menos na minha situação). Aí já adivinhei que estava completamente exausto. Mas era tarde demais para recuar.



Mas se alguém estiver interessado, então o código está aqui: (Provavelmente em vão adicionou este fragmento, mas o caminho será)



não parecer nervoso
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]
  )
}




Adoção O  nascimento da bicicleta que conquistou a todos. vuexok



Para os impacientes, o código está aqui , a documentação resumida está aqui .



No final, escrevi uma pequena biblioteca que cobre toda a lista de desejos e até um pouco mais do que o necessário. Mas as primeiras coisas primeiro.



O módulo vuexok mais simples se parece com este:



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
    },
  },
})


Bem, meio como vuex ... o que está na linha 10?



counterModule.mutations.plus(1)


Uau! Isso é legal? Bem, com vuexok - sim, legalmente) O método createModule retorna um objeto que repete exatamente a estrutura do objeto do módulo vuex, apenas sem a propriedade com namespace, e podemos usá-lo para chamar mutações e ações ou para obter estados e getters, todos os tipos são preservados. E de qualquer lugar onde possa ser importado.



E quanto aos componentes?



E com eles está tudo bem, pois na verdade é vuex, então, em princípio, nada mudou, commit, dispatch, mapState, etc. trabalhe como antes.



Mas agora você pode fazer os tipos do módulo funcionarem nos componentes:



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
  }
}


A propriedade state em um módulo é reativa assim como em store.state, então para usar o estado do módulo nos componentes Vue, você só precisa retornar uma parte do estado do módulo em uma propriedade computada. Existe apenas uma advertência. Eu deliberadamente fiz o estado Readonly um tipo, não é bom mudar o estado vuex assim.



Ações de chamada e mutações são simples de desgraçar e os tipos de parâmetros de entrada também são salvos



 private async doSomething() {
   counterModule.mutations.setNumber(10)
   //   this.$store.commit('counterModule/setNumber', 10)
   await counterModule.actions.increment()
   //   await this.$store.dispatch('counterModule/increment')
 }


Aqui está uma beleza. Um pouco mais tarde, também precisei reagir à mudança no jwt, que também está armazenado na loja. E então o método do relógio apareceu em módulos. Os observadores de módulo funcionam da mesma maneira que store.watch. A única diferença é que o estado e os getters do módulo são passados ​​como parâmetros da função getter.



const unwatch = jwtModule.watch(
  (state) => state.jwt,
  (jwt) => console.log(`New token: ${jwt}`),
  { immediate: true },
)


Então, o que temos:



  1. lado digitado - sim
  2. tipos funcionam em componentes - sim
  3. api como em vuex e tudo o que estava antes em vuex puro não quebra - é
  4. trabalho declarativo com o lado - sim
  5. tamanho de pacote pequeno (~ 400 bytes gzip) - sim
  6. não há necessidade de armazenar os nomes das ações e mutações em constantes - há
  7. deve funcionar - é


Em geral, é estranho que um recurso tão maravilhoso não esteja disponível no vuex fora da caixa, é incrível como ele é conveniente!

Quanto ao suporte para vuex4 e vue3 - não testei, mas a julgar pelos documentos deve ser compatível. Resolvem-



se também os problemas apresentados nestes artigos:



Vuex - resolvendo uma velha disputa com novos métodos

Vuex quebra o encapsulamento



Sonhos molhados:



Seria ótimo ter mutações e outras ações disponíveis no contexto das ações.



Como fazer isso no contexto de tipos de texto datilografado - o idiota sabe disso. Mas se você pudesse fazer isso:



{
  actions: {
    one(injectee) {
       injectee.actions.two()
    },
    two() {
      console.log('tada!')
    }
}


Que minha alegria não teria limite. Mas a vida, como o texto datilografado, é uma coisa dura.



Aqui está a aventura com vuex e datilografado. Bem, eu meio que falei. Obrigado pela atenção.



All Articles