Migrating an Angular 1.x app to Vue 2.x

Well, kind of.

As we stated above, we still need to maintain something Angular inside the new Vue codebase.

Simply no options here.

Vuex store, seamlessly shared between Angular and Vue.

We will progressively introduce Vuex as the one source of truth to manage application state.

vue-router.

We would like to introduce client side routing, to facilitate view switching.

A module bundler.

We will make use of ES6+ javascript and modules, a CSS preprocessor, and will bundle our transpiled code to include it in the existing application.

Webpack at rescue here.

So what?A lot to do, so many things to understand and to fit into each other.

“Me and my brother Vue here, We was hitchhikin’ down a long and lonesome road.

All of a sudden, there shined a shiny demon.

”(Tenacious D)ngVue enters here.

“ngVue is an Angular module that allows you to develop/use Vue components in AngularJS applications.

”(ngVue repo)Cool: I am a really bad swimmer, but at least a bridge exists.

I can write a Vue component and include it into the existing Angular application.

That’s a good start.

Angular, Vue, ngVue (and Webpack).

The Three Musketeers!Enlightening the pathThe Alien movie teaches us that the best way to generate a new creature is incubating it from the inside.

To avoid side effects, I would like to preserve things as they are, as much as possible.

I would like to isolate the source code I am going to add, and transpile it in a form I can use into the existing.

So i create a nest for Vue in the form of a new folder, let’s call it vueApp:Ideally the vueApp folder will contain everything related to the migration: Vue code, Vue-Angular temporary hybrid code, Webpack and package.

json configurations, node_modules, and the final "production ready dist" byproduct.

Furthermore, I want to keep Vue and hybrid code separated, to be able to delete no more useful Angular code in the future.

For a similar reason, I create a DEV folder also, which contains mockups or everything useful to webpack-dev-server only.

Adding a bunch of styles assets we then finally come to a development ready directory structure, which, in the end, will be similar to the following:Please note: here I will not initialize the Vue app through vue-cli.

I am reusing a Webpack custom configuration which suites my needs.

Nevertheless, everything should work the same way if you are using vue-cli.

See tag tag-02-app-directory-structure (with empty folders and files).

First things first: setting up Webpack and NPM dependenciesLet’s start by “emulating” an Angular app to re-create an environment to make quick development iterations before injecting the code into the real app.

For sure, this is a contrived example which delineates the way I dealt with my problem: as stated above, in the original Angular app I have got no support from Webpack (or other module bundlers), and ideally I do not want to modify in any way the existing codebase.

By bootstrapping a dev environment with modern tools I can instead quickly write and test new Vue code and Angular-Vue interactions through webpack-dev-server.

Please refer to tag-03-bootstrapping-dev-angular-app for a detailed view of the Webpack config files and NPM dependencies (I am using Webpack 4 here).

Webpack configBefore we start, a few points to note.

Let’s begin with webpack.

config.

js file.

Dev and “library” modeWe will initially build our components inside the DEV folder, taking advantage of our testing environment.

During development hence, the main entry file will be DEV/dev.

index.

js, and the generated javascript will be injected into index.

html page.

When will switch to the real production build, we will build the codebase as a javascript bundle to include in the existing Angular app, exactly as we would include a new library, and the main entry point will then be index.

js.

The production buildHere we are in essence telling Webpack to generate three files in the final build:appVueLib_VendorsDependencies.

js: a file to include all vendors dependencies (like vue, vuex, vue-router…).

appVueLib_NgVueBridge.

js: a bundle which contains the “hybrid” code required to temporary integrate Angular and Vue.

Virtually, once the migration is complete, this code could be completely removed, and the generated file simply will exist no more.

We will work on this folder later.

appVueLib.

js: the “real porting”, the new code completely written in Vue.

Those are the files we will include into the existing old Angular app.

Angular as a global objectThe old app already depends on Angular, which is included as an old script tag.

Hence, to allow the new bundles to access things defined by other javascript on the page, avoid duplication in the build process, and duplication warnings at runtime, we take advantage of Webpack externals.

The angular dependency is supposed to be already present in the consumer's environment.

Again, we will make use of it later on.

package.

jsonIn the package.

json are listed all the NPM dependencies we will use now and later (like ngVue or vuex).

The only thing to note here I will use ES6 to write angular code, so I will take advantage of the babel-plugin-angularjs-annotate to solve dependency injection.

In ES6 code you will find the /** @ngInject */ decorator.

From you terminal, go to the vueApp folder and run install:cd code/vueApp/npm installA new beginning: setting up a dev appAll the pieces are now in place to begin the real work.

Let’s start from the DEV folder.

Create an AngularAppWrapper to host our fake Angular app.

At the end this will be the structure of the DEV folder:We will use ES6 to write the angular component (ES6 syntax could also facilitate a porting from old angular codebase to a complete rewriting in Vue):DEV/AngularAppWrapper/index.

jswhose template is so simple as:And let’s use it into our development Angular app:DEV/dev.

index.

jsNow from you terminal launch:npm run devNice!.A simple Angular app on which experiment with our migration.

