Porting AngularJS 1.x to ES6
We maintain a rather large site (around 200 files) built in ES5 using an early version of AngularJS. We decided the risk of a full site rewrite was too large. Given how complex a full site port is we approximated a 6–8 month ramping period to gradually introduce a code style that is inline with the ES6 standard.
Introducing a code style
We introduced the John Papa style guide to any new feature development. This took time but as we continued to refactor and write new code, less of the code we maintained was the ‘old’ style.
Note we had to leave the ‘Application’ variable in app.js alone until the entire site was ready for ES6 due to our dependency of the global name spacing. Most of our files had Application.controller() in them.
This is looking more in line with how we expect to see an ES6 module.
The important part to remember is all the module formats can be interchanged pretty easily through a transformation.
Because the browser doesn’t understand any of the module formats we need a module loader to interpolate the command:
import $ from ‘jQuery’;
We decided to use SystemJS as a module format and loader in the browser. SystemJS is pretty light weight and provides, out of the box, the perfect amount of flexibly we needed to get our giant site ported to ES6. SystemJS provides ES6 transpilation but we aren't worried about that yet. Aside from our import syntax all our code is still ES5 compliant. All thats required is a little oil in the gears.
One of the larger hurdles of the port process is configuring SystemJS. Lets take a look at the most basic configuration.
We are taking the node modules folder and turning it into an alias. Anytime ‘npm:’ is used in a path it will resolve to that folder. Now, the map property in the config is used to map a module name to a specific path. In this case we have our app, and angular.
This is what index.html looks like
Notice we only include the SystemJS dependency with a script tag. The other 200 files we used to define in this file are gone. All our files and modules will be loaded via SystemJS by alias or path.
We are telling SystemJS to load our mapped configuration for that alias. In this case we told our SystemJS config ‘Application’ is the same as ‘app/app.js’. When the page loads our config will be loaded by path and filename, then ‘Application’ will be loaded by alias. The only difference being we are letting SystemJS do the loading of the file through configuration instead of letting the browser load it.
Now lets look at app.js
When we call import ‘angular’ SystemJS will try to resolve that module name. When we configured SystemJS we told it to look for angular in the node_modules folder. When we load our index you will notice SystemJS will make a call to node_modules to load the dependent library. Also notice we use the relative path to load the controller.
Lets take a look at app.controller.js
We took our ‘new style’ and just simply export the function using the ES6 module format and removed our angular module definition. This allows us to import the function in any context. Given how we have around 50+ controllers it is required to import and register each one in app.js. Yes we touched every file. Its also required to import all our angular module dependencies in app.js. The new code style we enforced allows us to remove a few lines and be off to ES6 module goodness.
Transpiling to ES6+
Insert Gulp and Babel.
We can create a gulp task that will take all our ES6 code and push it through babel in memory ahead of time instead of in the browser. This should speed things up considerately. We need to output the result of babel into a file that SystemJS can use to load as modules.
Welcome to our second hurdle. Fortunately theres a babel plugin that does just this. The ES2015 module to SystemJS plugin for babel will do some serious magic to our files. This will not only transpile our code, but it will take any file we transpile and register it as a SystemJS module. This means SystemJS will not need to go load and transpile the file when the page loads. It will already be loaded and registered in the browser by the time SystemJS goes to import it. This is important because that means we can combine all our JS into a single bundled file loaded ahead of time and then let SystemJS load the modules as we need them.
Couple things to note.
- Use gulp cache to cache all files and only pass through to the next step the files that have changed. This means when we add a watch statement later sequential builds will only transpile the files we updated. Speeds things up significantly.
- Our babel settings disable the es2015 module export convention because we are exporting all our files as system modules instead.
- The transform-es2015-modules-systemjs plugin will register each file as a systemJS module. That means every file. For example ‘app/app.js’ becomes a registered system module and will not be requested using XHR when the browser loads (speeding up load time by 50x)
- Gulp remember will return all the cached files to the stream that were not changed
Now that we have our bundle-app.js what do we do with it? Well the only thing we have to do is tell SystemJS about the registered modules (so it wont try to load them using XHR).
Our updated index.html
Notice we are still importing ‘Application’. The difference here being that all the js modules and files are already registered in the ‘bundle-app’.
Aside from css/less the other files we maintain in our app is the HTML templates. Because all our JS will be bundled into single file that means the only reason we need our folder structure in our app is to request the HTML templates from the appropriate folder.
Gulp and babel to the rescue! (again)
We create a new task that grabs all our HTML, converts them to angular templates, builds an es6 module, and transpile that module to systemjs es5 bundle.
We then have to load our registered SystemJS modules before we bootstrap the app. This may require some of the template paths throughout the app to be modified slightly (in our case we had to remove the top level folder).
Our index now looks like this:
This should result in no XHR calls being made to load the entire app. The load time performance will significantly improve.
Managing dependencies using NPM
Up until this point we are managing and transpiling all our app files that we maintain. Short of CSS that covers almost the entire site. The final remaining piece is bundling the dependencies. Remember our ‘import angular’ syntax? Well because we registered that as a NPM alias and that path is not a SystemJS module registered in our ‘bundle-app.js’ SystemJS will us XHR to try to load any dependency from the node_module folder. Slightly slow but the real driver here being we don’t want to copy our entire node_modules folder. We only want the app dependencies and node_modules is quite large and we dont know which files our app really depends on.
SystemJS bundler has this problem solved for us. While we could have used the bundler for our app, it tends to be a lot slower due to tree shaking and among other things. The up side is the SystemJS bundler is a lot smarter about managing dependencies.
We create another bundle. Except this time we let system decide which modules to include. We explicitly tell system to NOT include the files in our app (because we want to handle those ourselves through a faster means). SystemJS bundler will then output all of our dependencies (with tree shaking) into another bundle of modules in the SystemJS format.
In production workflows: don’t forget to copy the one node_module folder to dist that is required without a bundle — SystemJs!
No real graceful way to handle css dependencies automatically. We created a manual step in gulp to copy all the dependencies into a single file we include in our index.