iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🦐

Migrating a Vue 2 Project to Vue 3

に公開
2

About a year has passed since Vue 3 was officially released.

With Vuetify's release goal of Q3 2021 approaching, are you considering updating to Vue 3 soon?

In this article, I will describe the steps to migrate from Vue 2 to Vue 3.

I have prepared the following repository as a reference project.

https://github.com/azukiazusa1/vue3-migrate-test

If you want to experience the migration from Vue 2, please check out the vue2-todo-app tag.

Using the Migration Build

Official tools provide @vue/compat for migrating from Vue 2 to Vue 3.

Using @vue/compat allows the application to run in Vue 2 mode, so you can continue to use APIs that were removed or deprecated in Vue 3, with some exceptions. For features that are removed or deprecated in Vue 3, warnings will be output at runtime. This behavior can also be configured to be enabled or disabled on a per-component basis.

The migration build is scheduled to be discontinued in a future minor version (after the end of 2021), so you should aim to switch to the standard build by then.

Also, if the application is large and complex, migration might be difficult even with the migration build. Since the Composition API and other Vue 3 features are expected to be available in the version 2.7 release (scheduled for the latter half of Q3 2021), staying on Vue 2 is also an option.

Cautions

Currently, the following limitations exist, so it may not be possible to apply the migration tool.

  • Dependency on component libraries such as Vuetify, Quasar, or ElementUI. You will need to wait for a version compatible with Vue 3 to be released.
  • IE11 support. Vue 3 has discontinued support for IE11, so if you need IE11 support, you will likely be unable to migrate to Vue 3.
  • Nuxt.js. Wait for the release of Nuxt 3.

Performing the Upgrade

Now, let's go through the actual steps of migrating a Vue 2 application to Vue 3.

Removing Deprecated Slot Syntax

As a step before starting the migration process, you need to remove the slot syntax that was deprecated in version 2.6.0.

Commit Log

  • src/components/TodoForm.vue
src/components/TodoForm.vue
 <app-button>
-  <template slot="text">更新する</template>
+  <template v-slot:text>更新する</template>
 </app-button>

Installing Tools

Upgrade the tools as necessary.
In the reference project, since vue-cli is used, we upgrade to the latest @vue/cli-service using vue upgrade.

$ vue upgrade

In this case, since the latest vue-cli was already being used, there were no particular differences.

Next, install the following package versions:

  • vue => 3.1
  • @vue/compat => 3.1 (the same version as vue)
  • vue-template-compiler => Replace with @vue/compiler-sfc@3.1.0
$ npm i vue@3.1 @vue/compat@3.1
$ npm i -D @vue/compiler-sfc@3.1.0
$ npm uninstall vue-template-compiler -D

Commit Log

Modifying Build Settings

In the build settings, alias vue to @vue/compat and enable the compat mode in the Vue compiler options.

The example for vue-cli is as follows:

  • vue.config.js
vue.cofig.js
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.resolve.alias.set('vue', '@vue/compat')

    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        return {
          ...options,
          compilerOptions: {
            compatConfig: {
              MODE: 2
            }
          }
        }
      })
  }
}

Commit Log

Fixing Compilation Errors

After following the steps so far, you may encounter many compilation errors.
Let's fix them one by one.

Fixing ESLint

Install version 7.x of eslint-plugin-vue and change the configuration to 'plugin:vue/vue3-recommended'.

$ npm i eslint-plugin-vue@7 -D
  • .eslintrc.js
.eslintrc.js
 extends: [
-  "plugin:vue/essential",
+  "plugin:vue/vue3-recommended",
   "eslint:recommended",
   "@vue/typescript/recommended",
   "@vue/prettier",
   "@vue/prettier/@typescript-eslint",
 ],

Let's run the lint. Some unfixable errors will be output, but we will fix them later.

$ npm run lint

Some items were automatically fixed by the lint.
For example, the .sync modifier was removed and replaced with v-model.

Component v-model has been reworked, replacing v-bind.sync

  • src/components/EditTodo.vue
src/components/EditTodo.vue
 <todo-form
   @submit="onSubmit"
-  :title.sync="title"
-  :description.sync="description"
-  :status.sync="status"
+  v-model:title="title"
+  v-model:description="description"
+  v-model:status="status
 />

Commit Log

Upgrading Vue Router

If you are using vue-router, upgrade it to v4.