Again, refer to the tag-03-bootstrapping-dev-angular-app for everything done so far.

Enters ngVueLet’s now create and use our first Vue-inside-Angular component.

To do that we will ask for help to ngVue (refer to the official ngVue documentation for more info).

I will begin by defining a new Angular module to contain everything related to ngVue.

ngVueBridgeCode/ngVueComponentsModule.

jsWe are simply creating a new Angular module, using, as dependencies, ‘ngVue’ and ‘ngVue.

plugins’ (we will use ngVue plugins later, with vuex, for instance).

Basically, this will be the namespace to contain “angularized” Vue code.

Ok, time to create our first Vue component.

Let’s define a component for a simple app navigation.

Note I am using the vueCode folder here, because I am writing a fresh component completely in Vue, to replace existing Angular code.

Contrarily to DEV and ngVueBridgeCode folders, which will be eventually deleted, the vueCode one contains the real final migration.

vueCode/components/VueAppContainer.

vueNow, if you simply include this new Vue component inside the AngularAppContainer it will be ignored.

DEV/AngularAppContainer/index.

htmlYou have to tell Angular to render this component through ngVue.

Let’s create a file to “transform” Vue components into Angular ones.

With, ngVueDirectives.

js we are telling Angular, through ngVue, that our Vue components exist.

Again, ngVueDirectives.

js is only a temporary bridge file, so we will put it inside ngVueBridgeCode folder.

ngVueBridgeCode/ngVueDirectives.

jsWe are using ngVue’s createVueComponent factory to translate a Vue component into an Angular directive.

As a first step, we inform the main angular module of the existence of our angularized-vue-components, so inside dev.

index.

js replacewithet voilà: your first Vue component inside Angular!Basically, we have just fulfilled requirements #1 and #5: we can write new components in Vue, include them into the existing Angular application, and use modern development and bundling tools.

Back to the real: linking old and new applications“Where we’re going, we don’t need roads.

”(Dr.

Emmett Brown, Back to the Future)But, to say it all, we have to leave our safe development environment, take off, and use the new component inside the real application.

Add to index.

js the dependencies required:vueApp/src/index.

jsgo to your terminal and run:npm run buildWhat you get is a vueApp/dist folder which contains the following files:This is exactly the “lib” we were looking for to enhance our existing Angular application.

In the main index.

html, include those files and use the new Angular directive:code/index.

htmlAnd do not forget to inform Angular a new module for Vue components exists:code/angularApp/angularApp.

jsEt voilà, it simply works:As a reference, see tag-04-vue-component-inside-real-app.

If you are curious, yes, you can also pass props:code/index.

htmlcode/vueApp/src/vueCode/components/VueAppContainer.

vue“You still don’t understand what you’re dealing with, do you?.Perfect organism.

Its structural perfection is matched only by its hostility.

”(Ash, Alien)A simple client routing: Vue global pluginsOne of the reasons we started this journey was to replace the master-detail component in the Angular application.

So far we have seen how easy is to use a Vue component inside Angular.

Let’s now introduce a little bit of client routing through the vue-router module.

This will give us the opportunity to use the $ngVue factory from ngVue.

plugins, and analyze how to define root Vue instance properties.

Let’s start by defining a simple router file.

vueCode/router.

jsvueCode/components/Detail/index.

vue is a simple replacement for the existing detail view.

Then, in the container, empty the main tag and append a router-view component:vueCode/components/VueAppContainer.

vueUsually, in a Vue application, you would pass the store as a property to the root Vue instance.

Something like:But here, in the context of Angular/ngVue this will not work.

We have to use $ngVueProvider at the configuration phase of Angular module to inject the property.

Again, I will configure it in the ngVueComponentsModule, because there lives everything related to ngVue.

ngVueBridgeCode/ngVueComponentsModule.

jsvue-router is now enabled, and you can access it on any child component: we have just fulfilled requirement #4.

What most people don’t understand is that UFOs are on a cosmic tourist route.

That’s why they’re always seen in Arizona, Scotland, and New Mexico.

Another thing to consider is that all three of those destinations are good places to play golf.

So there’s possibly some connection between aliens and golf.

(Alice Cooper)Sharing factories: consuming Angular services from VueActually we still lack one piece: router links.

To add them we will refactor our code a little bit.

Even though we will soon replace it with something Vuex, refactoring routing give us the opportunity to rewrite the existing searchService.

js, and transform it in something both Angular and Vue can consume (and this could be useful in many situations).

Let’s start by rewriting it in ES6 into the ngVueBridgeCode/services, to transform it into something "less Angular".

ngVueBridgeCode/services/searchService.

jsOur service is a plain javascript class.

In the future we will simply import and use it as a ES module in Vue code.

For now, we will share it on Angular and Vue instances thanx to Angular’s providers and the $injector service.

An angular service registers a service constructor, invoked with new, to create the service instance.

It should be used (guess what) when we define the service as a class.

$injector is an Angular service used to retrieve object instances as defined by a provider.

$injector.

