Upgrade to AngularJs 1.6
When we started developing our load testing solution we had to choose a technology to create the UI. Most of us had previous experiences with GWT or Vaadin but we were not satisfied with it. It took us too much effort to create a sketch of the application and it didn't even look good.
So, we gave a try to AngularJS, even though none of us knew a bit of JavaScript. It felt really productive and a few weeks later we had a nice first version of OctoPerf that could start performance tests and display reports.
Three years later we added lots of features to our load testing solution and the code base reaches almost 10K lines of JS. In the meantime, Angular2 came out and AngularJS evolved to close the gap.
So, during the past months we took some time to upgrade our frontend to AngularJS 1.6 and to prepare the ground for Angular2:
- We switched from Controllers to Components,
- We replaced Grunt / Bower by Webpack,
- We convert all our JavaScript files to TypeScript.
From Controllers to Components¶
That is probably the longest task of the upgrade as it can’t be automated in any way.
A good code quality is mandatory for a fast and painless migration to AngularJS 1.6:
- Each controller had only one responsibility: we were already using the Controller as syntax and used to associate each controller to its dedicated HTML file.
- We also have a code coverage of about 90% using Karma and Jasmine unit tests.
It was really helpful when converting the majority of our controllers. We just had to replace the controller name by $ctrl in the HTML, rewrite the controller using the component syntax, and check that the unit tests were OK.
Get rid of the $scope¶
It became problematic where we injected the $scope
service.
Use services instead¶
For example, we used it to communicate across controllers via $event
and $on
, or to share data in the tree of controllers that displays Virtual Users.
To avoid the $scope
, we created services that hold the shared information and handle the communication between components.
Whereas in Angular2 you can benefit from the multiple injectors, in AngularJS all services are singletons (only one global injector). So, when we need two instances of a same service, we have to inject it using two different names.
We ended up using factories of services to prevent us from code duplication. Not great!
Deep watching and $doCheck()
¶
We also used $scope
to $watch
for data changes in a few places.
Most of them are replaced by component bindings and the $onChanges(changesObj)
listener.
However deep watching can only be replaced by the $doCheck()
method.
It is called on every AngularJS digest cycle.
This required a bit more elbow grease:
The automatic change detection of complex objects is to be done manually, by comparing the current object value to its previous version (angular.copy
and angular.equals
came handy for this).
I'm glad that we only used this in two places in the whole application.
ctrl.$doCheck = function() {
if (!angular.equals(ctrl.previousModel, ctrl.model)) {
ctrl.debounceUpdate();
}
ctrl.previousModel = angular.copy(ctrl.model);
};
CSS issues¶
One last minor issue with components is that they always come with a homonym HTML tag element:
you can’t use replace = true
or use a tag attribute like you would with directives.
This can lead to CSS issues (in the cases where you apply styles to children elements for example), and imply some refactoring there too.
Cleaner, Faster, Better¶
We used the $scope
service in trees of objects to share data and states across children and their parents.
Even though our unit tests served as a documentation, the code was not clear at first sight.
Components feel really cleaner than controllers:
- Component definitions clearly show the inputs (bindings with '<') and raised events (bindings with '&') for each object.
- In the few places where we use direct communication between components it's also visible by the
requires
configuration. - The whole code base is better structured, and it eases code splitting and object reusability.
This first step towards Angular2 is worth doing on its own!
From Grunt to WebPack¶
That's the biggest, hardest, nastiest part of the upgrade! Unlike migrating from controllers to components, here you have to do it all at once.
And WebPack is really a different way of thinking compared to the old Grunt + Bower stack.
Grunt was simple in the sense that you configured it to inject references to your JavaScript and resources (CSS and other assets) into your index.html
file.
That's a very classic build as it matches how you would do it by hand.
We used Bower to list and inject all the project dependencies but also NPM for the development ones.
WebPack does it all, at the price of ease of learning. Indeed, the learning curve is much stepper.
To simplify its functioning, it starts from an entry point (a JavaScript file) then lists all dependencies and creates a single compacted uber JS file.
It also uses loaders to parse resources (CSS, Less, Scss, images, HTML templates or anything else you could need) and a plugin to generate the index.html
required for your Single Page Application.
The configuration may be a bit tricky, for example, why the hell does it parses the loaders bottom first?
But there is a good community, most of it is documented, and it comes with all the features, plugins and loaders you may need:
- Multiple entry points to split the generated JS files,
- Code minification and uglification,
- Removal of dead code,
- Etc.
The require hell¶
To create a tree of file dependencies, WebPack uses the keyword require
(works also with import
).
In order for a file to be packaged in the resulting uber JS, it must be required by another one, that itself is required by another one, [...], that itself is required by the configured entry point.
To give you an AngularJS related example, instead of writing templateUrl: 'path/to/my-component.html'
you need to write template: require('./my-component.html')
in your components definitions to tell WebPack to include your HTML templates.
This case is fine, but handling dependency injection is not that easy because AngularJS references dependencies by their name.
A simple string that you can’t require!
The first ingredient for a good WebPack/AngularJS cake is a load of modules.
Don't be shy and use them everywhere. We opted for the one directory one module rule, and for declaring every module in a index.js
file.
Then you just need two things to have all your modules included:
- Each module must export its name,
- Instead of referencing module names, you require them.
Example of index.js module declaration file:
'use strict';
module.exports = angular.module('app.home', [
require('./about'),
require('./blog'),
require('Shared/common')
]).name;
//@@loadAll
It also requires a 'Shared/common' module. You can configure WebPack to avoid relative path when you require modules far from each other:
WebPack configuration:
resolve: {
root: __dirname,
extensions: ['', '.ts', '.js', '.json'],
alias: {
Shared: path.resolve("./src/app/shared"),
Vendor: path.resolve("./src/app/shared/vendor/dev"),
Components: path.resolve("./src/app/components")
}
},
Finally, you need to have each module require its Components, Factories and Services. This is done via a simple code snippet (to be appended at the end of each module definition):
var load = require.context('./', false, /^((?!\.spec\.(js|ts)).)*.(js|ts)$/);
load.keys().forEach(load);
To avoid code duplication, you can use a WebPack loader to replace a String of your choice by this snippet:
WebPack configuration:
{
test: /index\.js$/,
loader: 'string-replace',
query: {
search: '//@@loadAll',
replace: "var load = require.context('./', false, /^((?!\.spec\.(js|ts)).)*.(js|ts)$/);load.keys().forEach(load);"
}
},
The external dependencies nightmare¶
That worked pretty good for our core codebase. However, we use many external dependencies for OctoPerf. Some of them were a pain in the ass to require!
For example, both CodeMirror and TextAngular rely on global variables (window) to start. And making such libraries work was a real challenge.
require('codemirror/lib/codemirror.css');
require('codemirror/theme/eclipse.css');
// declare global: diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL
(window as any).diff_match_patch = require('diff-match-patch');
(window as any).DIFF_DELETE = -1;
(window as any).DIFF_INSERT = 1;
(window as any).DIFF_EQUAL = 0;
const CodeMirror: any = (window as any).CodeMirror = require('codemirror');
require('codemirror/addon/merge/merge.css');
require('imports-loader?CodeMirror=codemirror!codemirror/addon/merge/merge.js');
require('codemirror/mode/xml/xml.js');
[...]
require('codemirror/addon/dialog/dialog.css');
require('angular-ui-codemirror');
require('./code_mirror.less');
Also, you have to require Css and external JS file from your own JavaScript code.
Not from configuration files like package.json
or bower.json
, but from your own code.
And I must admit it feels a bit weird, to say the least.
Painful but useful¶
Once your penance is fulfilled you will benefit from:
- A faster application feeling: all files (HTML and other resources included) are packaged into a single JS, there are no intermediary loading times.
- A better update support on CDNs: CloudFront had a hard time invalidating our multiple files with the Grunt + Bower build. It’s smooth with WebPack and we don't experience incoherent states of the application with only half of the resources updated any more.
- Fast compilation times, even with uglyfication / minification activate for production build. During development, the refresh of the application is really quick: it takes longer for my browser to reload than for WebPack to update the JS (the whole refresh takes about 5-6 seconds after each modification).
Source mapping is still available to debug the application. But we really prefer writing unit tests than debugging at OctoPerf! ;)
From JavaScript to TypeScript¶
WebPack comes with a TS loader so once you migrated your build, enabling TypeScript is as quick as adding one single configuration line:
{test: /\.ts(x?)$/, loader: 'ts-loader'},
Much simpler and pleasant than Webpack, the TypeScript migration is really cool. Having typed objects and classes make our code easier to understand and help us identity potential bugs during compilation.
The migration can be done very softly. Each component and module can be rewritten separately, using TypeScript Classes.
Components declarations¶
We also enjoyed the capabilities of TypeScript annotations to simplify Components declarations (and to make another step towards Angular2).
For example, the following annotation...
export function Component(module: angular.IModule, selector: string, options: angular.IComponentOptions): Function {
return (componentController: any) => {
module.component(selector, angular.extend(options, {controller: componentController}));
};
}
import {Component} from 'Tools/annotations';
import {AppComponentsCorrelationListModule} from './';
import {SaveService} from 'Shared/save/save.service';
import {SaveServiceStatus} from 'Shared/save/save_service_status';
import {SaveStatusListener} from 'src/app/shared/save/save_status_listener';
@Component(AppComponentsCorrelationListModule, 'applyRules', {
template: require('./apply_rules.html'),
bindings: {
correlationRules: '<',
onClick: '&'
}
})
export class ApplyRulesController implements SaveStatusListener {
public disabled: boolean = true;
/*@ngInject*/
constructor(private SaveService: SaveService) {
}
public $onInit(): void {
this.SaveService.registerStatus(this);
this.updateDisabled();
}
[...]
}
Note:
The
/*@ngInject*/
comment from the ngAnnotate library is used to automatically write the injection names array (this.$inject = [‘dependecyStringName’];
). It's a must have!
It feels like writing Java code! The only minor trouble is that our IDE (WebStorm) does not understand our annotations and then stop doing auto-completion of component names in the HTML files.
Factories¶
The same kind of annotation can be used for Services, but not for Factories, Run or Config. Indeed AngularJS requires functions for theses, and you cannot annotate functions in TypeScript.
The solution we found is to create classes with only one single static method:
@Run(AuthModule)
export class AuthenticationRun {
/*@ngInject*/
public static run($state: IStateService,): void {
[...]
}
}
And use the following annotation:
export function Run(module: angular.IModule): Function {
return (run: any) => {
module.run(run.run);
};
}
When all our code will be written in TypeScript some AngularJS specific objects won’t be needed any more:
- Factories can be written using the simple factory design pattern (though dependency injection might be kept to ease unit testing!),
- Constants can be replaced by TypeScript enums and types,
- Etc.
index.ts¶
Our module declaration files can also be converted to TypeScript. Instead of exporting the module name we can export it whole:
import {AppComponentsCorrelationListModule} from './list';
export const AppComponentsCorrelationModule: angular.IModule = angular.module('app.components.correlation', [
AppComponentsCorrelationListModule.name
]);
//@@loadAll
Conclusion¶
The upgrade to AngularJS 1.6 is really a big improvement for code quality and maintainability.
It’s also mandatory to ease the upgrade of our project to Angular2. We could even include Angular2 components using the Upgrade Module We still need to finish converting all our code base to TypeScript before going further along this path. And there are still some libraries that prevent us to do so. For example ui-grid, ui-tree and uib-modals still need the $scope
service or use controllers.
In any case unit tests, modules splitting, and single responsibility controllers were a huge advantage to conduct this upgrade.
Links¶
Sample projects:
Blog post about it: