vuex + mecanografiado = vuexok. La bicicleta que montó y adelantó a todos

Buen día.



Como muchos desarrolladores, escribo mi propio  proyecto relativamente pequeño en mi tiempo libre  . Solía ​​escribir en react, pero en el trabajo uso vue. Bueno, para bombear vue, comencé a ver mi proyecto en él. Al principio todo estaba bien, francamente optimista, hasta que decidí que todavía necesitaba mejorar en mecanografía. Así es como apareció el mecanografiado en mi proyecto. Y si todos los componentes eran  buenos , entonces vuex todo estaba triste. Así que tuve que pasar por las 5 etapas para aceptar el problema, bueno, casi todo.



Negación



Requisitos básicos para una tienda:



  1. Los tipos de mecanografiado deberían funcionar en módulos
  2. Los módulos deben ser fáciles de usar en los componentes, los tipos de estados, las acciones, las mutaciones y los captadores deben funcionar
  3. No cree una nueva api para vuex, debe asegurarse de que los tipos de mecanografiado funcionen de alguna manera con los módulos de vuex para que no tenga que volver a escribir toda la aplicación a la vez
  4. Llamar mutaciones y acciones debe ser lo más simple y directo posible
  5. El paquete debe ser lo más pequeño posible
  6. No quiero almacenar constantes con nombres de mutaciones y acciones.
  7. Debería funcionar (y qué tal sin él)


No puede ser que un proyecto tan maduro como vuex no tuviera el soporte de mecanografiado normal. Bueno, abrimos Google  Yandex y manejamos  . Estaba 100500% seguro de que todo debería estar bien con el mecanografiado (qué equivocado estaba). Hay muchos intentos diferentes de hacer amigos vuex y mecanografiado. Daré algunos ejemplos que recuerdo, sin el código para no hinchar el artículo. Todo está en la documentación de los enlaces a continuación.



vuex-smart-module



github.com/ktsn/vuex-smart-module

Bueno, muy bueno. Todo conmigo, pero personalmente no me gustó el hecho de que para acciones, mutaciones, estados, captadores, es necesario crear clases separadas. Esto, por supuesto, es gusto, pero este soy yo y mi proyecto) Y, en general, el problema de la mecanografía no se ha resuelto por completo ( hilo de comentarios con una explicación del por qué ).



Soporte Vuex Typescript



Buen intento, pero mucho reescritura de algo, y generalmente no es aceptado por la comunidad.



vuex-module-decorators



Esta parecía la manera perfecta de hacer amigos vuex y mecanografiado. Parece el decorador de propiedades vue que uso en desarrollo, puedes trabajar con el módulo como con una clase, en general, super, pero ...



Pero no hay herencia. Las clases de módulo no se heredan correctamente y el problema ha estado pendiente de este problema durante mucho tiempo. Y sin herencia, habrá mucha duplicación de código. Tortita…



Enfado



Entonces no fue mucho, bueno o ± lo mismo: no hay una solución ideal. Este es el momento en el que te dices a ti mismo: ¿Por qué comencé a escribir un proyecto en vue? Bueno, ya sabes reaccionar, bueno, escribiría sobre reaccionar, ¡no habría tales problemas! En el trabajo principal, el proyecto está en vue y necesita actualizarlo: presione el argumento. ¿Vale la pena los nervios gastados y las noches de insomnio? Siéntese como todos los demás, escriba komponentiki, no, ¡es lo que más necesita! ¡Lanza este vue! ¡Escriba para reaccionar, actualice en él y pague más por él!



En ese momento, estaba dispuesto a odiar vue como ningún otro, pero era emoción, y la inteligencia aún estaba por encima de eso. Vue tiene (en mi opinión subjetiva) muchas ventajas sobre reaccionar, pero no hay perfección, así como ganadores en el campo de batalla. Tanto vue como react son buenos a su manera, y dado que una parte importante del proyecto ya está escrito en vue, sería lo más tonto posible cambiar para reaccionar ahora. Tuve que decidir qué hacer con vuex.



Negociar



Bueno, las cosas no van bien. ¿Quizás entonces vuex-smart-module? Este paquete parece estar bien, sí, tienes que crear muchas clases, pero funciona muy bien. ¿O tal vez podría intentar escribir tipos para mutaciones y acciones a mano en componentes y usar vuex puro? Allí, vue3 con vuex4 está en camino, tal vez lo estén haciendo mejor con mecanografiado. Probemos con vuex puro. En general, esto no afecta el trabajo del proyecto, sigue funcionando, no hay tipos, pero aguanta. Y espera)



