DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Sahin D.
Sahin D.

Posted on

How we migrated to VueΒ 3

In this article, I'll try to share with you an overview of the Vue 3 migration journey.

This article is not a guide on how to migrate Vue 3.
It's a story of how we made it, alive πŸ€“


Just like any other technology company out there, here at Insider, we also have faced this ultimate dilemma.
Upgrading our front-end framework to the new version of Vue.

This journey started with a discussion of course. On a sunny day, I don't remember when, we talked about Vue 3, and the point that it's becoming the new default, and we are still using the old version.

We wanted to switch, but we needed to make sure that this "new default" is gaining traction and support from the community, just like everyone.

We hesitated to upgrade long before because some third-party libraries out there that we may need at some point may not support Vue 3!
Just because of this reason, we've waited till Vue 3 is stable and adapted well.

Time has passed and Vue 2 has been marked as in maintenance mode. So we have started to make plans, and experiments on our products, research the migration process, and considered the possible outcomes of the upgrading process.

For the business part, we made sure that we can do this tech debt within our sprints, without affecting anything.

And we started!

But just to let you know, I'm not actually into the reason why this upgrade is needed.
There are plenty of resources and research out there about why migration is necessary for your product.


So, here we go;

First, I have read the Vue 3 Migration Guide https://v3-migration.vuejs.org/

I have shared the crucial points with my teammates and what breaking changes we will be going to face and how we can write a new code after the upgrade.

Since we are using the Options API, changing the structure was the scariest part. However, after the research, we understand that we don't need to convert our components, and files to the new Composition API.

They both are supported and can be used at the same time.
We discussed and have made a new rule for our codebase. We will use the new Composition API when creating a new component.
Of course, using both APIs together creates another tech debt item, but as you can guess, we couldn't afford to spend efforts to convert files. This can be done in the next sprints so that we can have new tech debt items.

So, as suggested in this article, I have upgraded the packages like the following:

   "dependencies": {
     // ...
-    "vue": "2.6.11",
-    "vue-i18n": "8.26.5",
-    "vuex": "3.0.1"
+    "@vue/compat": "3.2.37",
+    "vue": "3.2.37",
+    "vue-i18n": "9.1.10",
+    "vuex": "4.0.2"
   },
   "devDependencies": {
     // ...
-    "@babel/eslint-parser": "7.16.5",
-    "@vue/cli-plugin-babel": "4.5.0",
-    "@vue/cli-service": "4.5.0",
-    "node-sass": "4.14.1",
-    "sass-loader": "8.0.0",
-    "vue-template-compiler": "2.6.11"
+    "@babel/eslint-parser": "7.18.2",
+    "@vue/cli-plugin-babel": "5.0.8",
+    "@vue/cli-service": "^5.0.8",
+    "@vue/compiler-sfc": "3.2.37",
+    "sass": "1.53.0",
+    "sass-loader": "13.0.2",
   },
Enter fullscreen mode Exit fullscreen mode

You see, we are also expecting to upgrade Vuex too. But our next plan is to replace it with Pinia. It's the officially supported state management library by the Vue core team currently, and Vuex<4 has reached its final, and it's been in maintenance mode for a while.

So, after changing dependencies I have tried bundling the app and as the migration guide suggests, I have seen many errors about some deprecated things.

The first error that I have focused on is about the /deep/ selectors. It has been deprecated and the Vue documentation suggests the new way :deep(). I have changed all usages.

Then I saw that there's an error about util and assert library polyfills required because of another third-party library we use, and the WebPack 5 upgrade made them required.

After trying to bundle again, new warnings/errors popped for the deprecated usages within our files.
Such as v-bind, template slot, v-enter and v-leave transition keys, usages with v-for and v-if together.

And then there was the deprecated app entry, and the guide suggested that the new global mounting API needs to be used.
I have changed the file like the following:

const vueInstance = new Vue({
    store,
    render: h => h(App),
}).$mount(rootContainer);

Vue.directive('tooltip', tooltip);
Vue.use(VueI18n);
Enter fullscreen mode Exit fullscreen mode
const app = createApp(App);
const i18nInstance = createI18n({ locale, messages });

