С выходом Composition API в Vue появилось новые возможности повторного использования кода. Больше нет необходимости в миксинах, компонентах высшего порядка и прочих “хаках”, если вам нужно вынести общую логику для нескольких компонентов. Но что если у вас есть нереактивный сервис, инкапсулирующий бизнес-логику, а переписывать все на composition api не хочется? 

К примеру возьмем простой класс с состоянием:

export class MyService {
  foo: Object = {}

  setFoo (foo: Object) {
    this.foo = foo
  }
}

В нем все прекрасно, но что если мы хотим сделать его свойство реактивным? Согласно документации, мы можем сделать, например, вот так: 

import { Ref, ref } from 'vue'

export class MyService {
  foo: Ref<Object> = ref({})

  setFoo (foo: Object) {
    this.foo.value = foo
  }
}

Вроде бы тоже неплохо, но если ваш сервис большой и иерархичный, переводить каждое свойство в ref будет трудно и больно. Тут на помощь приходят декораторы Typescript:

export class MyService {
  @Reactive foo: Object = {}

  setFoo (foo: Object) {
    this.foo = foo
  }
}

Успех! Наше свойство реактивно, но теперь уже без манипуляций c ref-ами. Как это работает? Рассмотрим сам декоратор:

import { shallowRef } from 'vue'

function initRefs (target: any, key: string, value ?: any) {
  target.__refs = target.__refs ?? {}

  if (!target.__refs[key]) {
    target.__refs[key] = shallowRef(value)
  }
}

export function Reactive (target: any, key: string): void {
  Object.defineProperty(target, key, {
    configurable: true,
    enumerable: true,
    get () {
      initRefs(this, key)
      return this.__refs[key].value
    },
    set (value) {
      initRefs(this, value, key)
      this.__refs[key].value = value
    }
  })
}

Благодаря Object.defineProperty, свойство попросту заменяется геттером и сеттером, обращающимся к ref-у, который хранится в том же классе. Его инициализация происходит при первом чтении либо записи.

Заключение


Декораторы - мощный инструмент, позволяющий вынести низкоуровневый код на уровень инфраструктуры и не засорять им приложение. Но какие минусы у такого подхода?

  1. Декораторы все еще являются экспериментальной функцией, и ее реализация в будущем может измениться, о чем говорится в документации. Кроме того, для их работы необходимо явно проставить параметр experimentalDecorators.

  2. Этот подход похож на магию и прячет нюансы реализации от разработчика, что может привести к неожиданным багам и непредсказуемому поведению.

  3. Это не работает с SSR из коробки, хотя это преодолимая проблема, например, с помощью Nuxt и его ssrRef

Тем не менее, лично мне такой путь нравится, он красив и элегантен и не накладывает драконовских ограничений. А как вы считаете? Комментарии и предложения к подходу охотно принимаются.

Комментарии (5)


  1. Djaler
    30.06.2022 20:12

    А какой, собственно, профит от того, что теперь у нас это поле стало рефом (причём неявно)?


    1. popov-a-e Автор
      30.06.2022 20:45

      ну как же
      использовать в компонентах, очевидно)
      сервис можно использовать делая provide/inject локально или глобально, через плагины, например
      я довольно часто использую этот паттерн, не все же хранить в одном сторе и компонентах


      1. Djaler
        30.06.2022 21:08

        А какая причина хранить глобальное состояние не в сторе?


  1. ilyapirogov
    30.06.2022 21:17
    +4

    1. Осторожнее с декораторами, они не зря имеют экспериментальный статус. Основная проблема с ними в том, что если вы смешиваете декораторы и наследование, то можете получить крайне занимательные баги в будущем.

    target.__refs = target.__refs ?? {}

    Не очень хорошо влезать в структуру чужого объекта. Я бы предложил использовать WeakMap вместо этого:

    const metaRefs = new WeakMap();
    // ...
    function initRefs (target: any, key: string, value ?: any) {
      if (!metaRefs.has(target)) {
        metaRefs.set(target, shallowRef(value));
      }
    }
    
    1. Вместо декораторов можно использовать более стабильный и универсальный способ - Proxy. Кроме того, он будет работать и без TypeScript.


  1. LbISS
    03.07.2022 12:54

    Мне одному Composition Api кажется порочной технологией?

    Всю жизнь программисты борятся со сложностью кода. Реактивное программирование уже даёт определённый скачок сложности, по сравнению, скажем, с процедурным, т.к. много вещей происходит "неявно", обеспечивается фреймворком, и это надо держать в голове и учитывать. Чтобы бороться с этой сложностью люди придумал всякие Flux паттерны, натянули мввм на компоненты, выстраивают жёсткую архитектуру и задают ограничения контекстов - используя иерархическую структуру компонентов и модулей, микрофронты, гексагоналку и т.п.

    И тут вылезает технология, которая позволяет связать реактивно что угодно с чем угодно. И ты вот такой меняешь одно поле в классе и у тебя может поменяться в приложении всё что угодно и где угодно, причём это неявный вызов, отследить это возможностей крайне мало. Т.е. очень легко неправильно заиспользовать и вероятность получить тяжело поддерживаемый код с высокой связностью крайне высока. Так надо ли оно?

    Обычно когда возникают вопросы с тем, что "сложно реиспользовать код" - это вопрос к тому, что кто-то либо наговнокодил, либо просто накопился тех.долг и текущие компоненты и модули надо либо делить на несколько, либо объединять в общий, и после рефакторинга оказывается что никакие такие дополнительные горизонтальные связи не нужны.