How to Transform Your Vue2 Codebase to Vue3 with Script Setup
This article walks through the practical steps for migrating a Vue2 project to Vue3, covering script‑setup syntax, composition‑API patterns, removal of mixins and global variables, file‑naming conventions, and reusable composable functions to achieve a cleaner, more maintainable codebase.
Overview
Vue 3 has been stable for a long time, but many projects still use Vue 2 patterns that limit the benefits of the new composition‑based architecture. The following guidelines show how to migrate to a modern Vue 3 codebase using <script setup>, composable functions, and semantic file naming.
Using <script setup>
The <script setup> block replaces the traditional export default { … } component definition. All component logic lives directly inside the block, eliminating the need for an explicit export statement and the this context.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const title = ref<string>('')
onMounted(() => {
title.value = 'Demo'
})
</script>
<template>
<div :class="$style.container">{{ title }}</div>
</template>
<style lang="scss" module>
/* component styles */
</style>Key differences compared with the Options API:
No export default – the file itself is the component.
Composition functions ( ref, onMounted, etc.) are used directly; methods and this are unnecessary.
Router and other plugins are accessed via dedicated composables (e.g., useRouter) instead of this.$router, which improves type inference.
import { useRouter } from 'vue-router'
const router = useRouter()Component imports become straightforward:
<script setup lang="ts">
import CompA from './CompA.vue'
</script>
<template>
<div :class="$style.container">
<CompA />
</div>
</template>Same‑Name Shorthand (Vue 3.4+)
When a variable name matches a prop or attribute name, the binding can be shortened. This reduces boilerplate but requires disabling the ESLint rule vue/valid-v-bind if linting is enabled.
<script setup lang="ts">
const id = ref('container')
const title = ref('标题')
</script>
<template>
<div :id>
<CompA :title />
</div>
</template> {
"rules": {
"vue/valid-v-bind": "off"
}
}Replacing Mixins with Composables
Mixins rely on this and are hard to type‑check. A composable encapsulates reusable logic and can be imported where needed.
import { onMounted, ref } from 'vue'
const useNavbar = () => {
const navbarProps = ref<any>({})
const setNavbar = (newProps?: any) => {
navbarProps.value = { ...navbarProps.value, ...newProps, ...commonProps }
if (navbarProps.value.title && typeof document !== 'undefined') {
document.title = navbarProps.value.title
}
}
onMounted(() => {
// init logic
})
return { navbarProps, setNavbar }
}
export default useNavbarUsage:
import useNavbar from './useNavbar'
const { navbarProps, setNavbar } = useNavbar()Avoiding Global Properties
Vue 2 often attached utilities to Vue.prototype. In Vue 3 the preferred pattern is a composable that returns the needed functions, optionally exposing them through app.config.globalProperties for legacy code.
// vueThis.ts – safe wrapper around getCurrentInstance
import { getCurrentInstance } from 'vue'
export default () => {
return getCurrentInstance()?.appContext.config.globalProperties
} // useRequest.ts – isolated HTTP helper
export default () => {
const get = async (uri: string, params: any = {}) => {
return await Request.get(uri, params)
}
const post = async (uri: string, params: any = {}) => {
return await Request.post(uri, params)
}
return { get, post }
}Typical usage:
import useRequest from './useRequest'
const { post } = useRequest()
post('/url', {})For TypeScript type augmentation, extend @vue/runtime-core:
import request from '@host/request'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$request: typeof request
}
}One Composable per Concern
Adopt the principle “one use, one responsibility”. Examples: useRequest – handles all HTTP interactions. useNavbar – manages navigation‑bar state and document title. useDevice – encapsulates device‑specific logic. useLoad – controls loading indicators.
Semantic File Naming (Avoid index.vue )
In <script setup> the component name is derived from the file name, which also becomes the route name. Using generic index.vue files leads to ambiguous route names and makes keep-alive inclusion difficult. Prefer descriptive file names.
- pages
- home
- img
- components
- CompA.vue
- CompB.vue
- Home.vue // instead of index.vue
- rule
- Rule.vue // instead of index.vueWhen using keep-alive, the component name can be referenced directly:
<router-view v-slot="{ Component }">
<keep-alive :include="['Home']">
<component :is="Component" />
</keep-alive>
</router-view>Conclusion
Migrating to Vue 3 is most effective when you:
Adopt <script setup> for concise component definitions.
Replace mixins and prototype globals with dedicated composable functions.
Leverage same‑name shorthand (Vue 3.4+) where possible.
Structure the project with semantic file names instead of generic index.vue files.
Follow the “one composable per concern” principle to keep code modular and type‑safe.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
