A long but worthy journey!
We own a product which is a constantly growing web application, with data intensive visualisations, a considerable amount of screens, complex components and a lot of interactions between these components. In short, a JS Beast! We implement new features every months, release and deploy new versions every week, and provide intensive support (customers can reach our support team directly by mail/phone in case of malfunction).
Basically, every time a new feature is written, the code starts clean, as change requests pop up, for design and/or behavioural purposes, little by little, without being able to afford a proper refactor at each development iteration, it’s easy to end up with code containing more and more patches, hacks, and things that don’t fully belong to where they are (I believe I’m not the only one with this feeling!).
Even if theoretically, the ideal solution is to divide everything into components, it’s harder to know “what goes where” when the specs are evolving and moving every week, and we don’t have time to refactor everything we would want to.
While our AngularJS code was full of (useful but tricky) hacks, VueJS enforced good code practices, and seemed more robust in the long term. Especially when naughty developers (like me) maintain the projects through numerous feature evolutions, and are sometimes tempted to go for the fast way of implementing things instead of the proper and clean way to do it.
While VueJS definitely looked like a code and performance improvement, it also brought a fair amount of communication problems between components which were previously solved “the hacky way”. Happily, it also came with a solution to these problems: Vuex.
The dev team had heard of VueJS, and was able to try it on tiny projects. So after a few benchmarks, it was quite obvious that VueJS was definitely more efficient than AngularJS for the data intensive views of our product, so the will to switch to a newer, better, faster, stronger, framework was here!
Beside the hype of the new technology’s efficiency (through reactiveness), the main point is that, in comparison to angular, which is flexible, and which we can write in many different ways, VueJS, even having it’s fair amount of different coding styles, forces the developer to do things “the good way”.
The real challenge, as with every big migration project, was driving the upgrade on the whole application, without breaking changes or regressions, whilst continuing to implement new features and weekly releases.
- From Scratch: Start over and rewrite everything! (Obviously WRONG!)
- Horizontal migration: Replace code component by component.
- Vertical migration: Replace code page by page.
Preparing for the migration
The “Horizontal migration” was the path we choose to start with, and the first step was to be “Webpack compliant”. So we started with a small syntax refactor to ensure that all our angular controllers, services, and directives, were using the same consistent syntax (ES6 classes for AngularJS objects). Then, once the angular application was fully exportable as a collection of ES modules, we removed Grunt in favor of Webpack in order to prepare the build system to allow AngularJS and VueJS to live together in the same build environment.
We started to switch our low level components (in-house libraries) to VueJS, one by one, to have a set of VueJS components ready to be integrated in our AngularJS application.
Next we tried using ng-vue, but it didn’t really work for our complex application (too many interactions between the AngularJS parent and VueJS children). So instead we used a very simple in-house wrapper, in order to wrap all of these new VueJS components inside angularJS higher level components (angularJS wrapper for VueJS).
We Also wrote a node script to statically analyse the code at every deployment, and to keep track of what had been updated, as well as monitoring the migration process with percentages and diagrams (for the joy of management).
Horizontal Migration process (component by component)
At this moment, we were confident that everything was ready to safely and peacefully migrate the product one component at a time. Truth is, we were wrong!
Unfortunately, after the integration of a few basic components, we quickly saw the limits of this process.
The main issues were:
Component division: Usually, rewriting would allow us to clean the code, alter the number of components, or refactor code. But here we would have to keep the component structures, and logic separation.
Events: In angular, we were using events in both ways ($emit and $broadcast) in order to communicate through the whole tree of components. But to send events from AngularJS to VueJS and vice versa was tricky, even more so when many levels of component nesting were involved. And using JQuery events was not a good solution, as it would then mean rewriting twice (from AngularJS to JQuery now, and one day, from JQuery to VueJS).
States: in AngularJS, we were communicating most states through events, and storing them in the $rootscope (I know, right!), but as we were migrating to VueJS, we wanted to use Vuex. Making both of them coexist didn’t sound right.
Transclusions: Nested components with inter-framework "transclusions" (VueJS in ng-transclude or AngularJS in a slot) are not migration friendly, as we had to start migrating from the child component, and parenting may diverge from one page to another (A>B>C & B>A>C).
Performance: Having both AngularJS and VueJS running together, as well as pulling all components twice on the same page is clearly not optimal.
Procrastination: Once all components would have been merged, we would still have the AngularJS router, main pages, and all services/factories to migrate. This would be a long and complex process, postponed at the end of the migration, and this would be an open door to regressions without any easy fix.
So the hype of the new framework gave way to: “How are we going to migrate without loosing all our hair?!”. Along side this, we had high priority new features to implement at the moment, and for the sake of implementing them fast, we did them using AngularJS. So that didn’t help to keep us confident about the way we were doing things, and eventually made us think about using another approach to migration.
Vertical Migration process (page by page)
We came up with another idea to migrate. We were afraid of the “page by page” migration because it would require two routers to coexist, and bring long migration steps on complex pages.
But what if we could host two distinct single page applications, respectively for AngularJS and VueJS? Then no cohabitation would be needed! Just good links redirecting from and to the new version! If that looked hacky at the beginning, the more we were thinking about it, the cleaner the picture was.
This would bring:
Building from scratch: We would have all the advantages of starting a new project. We could choose the file structure, rethink the component division, the communication process, the state storage, and use VueJS best practices from the beginning, enjoying a fresh start without having to make any compromises.
Easy roll backs: As long as we keep similar URLs for each page of the application, it would allow us to convert existing pages one by one, test it properly inside the product (by just adding a /v2/ in the URL), and release it when we were sure it wouldn’t contain any regressions, and in the unfortunate case someone would encounter an issue with the new version, they would just be one link away from the old page, and the legacy behaviour.
Convenience for new features: Having the ability to write VueJS for new pages without waiting for everything to be fully merged. As we would have a clean new project, adding a page to it would be much easier than trying to integrate new features in existing pages.
Easy cleaning: One of my fears of migrations is forgetting about tiny things that will stay even if they lost their purpose (like this CSS lines left alone by long time gone DOM elements!). By starting a clean project in parallel, we would avoid the need of a big clean up at the end!
Anticipation: instead of keeping all the complex issues (authentication/permissions, page routing, building process, …) for the end of the migration, let’s get rid of all complex problem first, and only have good and easy coding tasks to deal with in the future. That sounded reasonable!
No co-existence: Last but not least, not having to load two libraries, and not having two different ways of dynamically updating the content in a single page! Because everyone agrees that one JS framework per page is already enough (if not too much)!
To make this possible, we were just a few steps away:
We started by creating a new clean VueJS project containing everything we dreamed of (Vuex, building using Vue-cli, Jest tests, ...). We hosted it in it’s own Docker image, running on a parallel url on the same domain, and handled proper redirection via Traefik.
Then implementing the navigation bar was the first thing to do (as it was our only application level component, common to every page). It had to be pixel perfect and identical to the old one, so we could switch from one version to the other without the user seeing any changes.
Finally, we wanted to handle authentication and permissions on the new VueJS router, sharing the same token as the legacy authentication system, so that both SPAs would secured and seamlessly accessible to the logged user.
After these three steps, the hype of writing new code using a new framework was finally back! With more certainty that our choice was the good one and that we would one day (not just yet) see a full VueJS version, which after all, was the objective of this journey!
Conclusion so far
After a few basic pages rewritten using full VueJS, it seemed that we made the right choice! The integration is much smoother than the first attempt using the “by component” approach. We will let you know how it went in a few months! See you in the next article!