app.use(store);
app.directive('tooltip', tooltip);
app.use(i18nInstance);
app.mount(rootContainer);
Enter fullscreen mode Exit fullscreen mode

After re-bundling again, there were some hooks like beforeDestroy, and destroyed are deprecated. So they needed to be changed as well.

I repeated this process until the project got clear from all warnings, and errors and make sure that our app works.

So, here I'm at the point where frustrations begin to appear.

First, it's the vue-i18n library. I have come across this issue https://github.com/kazupon/vue-i18n/issues/1493 and right now, it's not yet solved.
So I decided to remove vue-i18n since we are not using it in an advanced way (pluralization etc.). Just a simple, key-value matching, nothing fancy.

State Management Part

And then there's the state management library issue. Before diving into Vuex problems, we simply switched to Pinia and made integrations described in its guide.

This journey started by following this guide: https://pinia.vuejs.org/cookbook/migration-vuex.html

The conversion is quite simple and delightful for creating store files, and the process is explained quite well in that article.

Before the migration, we have the following structure on our app:

- store
  - common
    - index.js
    - types.js
  ...
Enter fullscreen mode Exit fullscreen mode

We simply changed it to:

- stores
  - common.js
  ...
Enter fullscreen mode Exit fullscreen mode

The files can be separated into submodules as well. But for our use cases, this folder structure was quite enough.

The part, that we no longer need to have mutations with Pinia, is the most exciting thing about this migration. I always think that there's no point in having actions and mutations.

So starting with the replacement procedure, we have created store files.

Then we search the entire codebase by finding each vuex and store usages. We pass through every single file and replace all Vuex usages with Pinia.

The Docker

Yeah, we were using an old version of NodeJS(v14) and we also update the NodeJS version that is being declared in our Dockerfile.

The new emits property

Our .vue files are also emitting custom events to their parents and this leads to an issue for us. I didn't want to bother changing all files manually, just for the emits, so I wrote a little node script that could handle this for us.

This small script will create the emits property by collecting event names across .vue files

addEmits.js
const fs = require('fs');
const glob = require('glob');

const directories = glob.sync('./src/**/*.vue');

class FileHandler {
    /**
     * @param {string} path
     * @param {string} content
     */
    constructor (path, content) {
        this.path = path;
        this.content = content;

        this.collectEventNames();

        if (!this.eventNames) {
            return;
        }

        this.addEmitsLine();
    }

    collectEventNames () {
        const matches = this.content.match(/(?<=\$emit\()'.*?'(?=[,)])/g);

        if (!matches?.length) {
            return;
        }

        const eventNames = [...new Set(matches)];

        eventNames.sort();

        this.eventNames = eventNames.join(', ');
    }

    addEmitsLine () {
        if (!this.eventNames) {
            return;
        }

        const newLine = `export default {\n    emits: [${this.eventNames}],`;

        this.content = this.content.replace('export default {', newLine);

        fs.writeFileSync(this.path, this.content, 'utf-8');
    }
}

const fileWithEmitsProp = [];

directories.forEach(filePath => {
    const content = fs.readFileSync(filePath, 'utf-8');

    if (!content.includes('$emit')) {
        return;
    }

    if (content.includes('emits:')) {
        fileWithEmitsProp.push(filePath);

        return;
    }

    new FileHandler(filePath, content);
});

if (fileWithEmitsProp.length) {
    console.log('File paths with `emits` props:', fileWithEmitsProp);
}
Enter fullscreen mode Exit fullscreen mode

Changing Vue-CLI with Vite

While investigating Vue 3 for further breaking-changes, I've come across with the Vue-CLI repository. Seems like it was also marked as in maintenance mode. At first, we didn't have any plans on changing the Vue-CLI.
Since we have the opportunity to change the compiler, we touch the Vite as well.
Through the migration, I have followed an article, How to Migrate from Vue CLI to Vite by Daniel Kelly

https://vueschool.io/articles/vuejs-tutorials/how-to-migrate-from-vue-cli-to-vite/

The article covered the essential headlines that we need.

Pinia's subscription flow

