After moving to Typescript to write Vue applications, I’ve stumbled through a lot of toolchains and dependencies, but there’s one popular feature, Mixin, that doesn’t seem to have an official solution yet.
We want to enjoy the flexibility and convenience of Mixin, but also the security of TS’s type system and the smooth experience of using IntelliSense when developing.
Vuejs official organization has a ‘vue-class-component’ and a recommended ‘vue-property-decorator’ that is not implemented. Check the issue of the former, there is a pending feature that has been suspended for some time that is supported by mixin.
It’s not that complicated. Write your own.
Postscript: Vue-class-Component 6.2.0 starts to provide mixins methods, similar to the implementation of this article.
implementation
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 })}Copy the code
Declare VClass
as a class constructor for T. At the same time, static methods (extend/mixin, etc.) on the Vue constructor are picked up by Pick to support the real implementation in the following section, which generates a new subclass constructor by calling the extend method on a Vue subclass constructor.
function Mixins<T> (c: VClass<T>, ... traits:Array<VClass<T>>) :VClass<T> {
return c.extend({
mixins: traits
})
}
Copy the code
As for ABC, this is purely manual work for type declarations.
use
In actual use:
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 })
}
}
interfaceIDisposable { 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: `
{{ title }}
Counted: {{ counter }}
`
})
export default class TimerPage extends Mixins(PageMixin, DisposableMixin) {
counter = 0
mounted() {
const timer = setInterval((a)= > {
if (this.counter++ >= 3) {
return this.redirectTo('/otherpage')}console.log('count to'.this.counter);
}, 1000)
this.registerDisposable({
dispose() {
clearInterval(timer)
}
})
}
}
Copy the code
count to 1
count to 2
count to 3
calling reidrectTo /otherpage
about to clear disposables
Copy the code
Note that direct extends Vue DisposableMixin is not a valid Vue component, nor can it be used directly in the Mixins option, if it is to be used by custom components that have been extended by Vue.extend, Remember to wrap a layer with Component.
const ExtendedComponent = Vue.extend({
name: 'ExtendedComponent',
mixins: [Component(DisposableMixin)],
})
Copy the code
Abstract class
In most cases, in business systems, where the requirements are more complex and provide some basic functionality, but some parts need to be left to the inheritors themselves, using abstract classes is appropriate.
Direct inheritance
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`}}Copy the code
Use Mixins
Bad: cheating, and comments
However, abstract classes cannot be instantiated and do not satisfy the {new(): T} requirement, so they can only be inherited, not mixed in. For the same reason, abstract classes cannot be decorated by the Component function of ‘vue-class-component’.
In this case, implemented functions are written into mixins, and implemented functions are put into interfaces and implemented by concrete classes.
interface IMusicSourceProvider { audioSrc: String} /** * IPlayerImplementation */ @Component class PlayerMixin extends Vue { string logSrc() { console.log(this.audioSrc) } } interface IPlayerImplementation extends IMusicSourceProvider {} @Component class OtherMixin extends Vue {description = 'another Mixin'} @Component class RealPlayer extends Mixins(PlayerMixin, OtherMixin) implements IPlayerImplementation {audioSrc = '/audio-c.mp3'} // Does not work properly @Component class BrokenPlayer extends Mixins(PlayerMixin, OtherMixin) { }Copy the code
Given the way the @Component decorator is implemented, this is a poor way to fool the compiler.
If a concrete class inherits PlayerMixin and does not implement the audioSrc property with getter or Property Initializer, the compiler cannot tell you about this error (without strict mode enabled). In practice, the audioSrc is not initialized. As you can see from a BrokenPlayer instance (BrokenPlayer), the _data value does not contain the audioSrc. Vue cannot detect this value even if it is set manually after instantiation, causing some hidden bugs.
We can only comment carefully in the code and hope that the user doesn’t forget about it.
You can also perform a few additional development-time checks as follows:
Custom decorator AbstractProperty
Vue-class-component provides the createDecorator method to create a custom decorator for its architecture. We can use it like this:
import { createDecorator } from 'vue-class-component'
// a do-nothing decorator, enabled in the Production environment. Do not use 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(! (keyin this)) {
console.error('Not implemented 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' not implemented
Copy the code
Not so bad: intermediate classes
@Component
class _PlayerImpl extends AbstractMusicPlayer {
audioSrc = '/audio-d.mp3'
}
@Component
export class RealPlayer2 extends Mixins(_PlayerImpl, OtherMixin) {
}
Copy the code
The middle class _PlayerImpl is used to implement the abstract part of the abstract class, which is then used by the real consumer, RealPlayer2. A bit wordy, but type-safe.