Использование многоразовых миксинов Vue в Typescript

внешний интерфейс переводчик TypeScript Vue.js

После перехода на написание Vue-приложений с помощью Typescript, после раунда крещения тулчейнов и зависимостей, я смог идти медленно, но есть очень часто используемая функция mixin, и кажется, что официального решения нет.

Вы хотите не только насладиться гибкостью и удобством примесей, но также хотите воспользоваться гарантией безопасности, предоставляемой системой типов ts, и удобством использования IntelliSense во время разработки.

В официальной организации vuejs есть «vue-class-component» и рекомендованный «vue-property-decorator», которые не реализованы соответствующим образом. Просматривая предыдущий выпуск, нужно сделать фичу, которая давно висит, а именно поддержку миксина.

Это не сложно, просто напишите сами.

Сообщение: vue-class-component 6.2.0 доступенmixinsМетод аналогичен реализации идеи этой статьи.

выполнить

import Vue, { VueConstructor } from 'vue'

export type VClass<T> = {
  new(): T
} & Pick<VueConstructor, keyof VueConstructor>

/**
 * mixins for class style vue component
 */
function Mixins<A>(c: VClass<A>): VClass<A>
function Mixins<A, B>(c: VClass<A>, c1: VClass<B>): VClass<A&B>
function Mixins<A, B, C>(c: VClass<A>, c1: VClass<B>, c2: VClass<C>): VClass<A&B&C>
function Mixins<T>(c: VClass<T>, ...traits: Array<VClass<T>>): VClass<T> {
  return c.extend({
    mixins: traits
  })
}

утверждениеVClass<T>Доступен как конструктор класса для T . в то же время черезPickПолучите статические методы (extend/mixin и т. д.) в конструкторе Vue, чтобы можно было поддерживать реальную реализацию в следующем абзаце, вызвав конструктор подкласса Vue.extendметод для создания нового конструктора подкласса.

function Mixins<T>(c: VClass<T>, ...traits: Array<VClass<T>>): VClass<T> {
  return c.extend({
    mixins: traits
  })
}

Что касается ABC, то это чисто ручной труд по объявлениям типов.

использовать

При фактическом использовании:

import { Component, Vue } from 'vue-property-decorator'
import { Mixins } from '../../util/mixins'

@Component
class PageMixin extends Vue {
  title = 'Test Page'

  redirectTo(path: string) {
    console.log('calling reidrectTo', path)
    this.$router.push({ path })
  }
}

interface IDisposable {
  dispose(...args: any[]): any
}

@Component
class DisposableMixin extends Vue {
  _disposables: IDisposable[]

  created() {
    console.log('disposable mixin created');
    this._disposables = []
  }

  beforeDestroy() {
    console.log('about to clear disposables')
    this._disposables.map((d) => {
      d.dispose()
    })
    delete this._disposables
  }

  registerDisposable(d: IDisposable) {
    this._disposables.push(d)
  }
}

@Component({
  template: `
  <div>
    <h1>{{ title }}</h1>
    <p>Counted: {{ counter }}</p>
  </div>
  `
})
export default class TimerPage extends Mixins(PageMixin, DisposableMixin) {
  counter = 0

  mounted() {
    const timer = setInterval(() => {
      if (this.counter++ >= 3) {
        return this.redirectTo('/otherpage')
      }
      console.log('count to', this.counter);
    }, 1000)

    this.registerDisposable({
      dispose() {
        clearInterval(timer)
      }
    })
  }
}

count to 1
count to 2
count to 3
calling reidrectTo /otherpage
about to clear disposables

заметьте прямоextends VueизDisposableMixinне является допустимым компонентом Vue, и его нельзя использовать непосредственно вmixinsвариант, если он будет использоваться сVue.extendспособ расширить использование пользовательских компонентов, не забудьте использоватьComponentОдин слой упаковки.

const ExtendedComponent = Vue.extend({
  name: 'ExtendedComponent',
  mixins: [Component(DisposableMixin)],
})

Abstract class

В бизнес-системе в большинстве случаев требования будут более сложными и предусматривать некоторые базовые функции, но некоторые части нужно оставить наследникам для самостоятельной реализации, в этом случае уместно использовать абстрактные классы.

