Introductionβ
In the ever-evolving world of software development, staying up-to-date with the latest technologies is crucial. When tasked with upgrading a legacy Vue2 application to utilize the Vue Composition API, our developer team embarked on an exciting and challenging journey. In this blog post, we will share our experiences, challenges, and triumphs during this upgrade process. Additionally, we will delve into the issues we encountered with end-to-end testing, which we handled using Ruby (Capybara) and Cypress. Let's dive in!
In July 2022, our Frontend team had the opportunity to undertake a highly ambitious project that only a few developers get to experience: the refactoring of an entire component library of a large and complex application. Our client was a cloud-based multi-metering operating system for buildings.
The challenge presented to us was that the company had built their application entirely reliant on the Vuetify library, which at the time was designed to be only compatible with Vue2 syntax. However, the company wanted to break free from this dependency and upgrade to Vue3. The Vue Composition API, introduced in Vue3, offered improved reusability, composability, and scalability. The decision was made to upgrade the application to leverage the power of this new API. To address this, we devised a solution that would be future-proof: developing a custom component library for the platform, that would introduce Composition API syntax to most components, but also create solutions where we could adapt the flexible Vue Options API in components that were too dependent on the previous functionality.
The beginning of the journey πβ
Given a large code base, how do we find an angle for an initial estimate, especially when the frontend accounts for over 80k lines of code? For this project, we chose two structural approaches:
- At first, we looked at the sheer amount of files that were relying on the Vuetify library, and we sampled different complexities for a more detailed estimate. Extrapolating from individual estimates to the entirety of the codebase gave us an initial number to work with.
- After that was done, we took another metric, which was occurrences of components in code that needed attention. We then made estimates for a sample of those occurrences, and again extrapolated to the whole of the codebase.
Getting similar numbers from different structural estimation approaches can provide some confidence when it is otherwise not feasible to estimate all of the more granular code changes in detail, and the codebase is largely unknown. A big project like this becomes easier to tackle, and with today's knowledge we can proudly say that we concluded the project within the original estimates margin of error.
Creating the components βοΈβ
The application is an extensive project that has been continuously developed over the past six years. During the refactoring process, our initial focus was on identifying the Vuetify components that needed to be replaced. We tackled around 13,000 instances of these components scattered across approximately 650 files. Another advantage of the structural approach to estimation that we chose here is that it can also inform tracking of progress over time. We made use of this by regularly reviewing the progress based on remaining occurrences of components that need updating, and keeping track of the changes over time. This allowed us to see how far we had gotten, but also how much still needed to be done.
We developed a total of 81 custom components, including buttons, cards, forms, containers, breadcrumbs, data tables, sparklines, progress bars, and a lot more. Our approach involved starting with the simpler components like icons and buttons. As we grew more familiar with the codebase and gained a better understanding of our plan of action, we gradually progressed to the more intricate components such as data tables and forms. The following dot plot shows the progress over the course of nearly a year's worth of contributions.
A logarithmic scale helped us highlight and remember that removing one or two components that were only used once or twice in the entire codebase can be more impactful than systematically removing 100 trivial component occurrences like v-col
that were used a couple hundred times throughout the project.
The chart also reveals the fact that the software was actively being worked on by the client during the entirety of this project. See for example the component v-textarea
, which went from 6 occurrences in October to 8 occurrences in December.
We revisited this chart often in our planning meetings to get a feeling for what's important next, where the most impact lies in our changes, but also to see how much the team already accomplished.
Toolingβ
For planning and collaboration with the client's team we used Linear. We relied on GitHub for version control and collaboration. Git branching and pull requests were essential for coordinating changes, reviewing code between the client's software developers and ours, and ensuring a seamless integration of new features and fixes. Regular code reviews and discussions within pull requests fostered a sense of team ownership and knowledge sharing.
The client was using Storybook internally, and we decided to add documentation of our components there as well. These storybook files provide clear instructions on how each component should be used and include variants for all encountered use cases.
When it came to styling, we opted for Tailwind due to its speed, user-friendly nature, and customization flexibility. Since the existing application utilized inline Vuetify classes, we adopted a prefixed approach, adding the tw-
prefix to our Tailwind classes to avoid conflicts and maintain consistency.
Data Tables ποΈβ
One of the core components of the Comgy frontend application were the Data-Tables
for displaying the data for meters, tenants, real estates, radiators, and other entities. To achieve this, we opted to create our custom data table and filters, using the PrimeVue library, which we found to be a suitable alternative to Vuetify. However, a significant challenge emerged as our clients desired minimal changes to the functionality and UI of their existing implementations. Ensuring that the PrimeVue library mirrored the look and feel of Vuetify proved to be quite demanding, as we had to strive to make one library resemble another while maintaining the desired functionality and user interface aspects.
In the bellow example, you can see the difference between the two syntaxes, both libraries have a lot of inbuilt functionalities for rendering their values, rows and columns:
Vuetify Data Table | Primevue Data Table |
---|---|
|
|
The Challenges of Vue Composition API Migrationβ
Migrating a legacy codebase to a new version of a framework always presents challenges, and our journey was no exception. Here are a few key hurdles we encountered during the Vue Composition API upgrade:
-
Paradigm Shift: The Composition API introduced a paradigm shift in how we structured our Vue components. Adapting to the new setup required adjusting our coding practices, rethinking component composition, and understanding the reactive system introduced by the Composition API.
-
API Differences: Vue 2 and Vue 3 have distinct APIs, and many components in our application had to be refactored to align with the new syntax. Adapting the existing codebase to leverage the Composition API's reactive properties and lifecycle hooks required careful consideration and code refactoring.
We had both versions installed, which could lead to many errors if not managed carefully due to their different rendering methods. To assist in identifying the Vue version, we developed a helper function:
const isVue2 = Boolean(Vue && Vue.version.startsWith("2"));
Usage :
const onInput = (value) => isVue2 && updateModelValue(value);
-
Tooling Compatibility: Integrating the Composition API with our existing tooling ecosystem required updates to various plugins and libraries. We faced compatibility issues with some third-party packages and had to find alternatives or create custom solutions to ensure a smooth transition.
As an illustration, consider the following example of syntax translation:
Old Options API:
<script>
export default {
computed: {
radiator() {
return this.$store.getters['radiators/radiator'](this.$route.params.id);
}
}
}
</script>
New Compositions API:
<script setup>
const store = useStore(); const radiator = computed(()=>
store.getters['radiators/radiator'](this.$route.params.id))
</script>
Handling End-to-End Testing with Ruby (Capybara) and Cypressβ
End-to-end testing is crucial for maintaining the quality and stability of our application. However, testing the upgraded Vue Composition API code posed unique challenges. We employed a hybrid approach, utilizing Ruby with Capybara for some tests and Cypress for others. Ruby with Capybara: For existing tests written in Ruby, we used Capybara, a powerful Ruby library for simulating user interactions in web applications. Capybara allowed us to write expressive tests that interacted with our Vue components and verified their behavior.
Cypress: To test the newly introduced Vue Composition API features, we embraced Cypress, an end-to-end testing framework designed for modern web applications. Cypress provided a comprehensive tool-set for writing and running tests, enabling us to validate our components' functionality across different browsers. By combining both tools, we ensured a smooth transition from legacy tests to new ones, gradually migrating our test suite as we upgraded components.
Overcoming Synchronization Challenges in a Growing Codebase πβ
One of the most intriguing challenges we encountered while collaborating with our client was staying synchronous with their front-end team's efforts. As we created new components, their team was simultaneously utilizing them and introducing fresh functionalities. While their team was highly communicative, and we conducted daily stand-ups and code reviews, a few instances of miscommunication still arose.
For example, there were a couple of occasions where we had already replaced all instances of a particular component, only to find that it was reintroduced by a developer who hadn't been entirely involved in the refactoring process. Although such miscommunications can happen, we were proactive in addressing them.
To effectively address these challenges, we continuously enhanced our coordination strategies and communication channels with the front-end team, while also maintaining a weekly track record of all replacements. Our dedication to collaboration and improvement allowed us to successfully overcome synchronization challenges and ensure positive outcomes for the project.
Conclusion πβ
After many months of dedicated work, we successfully translated 95% of the application and developed and documented a component library that provides greater long-term reliability and customization. This allows our client to effortlessly customize and reuse it for creating new functionalities, while we gained proficiency in a new framework. Successfully upgrading a legacy Vue2 application to leverage the Vue Composition API is a significant undertaking, but the benefits are worth the effort. This achievement reflects our dedication to providing durable solutions for our client. Through careful planning, effective task management and leveraging modern testing tools, we overcame obstacles, resulting in a stronger, more maintainable codebase and improved our team's expertise in Vue.js for future challenges.