Introduction
Our front end at ThousandEyes has evolved a lot over time, and we are currently migrating to Vue.js from AngularJS.
The app started when jQuery was still used as the primary JavaScript helper library. In fact, we still have a JavaScript file living at the root of the project with some code written back in 2011. I think on some level, we like to keep it for posterity.
When AngularJS came along, ThousandEyes went all in, like many companies at the time. It was an amazing framework given the state of the art back then. Its directives, its module and resolution system with controllers, services, factories…
But this was what, 6-7 years ago? An eternity in the world of front end development, right? Since then, ES6 hit us with its import/export system that we can use with Webpack, thus making the module system of AngularJS obsolete. Also, we all figured out that two-way data binding and not enforcing the creation of isolated scopes on components were terrible ideas, pushing us towards more modern UI frameworks like React or Vue.js.
The trouble is, transforming a full AngularJS web app with thousands of components into a Vue.js/JavaScript Native Module app is a daunting task, to say the least. Our app is made of many different kinds of components, ranging from our famous Path Visualization to various types of maps, graphs, tables, tabs, accordions, etc. It handles many different types of data, scenarios for all the different kinds of tests the company offers, as well as for the different products.
Let’s talk about the main challenge that comes with such a migration, that is, how to continuously transition from one framework to another, and the technical solutions we found at ThousandEyes to overcome these challenges, starting with embedding Vue.js code inside our AngularJS app, and finding a way to port our services into Native Modules.
Part 1: The Early Days
For us engineers, the perfect escape plan would have been to start a brand new web app from scratch. We would have loved to, but it was obviously not going to happen: our app is rather big, with many pages, and many different types of UI components, so it would not have been a quick migration. Plus, for a company like ours, there is always the need to provide new value to our customers with new and improved functionality. Hence, we went for a more continuous type of migration.
The first step we took several years back, when the app was still fully AngularJS-based, was to introduce Redux into the mix. We moved most of the state of the app to Redux stores. What this meant was that our AngularJS services/factories were now mostly stateless. Later, when it came to choosing a framework replacement for AngularJS, we narrowed it down to React or Vue.js. Because we were already using Redux, it might have seemed obvious to pick React, but we chose Vue.js instead.
There were two main reasons for this choice. Firstly, because of the more obvious separation between rendering and component logic. While React has a separate render function, nothing prevents us from having business logic in it and JSX manipulation in other parts of the component. We preferred having a stronger safeguard against this, rather than relying on convention. Secondly, the HTML + DSL approach of Vue.js, which is similar to what AngularJS did, is more immediately readable. In React, something like conditional includes/excludes is verbose and often requires splitting up the template into separate assignments. The reader has to reverse engineer DOM templates. And when taking the approach of breaking down components, this forces the reader to understand how all these many micro-components fit together.
Part 2: The New Guy
When we finally introduced Vue.js into our app, we started by integrating ngVue. This library enabled us to use Vue.js components within our AngularJS app for a smooth transition. Our approach from the start was to port things as we went.
A Vue.js component from within an AngularJS directive would look something like:
Not only will ngVue actually load our Vue.js component from within our AngularJS context, but also it will handle the binding between the enclosing AngularJS $scope
and the Vue.js component scope.
Vue.js will do its usual reactivity trick using defineProperty
on propertyOnAngularJsDirectiveScope
(you can read about it in the Vue.js documentation). So when a change is made on the AngularJS $scope
, Vue.js will know about it. But ngVue steps in to echo changes made in Vue.js components to the enclosing AngularJS $scope
. To do that, it will trigger the AngularJS dirty-checking after otherMethodOnAngularJsDirectiveScope
is invoked due to a $emit('my-event')
call made from within myVueComponent
. ngVue can’t help us when otherMethodOnAngularJsDirectiveScope
is raised, though. In this case, a manual AngularJS $scope.$apply
will have to be made from within the body of that method.
As already mentioned, the state of our application was already living in Redux stores. So we had to come up with a bit of a custom solution for state changes to pop up in our Vue.js components. So when initializing our Redux stores, we ran them through a little function that passes the state to a Vue.js component, rendering it reactive.
import Vue from 'vue'; export function enableVueReactivity(store) { const originalGetState = store.getState; const vm = new Vue({ data: () => ({ state: originalGetState() }) }); store.getState = () => vm.state; store.subscribe(() => { vm.state = originalGetState(); }); return store; }
At this point, to be able to access our existing AngularJS services from within Vue.js components, we had come up with a custom hook that injected AngularJS services into Vue.js components. This allowed us to write new Vue.js components and start porting AngularJS directives into Vue.js as needed.
Part 3: The Slow Down
The big caveat with ngVue is that while you can declare a Vue.js component inside an AngularJS directive, you can’t have it the other way around. This means that any component you need inside your freshly ported Vue.js component will need to be ported to Vue.js first.
This certainly slowed us down at first, when some of our heavily-used components, like our dropdown selector, were not yet ported to Vue.js. These cases were in-house, complex and very much tied to AngularJS’ inner-workings, and so they took a good amount of work to remake in Vue.js.
We also had to come up with a solution to port our AngularJS services into ES6 native modules.
Obviously, any new, independent module could be written in pure JavaScript and nicely imported into any Vue.js component. But what about when one of these new modules requires access to an existing AngularJS service? Well, simple, all you have to do is port that dependency into native module, as well. Except it’s not that easy. Our dependency trees are intricate and require planning and a deep understanding of the bigger picture. Any native module we would need for any kind of task, from small to big, would require hours of porting AngularJS services. This also comes with risks when updating shared services that affect code that isn’t necessarily well tested and in functionality not widely used.
There was another problem that kept us locked up: at the leaves of those dependency trees, we always ended up with AngularJS components like $http
, $timeout
and $q
. I’ll spare you the gritty details about how AngularJS works (you’re welcome!), but if you’ve never used AngularJS, these services wrap external events like http requests, click events and timeouts and ensure that the AngularJS “reactivity” works, triggering the dirty-checking. So as long as there are still AngularJS components in the app, we can’t just replace our $http
calls with fetch or axios calls, for instance. In the end, we were still often forced to write new AngularJS components. And every time it happened, a little part of us died.
Part 4: A Little Push
To help move things along, we came up with the ‘ngInjector’. The concept is simple: we define a native module in our app that can be imported from anywhere (another native modules, Vue.js components), and that defines an AngularJS module with a ‘run’ block, capturing a reference to AngularJS’ $injector
, which will give us access to AngularJS services:
let ngInjector; angular.module('te.ngInjector', []) .run(function ($injector) { ngInjector = $injector; }); export default function get(moduleName) { return ngInjector.get(moduleName); }
We just had to make sure the file was loaded within the context of our AngularJS app, and we were good to go, with one small caveat: we can’t access an AngularJS service at the root of a native module:
import ngInjector from './ngInjector'; const $http = ngInjector('$http'); // Error export default function makeSomeNetworkCall(url, postData) { return $http.post(url, postData); }
At this point in the life cycle of AngularJS, the angular.module.run
function hasn’t been called yet, so we haven’t yet gotten a reference to the $injector
. Instead, we used the following:
import ngInjector from './ngInjector'; export default function makeSomeNetworkCall(url, postData) { return ngInjector('$http').post(url, postData); }
With this new tool in our kit, there was nothing left preventing us from migrating old AngularJS services to native modules or creating new native modules with dependencies to old code. We were then able to pick up momentum. At first, it helped dramatically reduce the number of new AngularJS components per sprint. Before long, the overall number of AngularJS components was steadily decreasing. And it just got easier from there; the more code we had migrated, the simpler the migration was. It’s a positive feedback loop.
Large parts of the app are now implemented in Vue.js/native modules with many of the older components ported over. Working in such an environment makes upgrading existing functionalities and creating new ones faster, with less effort.
Conclusion
Little by little, we removed all blockers on our way out from AngularJS. All our major shared components are now in Vue.js, we write Vue.js components and native modules for new development, and we migrate old code. Life is great!
Now I’m not gonna lie, we still have a few AngularJS services and directives, but we are actively migrating them to Vue.js/native modules, and we’re getting there. Additionally, we’ve started breaking down our app into micro-front ends, so the future is bright and exciting at ThousandEyes. We like to be efficient and effective at what we do, to deliver maximum value.
Thanks for reading this. Stay tuned for my next article, 2-3 years from now: “Prison break: Escaping Vue.js”!