прямое наследование

abstract class AbstractMusicPlayer extends Vue {
  abstract audioSrc: string
  
  playing = false
  
  togglePlay() {
    this.playing = !this.playing
  }
}

@Component
class MusicPlayerA extends AbstractMusicPlayer {
  audioSrc = '/audio-a.mp3'
}

@Component
class MusicPlayerB extends AbstractMusicPlayer {
  staticBase = '/statics'

  get audioSrc() {
    return `${this.staticBase}/audio-b.mp3`
  }
}

Используйте миксины

Плохой способ: читерство и аннотации

Но абстрактные классы не могут быть созданы и не удовлетворены{ new(): T }Следовательно, это требование может передаваться только по наследству, ине можемСмешанный, по той же причине абстрактные классы не могут быть смешаны с помощью «vue-class-component».ComponentФункциональное украшение.

В это время мы должны написать реализованные функции в Mixin, поместить функции, которые должны быть реализованы в интерфейсе, и позволить реализовать их конкретным классам.


interface IMusicSourceProvider {
  audioSrc: string
}

/**
 * 需要实现 IPlayerImplementation
 */
@Component
class PlayerMixin extends Vue {
  /** @abstract 必须实现 */
  audioSrc: string

  logSrc() {
    console.log(this.audioSrc)
  }
}

interface IPlayerImplementation extends IMusicSourceProvider {}

@Component
class OtherMixin extends Vue {
  description = '另一个 Mixin'
}

@Component
class RealPlayer extends Mixins(PlayerMixin, OtherMixin) implements IPlayerImplementation {
  audioSrc = '/audio-c.mp3'
}

// 无法正常工作
@Component
class BrokenPlayer extends Mixins(PlayerMixin, OtherMixin) {
}

ввиду@ComponentТо, как реализован декоратор, этот способ обмана компилятора на самом деле довольно неуклюж.

Если конкретный класс наследуетPlayerMixin, но не реализован с использованием геттера или инициализатора свойстваaudioSrcЭтот атрибут, компилятор не может сообщить вам об этой ошибке (без включения строгого режима), но в реальном использованииaudioSrcНа самом деле он не инициализирован, вы найдетеBrokenPlayerв случае_dataне содержитaudioSrc, даже если значение устанавливается вручную после создания экземпляра, Vue не может отслеживать изменение значения, что вызовет некоторые скрытые ошибки.

Нам остается только аккуратно прописывать комментарии в коде, надеясь, что пользователи этого не забудут.

Вы также можете выполнить некоторые дополнительные проверки во время разработки, а именно:

Пользовательский декоратор AbstractProperty

vue-class-component предоставляетcreateDecoratorметод создания собственного декоратора под свою систему, мы можем использовать его так:

import { createDecorator } from 'vue-class-component'

// 一个什么都不做的装饰器,在 production 环境下启用。不使用 createDecorator
const HolderDecorator = (ctor: any) => ctor

/**
 * Only for vue-class-component decorated class
 */
export const AbstractProperty = isProduction ? HolderDecorator:
createDecorator((options, key) => {
  const originCreated = options.created
  options.created = function () {
    if (originCreated) originCreated.apply(this, arguments)
    if (!(key in this)) {
      console.error(`未实现 AbstractProperty '${key}'`)
    }
  }
})


@Component
class PlayerMixin extends Vue {
  @AbstractProperty
  audioSrc: string

  logSrc() {
    console.log(this.audioSrc)
  }
}

@Component
class BrokenPlayer extends Mixins(PlayerMixin, OtherMixin) {
}
const player = new BrokenPlayer
// 未实现 AbstractProperty 'audioSrc'

Не такой уж плохой подход: промежуточные классы

@Component
class _PlayerImpl extends AbstractMusicPlayer {
  audioSrc = '/audio-d.mp3'
}

@Component
export class RealPlayer2 extends Mixins(_PlayerImpl, OtherMixin) {
}

использовать промежуточные классы_PlayerImplДля реализации абстрактной части абстрактного класса, а затем для использования реальным пользователемRealPlayer2использовать. Немного многословно, но безопасно.