2 to 3 Migration – A Practical Journey
About Me
- Core member of Vue team
- Not a JavaScript or front-end developer by profession
- OSS proponent
Koel (rememer to switch to the app Nao)
First, Package Upgrade EzPz
- "vue": "^2.5.0",
+ "vue": "^3",
- "vue-loader": "^13.0.5",
+ "vue-loader": "^17",
- "eslint-plugin-vue": "^6.2.2",
+ "eslint-plugin-vue": "^8",
Mounting the App
// App.ts
import { createApp } from 'vue'
import { clickaway, droppable, focus } from '@/directives'
import App from './App.vue'
createApp(App)
.directive('koel-focus', focus)
.directive('koel-clickaway', clickaway)
.directive('koel-droppable', droppable)
.mount('#app')
v3
// App.ts
import Vue, { VNode, CreateElement } from 'vue'
import { clickaway, droppable, focus } from '@/directives'
import App from './app.vue'
Vue.config.productionTip = false
new Vue({
el: '#app',
render: (h: CreateElement): VNode => h(App)
})
Vue.directive('koel-focus', focus)
Vue.directive('koel-clickaway', clickaway)
Vue.directive('koel-droppable', droppable)
v2
The Case of Event Bus
// eventBus.ts
export const eventBus = {
all: new Map(),
on (name: string, cb: Function) {
this.all.has(name) ? this.all.get(name).push(cb) : this.all.set(name, [cb])
return this
},
emit (name: string, ...args: any) {
this.all.get(name)?.forEach((cb: Function) => cb(...args))
return this
}
}
v3
// App.vue
import { eventBus } from '@/utils'
eventBus.on('KOEL_READY', () => initEqualizer())
eventBus.emit('KOEL_READY')
// eventBus.ts
import Vue from 'vue'
export const eventBus = {
bus: new Vue(),
on (name: string, cb: Function) {
this.bus.$on(name, cb)
return this
},
emit (name: string, ...args: any) {
this.bus.$emit(name, ...args)
return this
}
}
v2
Reactivity SuperCharged
// SongStore.ts
import Vue from 'vue'
Vue.set(song, 'lyrics', '')
Vue.set(song, 'playCount', 0)
Vue.set(song, 'liked', false)
Vue.set(song, 'playbackState', 'Stopped')
v2
// SongStore.ts
import { reactive } from 'vue'
const song = reactive(fetchedData)
song.lyrics = ''
song.playCount = 0
song.liked = false
song.playbackState = 'Stopped'
v3
Life Before Composition API: Mixins
<!-- SongContextMenu.vue -->
<template>
<ul>
<li>
Add To
<ul>
<li @click="queueSongsAfterCurrent">After Current Song</li>
<li @click="queueSongsToBottom">Bottom of Queue</li>
<li @click="queueSongsToTop">Top of Queue</li>
<li @click="addSongsToFavorite">Favorites</li>
<li v-for="playlist in playlists" @click="addSongsToExistingPlaylist(playlist)">
{{ playlist.name }}
</li>
</ul>
</li>
</ul>
</template>
<script lang="ts">
import mixins from 'vue-typed-mixins'
import songMenu from '@/mixins/SongMenu.ts'
export default mixins(songMenu).extend({
//...
})
</script>
// mixins/SongMenu.ts
export default Vue.extend({
props: {
songs: {
type: Array,
required: true
} as PropOptions<Song[]>
},
methods: {
queueSongsAfterCurrent () {
queueStore.queueAfterCurrent(this.songs)
},
queueSongsToBottom () {
queueStore.queue(this.songs)
},
queueSongsToTop () {
queueStore.queueToTop(this.songs)
},
addSongsToFavorite () {
favoriteStore.like(this.songs)
},
addSongsToExistingPlaylist (playlist: Playlist) {
playlistStore.addSongs(playlist, this.songs)
}
}
})
v2
<!-- SongContextMenu.vue -->
<template>
<ul>
<li>
Add To
<ul>
<li @click="queueSongsAfterCurrent">After Current Song</li>
<li @click="queueSongsToBottom">Bottom of Queue</li>
<li @click="queueSongsToTop">Top of Queue</li>
<li @click="addSongsToFavorite">Favorites</li>
<li v-for="playlist in playlists" @click="addSongsToExistingPlaylist(playlist)">
{{ playlist.name }}
</li>
</ul>
</li>
</ul>
</template>
<script lang="ts" setup>
import { useSongMenu } from '@/composables'
const {
queueSongsAfterCurrent,
queueSongsToBottom,
queueSongsToTop,
addSongsToFavorite,
addSongsToExistingPlaylist
} = useSongMenu(toRef(context, 'songs') as Ref<Song[]>)
</script>
// composables/useSongMenu.ts
export const useSongMenu = (songs: Ref<Song[]>) => {
const queueSongsAfterCurrent = () => queueStore.queueAfterCurrent(songs.value)
const queueSongsToBottom = () => queueStore.queue(songs.value)
const queueSongsToTop = () => queueStore.queueToTop(songs.value)
const addSongsToFavorite = () => favoriteStore.like(songs.value)
const addSongsToExistingPlaylist = (playlist: Playlist) => playlistStore.addSongs(playlist, songs.value)
return {
queueSongsAfterCurrent,
queueSongsToBottom,
queueSongsToTop,
addSongsToFavorite,
addSongsToExistingPlaylist
}
}
v3
Life After Composition API: Composables
Async Components
// ModalWrapper.vue
import Vue from 'vue'
export default Vue.extend({
components: {
CreateSmartPlaylistForm: () => import('@/components/CreateSmartPlayListForm.vue'),
EditSmartPlaylistForm: () => import('@/components/EditSmartPlayListForm.vue'),
AddUserForm: () => import('@/components/AddUserForm.vue'),
EditUserForm: () => import('@/components/EditUserForm.vue'),
EditSongForm: () => import('@/components/EditSongForm.vue'),
AboutDialog: () => import('@/components/AboutDialog.vue')
}
})
v2
// ModalWrapper.vue
import { defineAsyncComponent } from 'vue'
const CreateSmartPlaylistForm = defineAsyncComponent(() => import('@/components/CreateSmartPlaylistForm.vue'))
const EditSmartPlaylistForm = defineAsyncComponent(() => import('@/components/EditSmartPlaylistForm.vue'))
const AddUserForm = defineAsyncComponent(() => import('@/components/AddUserForm.vue'))
const EditUserForm = defineAsyncComponent(() => import('@/components/EditUserForm.vue'))
const EditSongForm = defineAsyncComponent(() => import('@/components/EditSongForm..vue'))
const AboutKoel = defineAsyncComponent(() => import('@/components/AboutKoel.vue'))
v3
The Fantastic <Script SetUp>
<!-- YouTubeVideoItem.vue -->
<script lang="ts">
import Vue, { PropOptions } from 'vue'
import { youtubeService } from '@/services'
export default Vue.extend({
props: {
video: {
type: Object,
required: true
} as PropOptions<YouTubeVideo>
},
computed: {
url (): string {
return `https://youtu.be/${this.video.id.videoId}`)
}
},
methods: {
play: (video: YouTubeVideo) => youtubeService.play(video)
}
})
</script>
v2
<!-- YouTubeVideoItem.vue -->
<script lang="ts" setup>
import { computed, toRefs } from 'vue'
import { youTubeService } from '@/services'
const props = defineProps<{ video: YouTubeVideo }>()
const { video } = toRefs(props)
const url = computed(() => `https://youtu.be/${video.value.id.videoId}`)
const play = () => youTubeService.play(video.value)
</script>
<template>
<a :href="url" @click.prevent="play">
<img :src="video.snippet.thumbnails.default.url">
<h3>{{ video.snippet.title }}</h3>
<p>{{ video.snippet.description }}</p>
</a>
</template>
v3
Components and model Bindings
v3
// ViewModeSwitch.vue
import { computed } from 'vue'
const props = withDefaults(defineProps<{ modelValue?: ArtistAlbumViewMode }>(), { modelValue: 'thumbnails' })
const emit = defineEmits(['update:modelValue'])
const value = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value)
})
<!-- ArtistsScreen.vue -->
<ViewModeSwitch v-model="viewMode"/>
// ViewModeSwitch.vue
props: {
value: {
type: String as PropType<ViewMode>
}
},
data: () => ({
mutatedValue: null as ViewMode | null
}),
watch: {
value (mode: ViewMode) {
this.mutatedValue = mode
}
},
methods: {
onInput (e: InputEvent): void {
this.$emit('input', (e.target as HTMLInputElement).value)
}
}
<!-- ArtistsScreen.vue -->
<ViewModeSwitch v-model="viewMode"/>
v2
Components and model Bindings
<!-- EditUserProfile.vue -->
<FullName v-model:first-name="user.firstName" v-model:last-name="user.lastName"/>
v3
<!-- FullName.vue -->
<script setup>
defineProps({ firstName: String, lastName: String })
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
About Testing
- @vue/test-utils now considered low-level: Only use if you are building advanced components that require testing Vue-specific internals.
- Officially recommended unit testing library is now @testing-library/vue, built on top of @vue/test-utils.
- Vitest gaining momentum as a blazingly-fast test runner/framework powered by Vite. Support for Vue component is just a matter of one extra plugin.
- No significant or relevant changes made to E2E test suite.
- Since @testing-library's philosophy is to make tests resemble the way software is used by the user, the line between component unit testing and E2E can be very thin.
The Aftermath
$ npx cloc --diff v2 v3 --include-ext=ts,vue
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
SUM:
same 4 13 67 704
modified 22 0 14 251
added 136 1969 416 10002
removed 139 2138 580 11845
-------------------------------------------------------------------------------
- Does it have to be Composition API / <script setup>?
- Should everyone upgrade?
- Read more @ https://v3-migration.vuejs.org
Thank You!
Vue 2 to 3 Migration – A Practical Journey
By Phan An
Vue 2 to 3 Migration – A Practical Journey
- 321