I recently converted a second app from Vue 2 to Vue 3, added TypeScript, and swapped from options api to composition api. Here are my notes from the experience.
My 3 main target areas for this migration were:
- Vue 2 to Vue 3
- JavaScript to TypeScript
- Options API to Composition API
Almost every resource I used was from the official docs for Vue, the Vue CLI, the Vue Router, and Vuex. Some of these docs have sections in migrating, adding (like vue add typescript), or upgrading.
- Vue 3 docs
- Vue 3 Migration docs
- Vue CLI
- Vue Router 4.0 migration guide
- Vuex 4 docs
- Sample GitHub Repo and its Pull Request
All of these have docs. Some have migration guides, some do not. Some of the migration guides tell you the differences, but don't do it from an existing app.
The steps I followed may not be the most optimal path. I would be shocked if they were. Your mileage may vary (YMMV). After digging through the docs and the Vue CLI API (to check out the add/migration commands) this is where I landed. Hopefully by sharing the steps I went through they will be helpful to some of y'all.
If you want to host your Vue app in the cloud, I recommend trying Azure Static Web Apps.
Why
If you don't care why I went down this road, you can scroll past this "Why" section.
I find it valuable to understand why I should consider doing something before I invest time into doing it. Thus, I'll explain a bit of my reasons why I chose to migrate for each of these three target areas.
Vue 3
The migration to Vue 3 is to keep up to date with Vue. I find it important stay current with major version changes of web frameworks sooner rather than later. Often the tooling supports older versions for a while, but as the ecosystem evolves, the latest versions get the love and as with most software, the older versions start to lag.
Also, keeping up with the latest version offers a lot value in the core framework improvements. One feature I won't miss is mixins, which I rarely used. I find the hooks concept or simply importing other modules of code to be much easier to follow, reuse, and maintain.
JavaScript to TypeScript
One of the most exciting to me is that Vue 3 is written with TypeScript and in my experience this helps the stability of the platform and provides much better development and tooling help.
I've converted a lot of apps to TypeScript over the years. I've also started a lot of apps with TypeScript from scratch. Adding types to the development flow almost always reveals bugs in my code that were previously undetected in my JavaScript code. I do not follow the school of "type the heck out of everything though". This is a road that, IMO in a web app, can lead to hours of time with little to no reward. I do add types. I do avoid any
. But there are times to weigh the value.
Composition API
There is nothing wrong with the Options API. In fact, it is arguably one of the biggest reasons why Vue has been so approachable and easy to learn. That all said, I do find that spreading code out across my components does make it harder to read my own code vs keeping similar logic together. For this reason, I want to give the Composition API a chance. It took a lot of work to get the code converted to the Composition API.
Migration
Here is where I started. The notes below are indeed notes. They are not every single step I took. I will explain how I thought through these steps and which ones worked well for me and which ones I struggled through.
The first step for me was to get the migration kicked off on a new git branch. So I started by aking a new branch so I could track the changes.
1. Migrate from Vue 2 to Vue 3
The Vue CLI has a command to upgrade to Vue 3.
vue add vue-next
Running this command modified these files:
File | Change |
---|---|
eslintrc.js | for some Vue 3 eslint settings |
package.json | this upgraded Vue to 3.0.0 beta.1 and Vuex to 4.0.0-alpha.1. It made me wonder why the alpha and betas, of course. I had to manually add @vue/compiler-sfc , not sure why. But when I tried building the app, it complained about this being missing. How did I know this? I generated a new Vue 3 app and saw it there. Maybe I missed this in my existing app. |
_src/main.js |
The createApp API replaces the new Vue({ ... }) API |
_src/store/index.js |
The Vuex.createStore API replaced the Vue.use(Vuex) API |
2. Adding TypeScript
The Vue CLI has a command to upgrade to TypeScript.
vue add typescript
Running this command modified these files:
File | Change |
---|---|
eslintrc.js | SSome extends settings changed. But instead of adding the new ones, I ended up with 2 extends arrays. So I had to manually fix this. |
package.json | Several typescript packages were added |
| app.vue | This entire component was overwritten. I ended up with a reference to a HelloWorld component (which also was added). My app obviously doesn't need that so this was code to remove. It also added some CSS and template code that I had to remove. I had to manually revert the changes, and apply the ones that were needed for TypeScript. The key here was to revert the changes with git and apply the <script lang="ts">
and swap from export default { ... }
to export default defineComponent ({ ... })
|
| *.ts files | Many javascript files were renamed to typescript files |
| shims-vue.d.ts
| This typings file was added to support some Vue conventions |
| tsconfig.json | The typescript config file |
This is the second project I migrated from Vue 2 to Vue 3 and added TypeScript. I thought that this process migrated all components to TypeScript. Maybe I was wrong, because I had to manually upgrade every component this time, which leads me to the next step.
3. Fixing TypeScript
I went through every component file and applied the <script lang="ts">
and swapped from export default { ... }
to export default defineComponent ({ ... })
. This took a while.
4. Vue Router
The Vue CLI has a command to upgrade the Router.
vue add router
Running this command modified these files:
File | Change |
---|---|
package.json | A few router packages were added and modified. The vue router bumped up to 4.0.0-0 |
_src/main.ts |
The createApp API extended to include use(router) |
_src/router.ts |
The createRouter API replaced the old Vue.use(Router) API. It also added the createWebHistory API which replaces the mode: history technique |
When I built the app and served it, I found an error about the catch-all route in the browser console error messages. So I checked out the Vue Router docs and it said I needed to manually refactor the "catch-all" route in router.ts.
This went from this code:
{ path: '*', component: PageNotFound },
... to this code ...
{ path: '/:pathMatch(.*)*', name: 'not-found', component: PageNotFound },
This was in the Router docs, which was helpful.
5. Vuex
The Vue CLI has a command to upgrade Vuex.
vue add vuex
Running this command modified these files:
File | Change |
---|---|
package.json | A few Vuex packages were added and modified. The vuex version bumped up to 4.0.0-0 |
Nothing else changed, which I found surprising. I assumed the store logic would upgrade, but it did not. This lead me to manually modify the store file.
File | Change |
---|---|
src/store/index.ts | I swapped to the new crateStore API. Similar to the other API changes for Vue and the Vue Router |
6. Pausing for TypeScript
Then I made a ton of TypeScript modifications. I went through all of my Vuex code and added types. This took some time, but it was worth it (and expected). This is my code and only I knew the types ... and in some cases I had to create the types in the form of types, classes, and interfaces (again, for my own code).
One key aspect here is that I customized several ESLint settings. Here is what I added to eslintrc.js. When running npm run lint
the Vue compiler spits out the eslint errors and warnings.
Here is what I added.
'max-classes-per-file': 'off',
'no-useless-constructor': 'off',
'no-empty-function': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'import/prefer-default-export': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
Setting | Why |
---|---|
'max-classes-per-file': 'off', | When I create models I often do this one per file. But in my project I had a bunch of small 5 lines of code classes and it was easier to maintain those in a single file. |
'no-useless-constructor': 'off', | Some of my models have empty constructors. However, they have initialization parameters that allow me to call them to create a new instance and set properties like this new Hero(1, 'John',) |
'no-empty-function': 'off', | Same issue as the empty constructor above. |
'@typescript-eslint/no-useless-constructor': 'error', | Same issue as the empty constructor above. |
'import/prefer-default-export': 'off', | I don't prefer default exports. |
'no-use-before-define': 'off', | I often put function definitions where I want them in a file. basically i use hoisting to my advantage for readability. So I turn this off in most projects. |
'@typescript-eslint/no-unused-vars': ['error'], | If I didn't add this, then every time I imported a type/class/interface and used it as a type, eslint complained. |
Summary
Again ... the steps I followed may not be the most optimal path. I would be shocked if they were. Your mileage may vary (YMMV). After digging through the docs and the Vue CLI API (to check out the add/migration commands) this is where I landed. Hopefully by sharing the steps I went through they will be helpful to some of y'all.