Incrementally migrating a legacy jQuery web app to a Vue.js SPA

By Ricardo Ferreira

Our engineering team had in the past years a huge challenge to maintain a more than 6 years old application, based on PHP templates/modules being loaded by jQuery into the DOM. Scaling the application was becoming a nightmare and we were having a hard time maintaining and adding more features to it. This was having an impact on the team’s productivity and the value delivered to the client. Also, we were in need to modernize the application in terms of architecture, visual style, and user experience.

Image for post

Several legacy application showing some disruption between styles

Infraspeak’s core application is business-critical and thousands of users rely on it for their daily operations. We required a solution to deliver faster and better, but at the same time, we could not stop delivering features and value to the client. There was a lot of business logic existing on the legacy jQuery application that would take a long time to replicate in a brand new application.

So, we needed a solution that allowed us to migrate the legacy jQuery application into a whole new development stack, incrementally. New modules should be developed on the new application and bug fixes on the legacy application. The idea is to incrementally migrate the legacy modules into the new stack, but still delivering customer value.

We should have both applications alive each with its URL to create a user experience that allows a seamless transition between both applications. It was also very important to keep IE11 compatibility as some of our users are still using it.

The solution we found brought us the opportunity to create a whole new development stack built on top of Vue.js with a highly scalable architecture, unit/integration tests, storybooks for documenting components, new documentation, CI, etc. This was also a key factor for team motivation, output, creativity and hiring. Also, the Backend team took the opportunity to update our API’s to reflect new, more up-to-date standards which are consumed only by the new application.

Our first solution seemed a bit awkward for us at first, but the more we talked about it, the more it made sense… the idea of using IFrames was a bit out-of-fashion and kind of odd but still came to our rescue. Using this approach we could split the migration into two phases.

In the first phase, the new application was rendered inside the legacy application. In the second phase, we render the legacy application inside the new application. This way we could test and refine the new application structure while validating the IFrame solution.

Image for post

Main structure of our new web application (Second phase).
IFrame is what makes possible to see legacy application inside the new application.

First phase — implementation in legacy application

The first phase consisted of adding an IFrame module to the legacy application to load modules from the new application. The main goal was to buy time to build the core modules of the new application while still delivering value to the client on the legacy application. With this in mind, it was possible to build all new features in the new application.

To accomplish this, we needed to:

  • Create a new application with Vue.js.
  • Support a URL query parameter that hides all headers and menu components on the new application. This will allow us to show only the main content of the module to be rendered in the IFrame.

Note: We could have added routes on the new application that only render the module screen instead.

  • Develop the new modules in the new application.
  • Add a new PHP module on the legacy app which renders an IFrame. Inside the IFrame, we show the module previously developed using the URL query parameter to hide all headers and menu components.

Here’s what the PHP module should render on the page:

Image for post

Both apps should be on the same domain, otherwise you might have CORS errors.

And this was the final result:

Image for post

Legacy application rendering the new application inside an IFrame

Second phase — implementation of the new application

All the initial architecture of the new application took us a while to be built. It was required to have the three core modules completely migrated to the new application. The remaining ones should be loaded using an IFrame to the legacy application (the opposite of what was done in the first phase).

To achieve this, we had to:

  • Add a unique URL to all legacy modules that should be rendered on the new application. This way, we could map the route on the new application to a distinct page on the legacy application.
  • Add the URL query parameter on the legacy application to hide its headers and menus to show only the main section of the page.
  • Include some styles when the URL query parameter is present to make the legacy application’s style to better match the new application’s style.

Image for post

  • Communicate to the new application that the legacy application was rendered and ready to be used.

Image for post

Now, the legacy application is ready to be rendered inside the IFrame!

  • Map each route to the legacy application and render the component created in the next step:

Image for post

We also added a feature toggle in the code above to activate/deactivate the legacy screen for each module of the new application.

  • Create inside new application a component to render the legacy application modules (ScreenLegacyApplication.vue).

Image for post

Looking at the code above, we have a loading div that, while legacy application is not ready, it shows a loading animation.

We have a switch case that allows us to map between the new application URL to the respective URL on the legacy application.

When we change between two routes that render the legacy screen component the Vue framework reuses the screen component. That caused an issue when switching between two legacy screens. The easy fix was to watch over $route and force a recalculation of the URL.

We could have used beforeRouteUpdate method of vue-router to force recalculation of the URL.

That brought us another issue because if the only thing changing on the IFrame URL is in front of the hashbang, then the IFrame does not re-render the legacy application. To fix this, we added a ‘counter’ variable that is incremented and appended to the URL of the legacy application before the hashbang.

This was the final result of the new application:

Image for post

New application rendering a new module
Image for post

New application rendering a legacy module with IFrame (with stylesheet updated)

Conclusion

Migrating a large-scale legacy application to a brand new one can be a very risky and time-consuming task. The approach we took allowed us to incrementally migrate our codebase to a brand-new application build on top of a very solid architecture that allowed us to deliver better and faster value to the client.

We still have a lot of legacy modules to be migrated, but we can now do it without affecting application stability or deployment cycles.

Also, having the legacy application alive, enabled us to have a quick rollback plan in case of disaster in the new application which is critical for applications that are used in people’s daily operations.

We never thought to ever say this before, but for us, “IFrame rules”!