npm i vue-router@4

Let's fix the various compilation errors that occurred as a result of upgrading vue-router.

new VueRouter => createRouter

VueRouter is no longer a class, so use createRouter instead of new VueRouter().

  • src/router/index.ts
src/rouer/index.ts
- import VueRouter, { RouteConfig } from "vue-router";
+ import { createRouter } from "vue-router";

// Omitted

- const router = new VueRouter({
+ const router = createRouter({
   mode: "history",
   base: process.env.BASE_URL,
   routes,
 });

mode: history => history: createWebHistory()

The mode option has been removed and replaced with history. If you specified history in mode, use createWebHistory().
Also, the base option is now passed as an argument to createWebHistory().

  • src/router/index.ts
src/router/index.ts
- import { createRouter } from "vue-router";
+ import { createRouter, createWebHistory } from "vue-router";

// Omitted

 const router = createRouter({
-  mode: "history",
-  base: process.env.BASE_URL,
+  history: createWebHistory(process.env.BASE_URL),
   routes,
 });
RouteConfig => RouteRecordRaw

This is simply a rename of the TypeScript type.

  • src/router/index.ts
src/router/index.ts
- import { createRouter, createWebHistory } from "vue-router";
+ import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

 Vue.use(VueRouter);

- const routes: Array<RouteConfig> = [
+ const routes: Array<RouteRecordRaw> = [
Removing Vue.use(VueRouter)

Vue.use(VueRouter) is no longer necessary, so remove it. (We will replace it with the correct router registration method later.)

  • src/router/index.ts
src/router/index.ts
- import Vue from "vue";
 import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

- Vue.use(VueRouter);

Commit Log

Upgrading Vuex

Similarly, upgrade vuex to v4.

$ npm i vuex@4

Since the differences are fewer compared to vue-router, I will summarize them into one. Use createStore instead of new Vuex.Store and remove Vue.use(Vuex).

  • src/store/index.ts
src/store/index.ts
- import Vue from "vue";
- import Vuex from "vuex";
+ import { createStore } from "vuex";
 import todos from "@/store/todos/index";

- Vue.use(Vuex);

 export default createStore({
   modules: {
     todos,
   },
 });

Commit Log

Fixing the Vue Global API

One of the major changes in Vue 3 is that the Vue global instance has been deprecated.

For example, new Vue({}) is replaced by createApp, and Vue.use() is replaced by app.use() (where app is the instance created by createApp).

Additionally, Vue.config.productionTip has been removed.

Modifying main.ts

First, let's modify main.ts.

  • src/main.ts (Before)
main.ts
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: (h) => h(App),
}).$mount("#app");
  • src/main.ts (After)
main.ts
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

const app = createApp(App)

app.use(router)
app.use(store)

app.mount("#app")

Since type errors will occur, let's also fix shims-vue.d.ts while we're at it.

  • Before
shims-vue.d.ts
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}
  • After
shims-vue.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent
  export default component
}
Removing Vue.extend

If you were using the Options API with TypeScript, you used Vue.extend to provide type inference for components. This has also been removed. Use defineComponent instead.

It's quite a hassle because you have to replace it in every component...

- import Vue from "vue";
+ import { defineComponent } from "vue";

