📜 ⬆️ ⬇️

Angular - setting up the development and production environment for assemblies with AOT compilation and tree-shaking (Gulp, Rollup, SystemJS)

One of the features of Angular, inherent in both the first and the new version is the high threshold of entry. New Angular, among other things, it is difficult to even run. And running it is easy to get 1-2 MB of scripts and the order of several hundred requests when loading the hello world page. You can, of course, use all sorts of starters, seed'y or Angular CLI, but for use in a serious project you need to figure it out for yourself.


In this article I will try to describe how to set up a convenient development environment using SystemJS, and production build Angular applications based on Rollup, with the output of about 100kb scripts and several requests when opening the page. We will use TypeScript and SCSS.


You can try everything in my angular-gulp-starter project.




Development environment


During development, in my opinion, the most important thing is to quickly see your code at work. You make changes to the code, look at it in work, edit the code again. The faster all this happens, the more comfortable the environment. In addition, it is important to have a comfortable debugging, informative error messages (which are easy to find in the code). In case of unforeseen situations, it is important to keep everything under control - you need to have access to all intermediate files to easily investigate the problem.


Technically, we need to solve three problems:


  1. Compile TypeScript
  2. Compile scss
  3. Download all (including dependencies) to the browser in the correct order

The first two tasks are most conveniently solved using the compile-on-save function, which works in almost any IDE. With this approach, it is enough to save your edits in the code, switch to the browser window and press F5 - very quickly and conveniently. In addition, the compilation results are easy to control, js-files lie next to c ts, and in which case you can always search for them.


From the IDE to work with TypeScript I can recommend Visual Studio (for example, Visual Studio 2015 Community Edition), which has built-in support for the TypeScript + Web Compiler extension for SCSS. I tried Atom, Visual Studio Code, but on my laptop they are too slow. Visual Studio (not Code) does well with backlighting, autocompletion and compilation on the fly, even on a weak machine. Although there are some backlight problems when using es6 import.


The third task (download everything to the browser) is the most problematic, since The scripts are dependent on each other, and must be loaded in the correct order. Manually controlling all this is difficult and unnecessary. It is best to leave the SystemJS library to deal with dependencies: in the code we use the ES6 import / export syntax, and based on this, SystemJS loads all the necessary files dynamically. No need to build any bundles, perform some special build, just configure the config.


The SystemJS configuration is a js file that might look something like this:


SystemJS Configuration Example for Angular Application
System.config({ defaultJSExtensions: true, paths: { "*": "node_modules/*", "app/*": "app/*", "dist-dev/*": "dist-dev/*", "@angular/common": "node_modules/@angular/common/bundles/common.umd", "@angular/core": "node_modules/@angular/core/bundles/core.umd", "@angular/http": "node_modules/@angular/http/bundles/http.umd", "@angular/compiler": "node_modules/@angular/compiler/bundles/compiler.umd", "@angular/platform-browser-dynamic": "node_modules/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd", "@angular/platform-browser": "node_modules/@angular/platform-browser/bundles/platform-browser.umd", "@angular/router": "node_modules/@angular/router/bundles/router.umd", "@angular/forms": "node_modules/@angular/forms/bundles/forms.umd" }, packageConfigPaths: ["node_modules/*/package.json"] }); 

Here we do the following:


  1. We specify that SystemJS automatically substitute js extensions to files (defaultJSExtensions).
  2. We indicate that, unless otherwise specified, search for everything in the node_modules ( "*": "node_modules/*" ) folder. This makes it easy to install dependencies via npm.
  3. We prescribe that modules starting with the app need to be loaded not from the node_modules , but from the app folder (our main folder with scripts). This is used only in index.html, where app/main imported.
  4. Register paths to angular modules. Ideally, this should happen automatically due to the packageConfigPaths parameter, but I could not get it to work (what did I do wrong?).
  5. If some third-party library is not automatically located, then we also prescribe the path to it explicitly.

After that, we just need to include in the index.html a number of service scripts: zone.js, reflect-metadata, core-js (or es6-shim), systemjs itself, its config and call the import of the main module:


 <script>System.import('app/main');</script> 

As a result, SystemJS will load the file app/main.js , analyze its import and load these imported files, analyze their import and so all the application files will be downloaded in turn.


However, this is not all. The fact is that the rxjs library, actively used in Angular, consists of many small modules. Therefore, if you leave everything so, then when you refresh the page, they will all be loaded one by one, which is somewhat slow (up to 100-300 requests).





Therefore, in my starter project I collect all the rxjs in one bundle using Rollup. Before this, it is additionally compiled in ES6, which is used later, in production assembly.


Build rxjs into one bundle to speed up page loading in a dev-environment