After building again and running our app, I have seen some subscription-related issues.
I then noticed that Pinia was handling subscriptions differently than Vuex.
On our app, we were subscribing to the store and listening to every mutation that was used, and handling the logic accordingly.
However, by looking at the docs of Pinia, we have noticed that there's a different behavior that requires a little bit of refactoring.

When the app calls an action declared in a Pinia store, Pinia calls subscribed listeners via the $onAction method, before mutating the state. The callback function will get a parameter called context and it will have a method called after which will be called after the mutation has been made.
Explained here: https://pinia.vuejs.org/core-concepts/actions.html#subscribing-to-actions

Before we notice the after callback, this was like a deal breaker for us but once we notice the after callback, it was quite a lifesaver.

The @ckpack/vue-color

We continued to modify our app and we also saw that we have a dependency that was not working correctly. After searching again, I noticed that it doesn't support Vue 3 so we had to replace it with another library called @ckpack/vue-color

Migrating webpack resolvers

After trying to build the app again, I've seen that we had issues with some browserify related polyfills.

We have used the following solution


import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill';
import rollupNodePolyFill from 'rollup-plugin-node-polyfills';
import { defineConfig } from 'vite';


export default defineConfig(() => ({
  resolve: {
    alias: {
      vue: 'vue/dist/vue.esm-bundler.js',
      util: 'rollup-plugin-node-polyfills/polyfills/util',
      events: 'rollup-plugin-node-polyfills/polyfills/events',
      assert: 'rollup-plugin-node-polyfills/polyfills/assert',
      buffer: 'rollup-plugin-node-polyfills/polyfills/buffer-es6',
      process: 'rollup-plugin-node-polyfills/polyfills/process-es6',
    },
  },
  build: {
    minify: !isDevelopment,
    rollupOptions: {
      plugins: [
        rollupNodePolyFill(),
      ],
    },
  },
  optimizeDeps: {
    esbuildOptions: {
      define: {
        global: 'globalThis',
      },
      plugins: [
        NodeGlobalsPolyfillPlugin({
          process: true,
          buffer: true,
        }),
        NodeModulesPolyfillPlugin(),
      ],
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Editor/IDE support

For the editor integration, we also need to migrate some extensions.

However, this part doesn't concern IntelliJ users since it supports both Vue 2 and Vue 3 out of the box.

But me and some of my colleagues, are using VSCode ✨.
We've switched from Vetur to Volar extension to make sure that our editor shows the right syntax highlighting, etc. for Vue 3 files.

Switching to Composition API

After learning that Vue 3 still supports Options API, we didn't change the whole component structure at first, all together.
We decided that we can refactor files later and we can migrate small components to Composition API through the next development.

However, we have to change each component and wrap exported objects with defineComponent function.

Before:

export default {
  components: {
      Box,
  },
  props: {
      prop1: {
          type: Array,
          required: true,
      },
  }
};
Enter fullscreen mode Exit fullscreen mode

After:

export default defineComponent({
  components: {
      Box,
  },
  props: {
      prop1: {
          type: Array,
          required: true,
      },
  }
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Yes, it was quite an exhausting journey.
I still think that it is hard and maybe impossible to find every outcome while researching the migration and look out for every possible blocker at first glance without actually experimenting with it.

Was it worth it?
Definitely! You gain a lot more than before.

Top comments (2)

Collapse
 
thi3rry profile image
Thierry Poinot

Did you make benchmarks before/after, or the upgrade is enough to see an user experience improvment ?

Collapse
 
seahindeniz profile image
Sahin D.

Hi Thierry,
For the upgrade, we didn't make a benchmark for our app due to its nature. However, while researching, I have come across some other articles which will help you decide whether it's improving UX and DX.

docs.google.com/spreadsheets/d/1VJ...
medium.com/front-end-weekly/whats-...
geckodynamics.com/blog/vue2-vs-vue3

From what I can tell about the development part, it has a little learning curve for the developers who are not familiar with the Composition API ☺️

Let's Get Wacky


Use any Linode offering to create something unique or silly in the DEV x Linode Hackathon 2022 and win the Wacky Wildcard category

β†’ Join the Hackathon <-