get returns the instance of the service.

Exporting an instance of an Angular service allow then us to import and use it anywhere.

“My dear, here we must run as fast as we can, just to stay in place.

And if you wish to go anywhere you must run twice as fast as that.

”(Alice in Wonderland)Add this code to ngVueComponentsModule.

js:ngVueBridgeCode/ngVueComponentsModule.

jsand we are done:we have rewritten the service as a class (previous code snippet)instantiated it as an Angular service (#1)exported the instance through searchService (#2).

A note: to simplify a little bit, I deleted the ngVueDirectives.

js file from ngVueBridge folder, and move the code there directly into ngVueComponentsModule (remove also the import inside vueApp/src/index.

js e vueApp/src/DEV/dev.

index.

js).

Refer to the codebase in tag-05-vue-globals.

Thanx to point 2 above you can safely delete angularApp/services/searchService.

js (and the script tag inside index.

html).

You can leave the existing Angular code untouched, and everything will keep working (remember to npm run build).

Move on and migrate also "detail" and "searchResults" components.

Here, we can barely mimic the existing code with little effort.

vueCode/components/SearchResults/index.

vueThe HTML template is quite the same (the only difference being the use of a routing system).

You can also simply copy and paste the css code from style.

css.

And magic:we are importing and using the searchService previously instantiated.

Basically vueCode/components/Detail/index.

vue works exactly as SearchResults (refer to the repo).

Complete the refactor simplifying the container:vueCode/components/VueAppContainer.

vueadd the component to index.

html, and rebuild:code/index.

htmlWe have just doubled (and almost completely migrated) our dear old Angular code:Starting a search (Angular component) will now activate Vue components.

You can safely delete all the related dead Angular code.

Cool!.We have just migrated to Vue a huge part of our application.

For details, refer to tag-05-vue-globals.

A centralized store: VuexIn some way, we are using the searchService as a sort of centralized store to manage all our search and routing needs.

We can do better, replacing part of or completely remove it, and introduce Vuex as a more advanced, performant, and predictable state manager.

Let's start by adding a basic store file.

vueCode/store.

js“A journey of a thousand miles must begin with the first step.

”(Lao Tzu)One day we will let the store manage everything.

Now we only need one single small step (for a man).

No, we are not sending our application to the Moon (even though sometimes we would like to).

We want to move to the store only the code responsible for retrieving and store search results.

As for vue-router previously, we have to add the store to the global properties.

ngVueBridgeCode/ngVueComponentsModule.

jsBut, wait a minute: and now what?.Angular service and Vuex are separated worlds, how can they communicate?“What we’ve got here is failure to communicate.

”(The Captain, Cool Hand Luke)Well, Vuex is simply a JavaScript object that stores data, isn’t it?I admit I was stumbling on my way to nowhere for a while, desperately searching for a solution, until I ran into it thanx to the suggestion given by a couple of sentences in How to embed Vue.

js & Vuex inside an AngularJS app… wait what?.

“In Angular, there are providers, which are by far the most confusing aspect of Angular.

[…] One of these providers is called a “service”, which can be used to create a single store to reference throughout the app.

All it needs is a function that returns an object.

With a single line of code, I can return Vuex as an Angular service.

”(Jonnie Hallman)Really intriguing!.The solution is criptically dug there (in clear).

Read that, and read it again; lucubrate, my little brain; use the Rosetta Stone to decipher Angular’s documentation for providers.

The keys here are factory and service recipes.

“JavaScript developers often use custom types to write object-oriented code.

”Yes, it’s me.

“The Factory recipe can create a service of any type, whether it be a primitive, object literal, function, or even an instance of a custom type.

”For example:Remember?.From store.

js we are exporting new Vuex.

Store().

And then, all of a sudden: EUREKA!.Add a single line of code.

ngVueBridgeCode/ngVueComponentsModule.

jsBrilliant!.We have just exposed our store instance as an Angular service.

Thanx Jonnie.

To consume it, just inject it into the constructor of searchService.

js, replace code in executeQuery, and use it exactly as you would in Vue:ngVueBridgeCode/services/searchService.

jsCould you see the potential?.You can progressively migrate your services.

And you can use it also inside your dear plain old Angular components.

For example, let’s say we would like to add a results counter in the header:angularApp/components/search.

jsOpinionated tip: If you inject the service renaming it $store you got something very VueThe resultsCount function to me is like simulating a Vue's computed property (ok, just a ton heavier).

But, if you rebuild, launch the application, and start a search.

WTF?.No count!As you know, we are crashing here with the mysterious world of Angular’s digest loop.

We are doing something secretly from Angular.

We explicitly need to call Angular, and inform it something has changed to trigger the digest.

Yes: $apply at rescue here.

“Isn’t it unsafe to travel at night?.It’ll be a lot less safe to stay here… Is there anybody out there?”(Pink Floyd)ngVueBridgeCode/utilities/safeApply.

jsI am wrapping the function in a “safe apply” to avoid possible “$apply already in progress” errors.

But how to use it?. More details

Leave a Reply