Al principio comencé a hacer esto, pero el código resulta ser monstruoso ...



Depresión



Tenía que seguir adelante. Pero donde se desconoce. Fue un paso completamente desesperado. Decidí hacer un  contenedor estatal desde cero . El código se redactó en un par de horas. Y resultó incluso bien. Los tipos funcionan, el estado es reactivo e incluso la herencia está ahí. Pero pronto la agonía de la desesperación comenzó a remitir y el sentido común comenzó a regresar. Con todo, esta idea se fue a la basura. En general, este fue el patrón de bus de eventos global. Y solo es bueno para aplicaciones pequeñas. Y, en general, escribir tu propio vuex sigue siendo bastante exagerado (al menos en mi situación). Entonces ya supuse que estaba completamente exhausto. Pero era demasiado tarde para retirarse.



Pero si alguien está interesado, entonces el código está aquí: (Probablemente en vano agregó este fragmento, pero la ruta será)



no parecer nervioso
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]
  )
}




Adopción El  nacimiento de la moto que ha superado a todos. vuexok



Para los impacientes, el código está aquí , la breve documentación está aquí .



Al final, escribí una pequeña biblioteca que cubre toda la lista de deseos e incluso un poco más de lo que se requería de ella. Pero lo primero es lo primero.



El módulo vuexok más simple se ve así:



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


Bueno, algo así como vuex, aunque ... ¿qué hay en la línea 10?



counterModule.mutations.plus(1)


¡Guau! Es legal Bueno, con vuexok, sí, legalmente) El método createModule devuelve un objeto que repite exactamente la estructura del objeto del módulo vuex, solo que sin la propiedad de espacio de nombres, y podemos usarlo para llamar mutaciones y acciones o para obtener estados y captadores, todos los tipos se conservan. Y desde cualquier lugar donde se pueda importar.



¿Y los componentes?



Y con ellos todo está bien, ya que en realidad esto es vuex, entonces, en principio, nada ha cambiado, commit, dispatch, mapState, etc. funciona como antes.



Pero ahora puede hacer que los tipos del módulo funcionen en los 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
  }
}


La propiedad de estado en un módulo es reactiva como en store.state, por lo que para usar el estado del módulo en los componentes de Vue, solo necesita devolver una parte del estado del módulo en una propiedad calculada. Solo hay una salvedad. Deliberadamente hice que el estado Readonly fuera un tipo, no es bueno cambiar el estado de vuex así.



Llamar a acciones y mutaciones es fácil de deshonrar y los tipos de parámetros de entrada también se guardan



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


Aquí hay tal belleza. Un poco más tarde, también tuve que reaccionar al cambio en jwt, que también se almacena en la tienda. Y luego apareció el método de reloj en módulos. Los observadores de módulos funcionan de la misma manera que store.watch. La única diferencia es que el estado y los captadores del módulo se pasan como parámetros de la función captadora.



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


Entonces, lo que tenemos:



  1. lado escrito - sí
  2. tipos funcionan en componentes - sí
  3. api como vuex y todo lo que estaba antes en vuex puro no se rompe, es
  4. trabajo declarativo con el lado - si
  5. tamaño de paquete pequeño (~ 400 bytes gzip) - sí
  6. no es necesario almacenar los nombres de las acciones y mutaciones en constantes - hay
  7. debería funcionar - es


En general, es extraño que una característica tan maravillosa no esté disponible en vuex lista para usar, ¡es increíble lo conveniente que es!

En cuanto al soporte para vuex4 y vue3, no lo he probado, pero a juzgar por los documentos, debería ser compatible.



Los problemas presentados en estos artículos también se resuelven:



Vuex - resolviendo una vieja disputa con nuevos métodos

Vuex rompe la encapsulación



Sueños húmedos:



Sería bueno hacerlo de modo que las mutaciones y otras acciones estén disponibles en el contexto de las acciones.



Cómo hacer esto en el contexto de tipos mecanografiados: el idiota lo sabe. Pero si pudieras hacer esto:



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


Que mi alegría no tuviera límites. Pero la vida, como la escritura mecanografiada, es dura.



Aquí está la aventura con vuex y mecanografiado. Bueno, como que hablé. Gracias por la atención.



All Articles