written by Andre Liem
07/26/2017

Tips for integrating VueJs into a legacy site

TL;DR
I've been in the process of adding VueJs to two existing sites for the past year, one using MarionetteJs + BackboneJS and the other using vanilla Javascript + jQuery. Here are some tips that have made the process of bringing in VueJs less painful.

Since VueJs came out, I was eager to use it on all my existing projects as I had up to that point been using the Marionette JS framework or basic Javascript libraries with jQuery.

Fortunately, two projects I develop on had opportunities to bring in VueJs to make the migration process happen.

One site, lets call it Site A, required a new administration CMS that was basically independant from the public facing site. This was perfect, I could isolate all of this code on its own. Site A had the following stack:

  • jQuery
  • Marionette Js
  • Marionette Routing + SPA
  • A REST API serving the entire site

The other site, Site B, was undergoing a lot of mini features updates. Not necessarily a perfect opportunity, but one feature required a lot of interactivity so I figured isolating this feature with VueJs would work well. Site B was not quite as modern as Site A though, here's how its stack looked:

  • jQuery
  • Regular Javascript files
  • No API
  • Templates using server side HTML (Laravel blades)

Why Migrate?

For Site A, MarionetteJs is a pretty good framework and at the time of its creation was amazing. It just felt that it was starting to show its age. I was finding a few things a bit painful using MarionetteJs.

  • The release I was stuck on did not support browserify. (No HMR)
  • Stuck on a quirky module pattern and requirejs.
  • No Webpack option
  • Community support is decreasing.
  • Tendency to write verbose code (very imperative)
  • Backbone Js Models hardly support nested data.

These are subjective/debatable points, but for me it was clear it was time to move on when I built my first VueJs demo app integrated with Vuex. The developer experience was completely different. I coded faster with Vue, it was easier to understand, and things stayed DRY with less effort.

Perhaps the biggest revolution was Vuex. After building a few medium sized apps with Marionette Js I really understood why an architecture like Vuex/Redux was conceived.

For Site B, the choice was a lot clearer on the front end as we did not have any existing Javascript framework in the works. In addition, VueJs doesn't necessarily need a RESTful API in the way that a framework like MarionetteJs does... so there really was no reason not to give it a shot.

The Process

Okay, moving onto the actual tips. Here are some bite sized tips that helped make the process easier. If you're thinking of migrating a site over, I hope some of these can help you out.

Tip - Getting started with your foot in the door

For Site B, I needed to wait for the right feature request to get my foot in the door and easily justify the move. I couldn't just spinkle it onto a small piece of the site as the overhead costs of setting up VueJs would not pay off.

Fortunately, we had a new feature request to build an interactive Twitter like feed. Doing a bit of rough estimation, I knew that VueJs was the right choice because handling all of this state and events would be quite challenging using Vanilla Js. This took care of the first step, getting VueJs setup. From here, I ended up adding VueJs to two more significant sections of the site as the benefits became clear as we progressed through the first feature. If I had proposed re-writting all three features at once, it would be a much harder sell.

For Site A, the entire admin piece justified the new path as it had a lot of complexity on the UI and there was very little code that would be reused from MarionetteJs.

Tip - One VueJs App per route

For Site B, there was no SPA as all pages were doing full page refreshes. This was fine and is okay for the foreseeable future. The challenge is how to patch this code together so that you can have VueJs loading on some pages and regular legacy code on the rest. As of this post, we have 3 sections on the site which run as full VueJs apps, and the rest do not. To deal with this I separated each section into 1 VueJs app.

Here's an example of how the entry into the VueJs app works

main.js

<script type="text/javascript">
import Vue from 'vue'
import * as DashboardApp from './Dashboard.vue'
import * as UserManagerApp from './UserManager.vue'
import * as AccountsApp from './Accounts.vue'

const appMapper = {
  'page.dashboard' : DashboardApp,
  'page.user' : UserManagerApp,
  'page.accounts' : AccountsApp,
};

const app = appMapper[App.CurrentRoute]

new Vue({
  store,
  el: 'app',
  components: { app },
  render: h => h(app)
})
</script>

In this example, there are 3 VueJs apps which do different things. In order to load the correct one, we pass in a global value App.CurrentRoute which tells us which app to initiate.

This value is defined in a general layout file via a Laravel blade like so.

<script type="text/javascript">
    var App = App || {};
    App.CurrentRoute = '{{  Route::getCurrentRoute()->getName() }}';
</script>

Tip - Preload shared data

For both sites, we still relied on some global Javascript variables that is loaded on the initial page load to avoid making lots of independant API calls. Ignoring the question of whether this is good practice or not :) , assume your site has some global data you need to share across your new and old code base.

Following the same example as above, here is how we pulled in the data into Vuejs without having to change existing legacy JS. Take note of the created method.

<script type="text/babel">
new Vue({
  store,
  el: 'app',
  components: { app },
  render: h => h(app),

  created () {
    // Set preloaded data into store modules
    this.$store.dispatch('setAuthenticatedUser', App.CurrUser)

    this.$store.dispatch('setSettings', {
      'keys' : App.Keys,
      'urls' : App.Urls
    })
  }
})
</script>

Within the created method, all we are doing is dispatching some Vuex requests to set initial data. In this case, there's an authenticated user profile and some general site setting info.

While you could simply acccess a value in your VueJs component like App.Keys.Google, this approach exposes your components to the Global namespace.

Instead, you can now rely on a getter to get data as needed:

e.g. - this.$store.getters.googleKey

This way you have seperated out the legacy code from most of your components and left that bridging work to your initial root component.

Tip - Beware of shared data with VueJs

Following the previous tip, you do need to be careful as VueJs will modify the object when it creates reactive objects (getter/setters).

To avoid the problem of VueJs changing your Global data you could do this:

this.$store.dispatch('setAuthenticatedUser', $.extend(true, {}, App.CurrUser);

In this code example we are using the jQuery extend function to clone App.CurrUser ensuring we have an independant copy that will not modify the original object. You could use lodash or underscore js instead if you prefer.

You only need to do this if your code will do real route transitions between VueJs and your legacy code. If your app does a refresh between URLs and runs through the entire setup process the Javascript from the legacy side and VueJs shouldn't touch either.

Tip - It's not all or nothing! e.g.- window.location.replace instead of routes is okay

Following the previous tip, for Site A I struggled in finding a bridge between the Marionette router and Vue Js's routers. That is, how could I have a user navigating from the VueJs app to Marionette to trigger the Marionette route and also hide or dispose of the VueJs app?

I thought about using the Global Javascript variables as a way of communicating between the two sides, listening for changes and then triggering the routes as needed. Basically creating some event listener pattern between the two apps. This ended up becoming convoluted and not a path I was really looking to go down given time it could take.

In the end, instead of finding a way to actually integrate the apps more I opted for the most basic solution and simply redirect the user!

window.location.replace = '...'

Far from elegant it's one area I'm still looking at fixing, but it hasn't stopped progress on making each site use more and more VueJs.

If you have any tips in this regard please let me know. I thought it would be important to highlight that any switch or migration to another technology is likely to encounter a mix of clean and hacky solutions. As long as the end user isn't affected too much by it, I believe the solution should be on the table. For now I call this code debt that may or may not be resolved. If both projects go all VueJs, which is the end goal, I won't have to deal with this at all! :)

Wrapping Up

If you are thinking of migrating an existing legacy site to VueJs, or in the process, I would be interested in hearing your feedback. Any tips to share?