Building this rxjs bundle is quite complicated. First, TypeScript sources are compiled in ES6 (from the node_modules/rxjs/src folder), after which all of this is packaged with the help of a Rollup into one file, and transported to ES5. At the same time, in order to make this bandl friend with SystemJS, a temporary file is created, which serves as an entry point for the Rollup, and looks like this:


 import * as pkg0 from 'rxjs/add/observable/bindCallback'; System && System.set && System.set('rxjs/add/observable/bindCallback', System.newModule(pkg0)); import * as pkg1 from 'rxjs/add/observable/bindNodeCallback'; System && System.set && System.set('rxjs/add/observable/bindNodeCallback', System.newModule(pkg1)); ...     rxjs 

All this can be found in the files build.common.js / rxjsToEs and build.dev.js / rxjsBundle. Source code compiled in ES is also used in production, therefore compilation is made separately.


After the bundle is compiled, it must be downloaded before the code of our application is loaded. This is done like this:


  System.import('dist-dev/rxjs.js').then(function () { System.import('app/main'); }); 

As a result, we get about a second faster page load:



')

For ease of development, you also need a simple web server that supports HTML5 routing (when index.html is returned to all requests). An example of such a server based on express can also be found in the starter .


A knowledgeable reader may still ask, why not a webpack? In short - webpack is good for production, but, IMHO, inconvenient during development. More details in the spoiler below.


Why not Webpack, not JSPM, not Browserify

Webpack


Angular CLI and many starter and seed projects use Webpack. He is now able to do tree-shaking, and they say, even hot module reloading (did anyone try in the context of Angular?). But I do not share the hype around this collector, and I do not understand where it comes from. Webpack is a bundler, and he can only build a bundle. This raises many problems:


  1. Assembly takes some significant time (at least a few seconds). We can't use compile-on-save, which is much faster (at least it's not that easy).
  2. Yes, you can use watch, so when you save changes, the assembly will start automatically. But this does not solve the problem. 1. In practice, everything looks like this: I enter a part of the code, save, build starts, while it lasts, I enter the following code and save - the result is an outdated bundle, without recent edits. Has anyone encountered this problem? How do you solve it?
  3. If source maps do not work for you (and for some reason they constantly break down and sometimes slow down), then error messages will be difficult to localize.

However, I could be wrong, since I didn’t work with Webpack.


Jspm


JSPM is the first thing that comes to mind when it comes to SystemJS. Indeed, using it is quite easy to set up a convenient development environment for Angular. You can use both the compile-on-save and TypeScript bootloader. They say that there even works tree-shaking based on the Rollup. It would seem that everything is perfect.


But this is only at first glance. Sometimes it seems to me that JSPM lives in some kind of parallel world, far from everything that is happening around. For some reason, they needed to store all the packages, including npm-packages, in their own folder in a special way. As a result, instead of a convenient out-of-the-box tool, you get a bunch of headaches about how to get all the other utilities (which, as a rule, can work with node_modules) make friends with JSPM.


At a minimum, you will have to set up separate typings for dependencies in order to make JSPM friends with TypeScipt (or even worse, prescribe paths). Making the AOT compiler work is also a separate topic. If you need to do something non-standard (as with rxjs), also problems. In general, I just did not manage to tie everything up and make a production build on JSPM. If someone succeeds, it would be very interesting to see.


Browserify


It seems there is support for Rollup . Perhaps it is worth trying to build an assembly on its basis, I haven't tried it. However, to be honest, I don’t see much point in this when the Rollup itself does a good job with the task. The rest is the same as with the Webpack.



Production build


The Angular release build includes the following steps:


  1. Ahead of time (AOT) compilation of templates (html and css parts of components). In the dev-environment, they are compiled right in the browser, but for release it is better to do it in advance. Then we don’t have to drag the compiler code into the browser, the tree-shaking efficiency will increase, the launch of the application will speed up a bit.
  2. Compiling TypeScript in ES6 (including the results of the first step). We need exactly ES6, because Rollup can only work with ES6. We also compile SCSS, run post-processing.
  3. Build a bundle using tree-shaking using a Rollup. As a result, all unused parts are removed from the code, and the size of scripts is reduced tenfold.
  4. Resulting the result in ES5 using the same TypeScript, minification.
  5. Preparing release index.html, copying files to dist.

AOT compilation


AOT compilation is done using the @angular/compiler-cli (also called ngc), which is based on the TypeScript compiler. To perform the compilation you need:


  1. Install packages: @angular/compiler , @angular/core , @angular/platform-browser-dynamic , typescript and, actually, @angular/compiler-cli . Best of all, install everything locally in the project.
  2. Create a tsconfig.json file (for example, such ).
  3. Start the compilation with the command "./node_modules/.bin/ngc" -p tsconfig.ngc.json , or with the help of gulp-plugin .