- export default Vue.extend({
+ export default defineComponent({
$store Type Definition

Since the type definition for this.$store is lost, create a vuex.d.ts file to provide a type definition for this.$store.

vuex.d.ts
import { Store } from "vuex";

declare module "@vue/runtime-core" {
  interface ComponentCustomProperties {
    $store: Store;
  }
}

Removing $listeners

The $listeners object has been removed in Vue 3 and has simply become part of the $attrs object.

  • src/components/AppInput.vue
src/components/AppInput.vue
<template>
  <label>
    {{ label }}
    <input id="input" v-bind="$attrs" />
  </label>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  inheritAttrs: false,
  props: {
    label: {
      type: String,
      default: "",
    },
  },
});
</script>

Removing Filters

In Vue 3, filter syntax has been removed. Instead, replace them with simple methods or computed properties.

  • src/components/TodoItem.vue (Before)
src/components/TodoItem.vue
<template>
  <div class="card">
    <div>
      <span class="title">
        <router-link :to="`todos/${todo.id}`">{{ todo.title }}</router-link>
      </span>
      <span class="status" :class="todo.status">{{ todo.status }}</span>
    </div>
    <div class="body">Creation Date: {{ todo.createdAt | formatDate }}</div>
    <hr />
    <div class="action">
      <button @click="clickDelete">Delete</button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from "vue";
import { Todo } from "@/repositories/TodoRepository/types";

export default defineComponent({
  props: {
    todo: {
      type: Object as PropType<Todo>,
      required: true,
    },
  },
  filters: {
    formatDate(date: Date): string {
      return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
    },
  },
  methods: {
    clickDelete() {
      this.$emit("delete", this.todo.id);
    },
  },
});
</script>
  • src/components/TodoItem.vue (After)
src/components/TodoItem.vue
<template>
  <div class="card">
    <div>
      <span class="title">
        <router-link :to="`todos/${todo.id}`">{{ todo.title }}</router-link>
      </span>
      <span class="status" :class="todo.status">{{ todo.status }}</span>
    </div>
    <div class="body">Creation Date: {{ formatDate }}</div>
    <hr />
    <div class="action">
      <button @click="clickDelete">Delete</button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from "vue";
import { Todo } from "@/repositories/TodoRepository/types";

export default defineComponent({
  props: {
    todo: {
      type: Object as PropType<Todo>,
      required: true,
    },
  },
  computed: {
    formatDate(): string {
      const { createdAt } = this.todo;
      return `${createdAt.getFullYear()}/${
        createdAt.getMonth() + 1
      }/${createdAt.getDate()}`;
    },
  },
  methods: {
    clickDelete() {
      this.$emit("delete", this.todo.id);
    },
  },
});
</script>

Using <template v-for>

VueCompilerError: <template v-for> key should be placed on the <template> tag.

In Vue 2, the key could not be included in the <template> tag, so keys were placed on each child element. In Vue 3, the key can now be included in the <template> tag, so we fix this as follows:

  • src/pages/TodoList.vue
src/pages/TodoList.vue
- <template v-for="(todo, index) in todos">
-   <span :key="`index-${todo.id}`">{{ index + 1 }}</span>
-   <todo-item :todo="todo" :key="`item-${todo.id}`" @delete="deleteTodo" />
+ <template v-for="(todo, index) in todos" :key="todo.id">
+   <span>{{ index + 1 }}</span>
+   <todo-item :todo="todo" @delete="deleteTodo" />

By this point, the application should be working without any problems! 🎉

Commit Log

Fixing Individual Warnings

While the application is running thanks to @vue/compat, to fully migrate to Vue 3, you also need to fix errors at the warning level.

Screenshot 2021-09-12 14.18.22

As an example, let's fix the second one, deprecation COMPONENT_V_MODEL. This is a change in property and event names when using v-model with custom components.

  • Property: value -> modelValue
  • Event: input -> update:modelValue

I fixed it as follows:

  • src/components/AppInput.vue
src/components/AppInput.vue
<template>
  <label>
    {{ label }}
    <input
      :value="modelValue"
      @input="(e) => $emit('update:modelValue', e.target.value)"
    />
  </label>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  props: {
    modelValue: {
      type: String,
      default: "",
    },
    label: {
      type: String,
      default: "",
    },
  },
});
</script>

Then, a new warning is output.

Screenshot 2021-09-12 14.26.05

This error occurs because we are using Vue 3 syntax while the application is running in Vue 2 mode. To remove this, modify the compatConfig option.

  • src/components/AppInput.vue
src/components/AppInput.vue
 import { defineComponent } from "vue";
 export default defineComponent({
+  compatConfig: {
+   COMPONENT_V_MODEL: false,
+  },
   props: {
     modelValue: {
       type: String,
       default: "",
     },
     label: {
       type: String,
       default: "",
     },
   },
 });

I added COMPONENT_V_MODEL: false to compatConfig. As shown above, it's possible to opt-in to Vue 3 behavior on a per-component basis.

You can also change this via global settings rather than per-component.

import { configureCompat } from 'vue'

// Disable compat for specific features
configureCompat({
  FEATURE_ID_A: false,
  FEATURE_ID_B: false
})

Commit Log

Complete Migration

Once you have removed all warnings, you can remove the migration tool!

$ npm uninstall @vue/compat
  • vue.config.js
// vue.config.js
module.exports = {};

Commit Log

References

GitHubで編集を提案

Discussion