Unpleasant features of the AOT compiler

NGC is built on the basis of TypeScript, but built, it should be said, bad. Not all the features of TypeScript in it work as it should. For example, inheritance of configurations does not work (therefore, there are 3 separate tsconfig files in the starter). In this article, you can see what the AOT compiler does not yet support. The list is far from complete (for example), so be prepared that there will be problems with this. The compiler can "fall" somewhere in its depths or go into an infinite loop, and finding out the cause is not always easy. Check that everything is compiled often, so you do not have to deal with it all at once.


The configuration file looks basically the same as the main tsconfig. However, the compiler generates a lot of files, cluttering with which the source folder is unpleasant. Therefore, in the configuration, it is desirable to specify the folder where the compilation results will be placed:


 "angularCompilerOptions": { "genDir": "app-aot" } 

This is also true because the compiler also processes the components of Angular itself. Therefore, if you do not specify genDir, then part of the results will appear in the node_modules folder. This is at least strange .


It is worth paying attention that AOT-files refer to the main sources by relative paths. Therefore, the relative position of the folders is important.


TypeScript release compilation


The difference between release compilation and the usual one is, firstly, that it is necessary to create a separate main.ts file. During development, it should be excluded from compilation, and in the release build, on the contrary, it should replace the dev version. The difference of this file is that a special bootstrap function is used, which uses the results of the AOT compilation. In particular, we run the AppModuleNgFactory (the result of the compilation of the AppModule) from the genDir AOT compilation:


 import { platformBrowser } from '@angular/platform-browser'; import { AppModuleNgFactory } from '../app-aot/app/app.module.ngfactory'; platformBrowser().bootstrapModuleFactory(AppModuleNgFactory); 

Also here we turn on the production mode for Angular (this is important to do, since it greatly affects the performance):


 import { enableProdMode } from "@angular/core"; enableProdMode(); 

The second difference in release compilation is the use of the ES6 target platform. If you do not do this, Rollup will not give an error, but tree-shaking will not. For the same reason, we need an ES6 version of rxjs. Previously, rxjs had a special rxjs-es package, and all the examples of building Angular on gulp, which Google shows on the first pages, use it. Unfortunately, this package is no longer supported . Therefore, we need to compile rxjs from TypeScript sources ourselves, as described above .


The release compilation configuration includes the app , and app-aot (genDir AOT compilation) and excludes dev main.ts , as described above. Also, for order, in my starter, the results of the prod compilation are placed in temp / app-prod-compiled. All this is in the file build.prod.js .


Tree-shaking with the Rollup library


Build with Rollup is a key build step that can turn 1 MB of source code into 100 KB. Rollup analyzes the source code, and throws out of them those parts of the code that are not used.


It accepts a single file as input: main.js (more precisely, main-aot.js), analyzing an import expression in which all other modules are assembled. It follows that the Rollup must be able to find the necessary libraries. Most problems are solved by the plugin rollup-plugin-node-resolve , which finds the libraries in node_modules. Its use is prescribed in the corresponding configuration file .


In case you need to do something specific, then it is easy to write your own plugin. For example, in this way I specify Rollup that rxjs should be taken from the same folder where our compiled ES6 version lies (RollupNG2 in the same rollup-config ).


Of the configuration features, it is worth noting the treeshake: true parameter (of course), context: 'window' (we say that we build for the browser) and format: 'iife' . The IIFE format will do without SystemJS, simply by adding the resulting file as a script tag in index.html.


Translating the result in ES5 using TypeScript is quite simple, the main thing is to set the parameter allowJs. The operation takes a couple of lines in the bundling.js file in the rollupBundle function.


Preparing the release index.html


After all the work done above, we can only collect all the auxiliary libraries in one bundle, and add the result of the rollup to the page via the script tag. All these are standard tasks for gulp.


In the starter, all this is done on the basis of maximum simplicity, so as not to force users to once again figure it out. You can find the corresponding code in the build-prod.js file . For testing, there is also configured an express-server , with gzip-compression enabled.


As a result, we get 118 KB after gzip:




The example uses the Tour of Heroes from the official Angular guides, which is not exactly "Hello world". If to simplify completely, it can turn out up to 50-80 Kb.


On the links below you can try both versions of the assembly live:


DEV version online
PROD version online


In conclusion, I want to recommend an excellent article on the topic of Minko Gechev. In it, he gives an example of the simplest assembly of 6 npm-scripts, which performs all the basic steps (note that rxjs-es is used there, which is no longer supported). True, the seed-project for its authorship I did not like, because of the high complexity and not very high convenience.


That's all, good luck!

Source: https://habr.com/ru/post/321236/


All Articles