Lessons learned from AngularJS in the wild
Although the previous section listed a lot of arguments for the required re-implementation of the framework responding to the latest trends, it's important to remember that we're not starting completely from scratch. We're taking what we've learned from AngularJS with us. In the period since 2009, the Web is not the only thing that evolved. We also started building more and more complex applications. Today, single-page applications are not something exotic, but more like a strict requirement for all web applications solving business problems, which are aiming for high performance and a good user experience.
AngularJS helped us to efficiently build large-scale, single-page applications. However, by applying it in various use cases, we've also discovered some of its pitfalls. Learning from the community's experience, Angular's core team worked on new ideas aiming to answer the new requirements.
AngularJS follows the Model View Controller (MVC) micro-architectural pattern. Some may argue that it looks more like Model View ViewModel (MVVM) because of the view model attached as properties to the scope or the current context in case of "controller as syntax". It could be approached differently again, if we use the Model View Presenter pattern (MVP). Because of all the different variations of how we can structure the logic in our applications, the core team called AngularJS a Model View Whatever (MVW) framework.
The view in any AngularJS application is supposed to be a composition of directives. The directives collaborate together in order to deliver fully functional user interfaces. Services are responsible for encapsulating the business logic of the applications. That's the place where we should put the communication with RESTful services through HTTP, real-time communication with WebSockets, and even WebRTC. Services are the building blocks where we should implement the domain models and business rules of our applications. There's one more component, which is mostly responsible for handling user input and delegating the execution to the services-the controller.
Although the services and directives have well-defined roles, we can often see the anti-pattern of the Massive View Controller, which is common in iOS applications. Occasionally, developers are tempted to access or even manipulate the DOM directly from their controllers. Initially, this happens while you want to achieve something simple, such as changing the size of an element, or quick and dirty changing elements' styles. Another noticeable anti-pattern is the duplication of the business logic across controllers. Often, developers tend to copy and paste logic, which should be encapsulated inside services.
The best practices for building AngularJS applications state that the controllers should not manipulate the DOM at all; instead, all DOM access and manipulations should be isolated in directives. If we have some repetitive logic between controllers, most likely we want to encapsulate it into a service and inject this service with the dependency injection mechanism of Angular in all the controllers that need that functionality.
This is where we're coming from in AngularJS. All this said, it seems that the functionality of controllers could be moved into the directive's controllers. Since directives support the dependency injection API, after receiving the user's input, we can directly delegate the execution to a specific service, already injected. This is the main reason why Angular now uses a different approach, by removing the ability to put controllers everywhere by using the ng-controller
directive. We'll take a look at how AngularJS controllers' responsibilities could be taken from the new components and directives in
Chapter 4, Getting Started with Angular Components and Directives.
Data binding in AngularJS is achieved using the scope
object. We can attach properties to it and explicitly declare in the template that we want to bind to these properties (one- or two-way). Although the idea of the scope seems clear, it has two more responsibilities, including event dispatching and the change detection-related behavior. Angular beginners have a hard time understanding what scope really is and how it should be used. AngularJS 1.2 introduced something called controller as syntax. It allows us to add properties to the current context inside the given controller (this
), instead of explicitly injecting the scope
object and later adding properties to it. This simplified syntax can be demonstrated through the following snippet:
<div ng-controller="MainCtrl as main">
<button ng-click="main.clicked()">Click</button>
</div>
function MainCtrl() {
this.name = 'Foobar';
}
MainCtrl.prototype.clicked = function () {
alert('You clicked me!');
};
The latest Angular took this even further by removing the scope
object. All the expressions are evaluated in the context of the given UI component. Removing the entire scope API introduces higher simplicity; we don't need to explicitly inject it anymore, instead we add properties to the UI components to which we can later bind. This API feels much simpler and more natural.
We will take a more detailed look at the components and the change detection mechanism of Angular in
Chapter 4, Getting Started with Angular Components and Directives.
Maybe the first framework on the market that included inversion of control (IoC) through dependency injection (DI) in the JavaScript world was AngularJS. DI provides a number of benefits, such as easier testability, better code organization and modularization, and simplicity. Although the DI in the first version of the framework does an amazing job, Angular 2 took this even further. Since the latest Angular is on top of the latest Web standards, it uses the ECMAScript 2016 decorators' syntax for annotating the code for using DI. Decorators are quite similar to the decorators in Python or annotations in Java. They allow us to decorate the behavior of a given object, or add metadata to it, using reflection. Since decorators are not yet standardized and supported by major browsers, their usage requires an intermediate transpilation step; however, if you don't want to take it, you can directly write a little bit more verbose code with ECMAScript 5 syntax and achieve the same semantics.
The new DI is much more flexible and feature-rich. It also fixes some of the pitfalls of AngularJS, such as the different APIs; in the first version of the framework, some objects are injected by position (such as the scope, element, attributes, and controller in the directives' link function) and others, by name (using parameters names in controllers, directives, services, and filters).
We will take a further look at the Angular's dependency injection API in
Chapter 5, Dependency Injection in Angular.
The bigger the requirements of the Web are, the more complex web applications become. Building a real-life, single-page application requires writing a huge amount of JavaScript, and including all the required external libraries may increase the size of the scripts on our page to a few megabytes. The initialization of the application may take up to several seconds or even tens of seconds on mobile until all the resources get fetched from the server, the JavaScript is parsed and executed, the page gets rendered, and all the styles are applied. On low-end mobile devices that use a mobile Internet connection, this process may make the users give up on visiting our application. Although there are a few practices that speed up this process, in complex applications, there's no silver bullet.
In the process of trying to improve the user experience, developers discovered something called server-side rendering. It allows us to render the requested view of a single-page application on the server and directly provide the HTML for the page to the user. Later, once all the resources are processed, the event listeners and bindings can be added by the script files. This sounds like a good way to boost the performance of our application. One of the pioneers in this was React, which allowed prerendering of the user interface on the server side using Node.js DOM implementations. Unfortunately, the architecture of AngularJS does not allow this. The showstopper is the strong coupling between the framework and the browser APIs, the same issue we had in running the change detection in Web Workers.
Another typical use case for the server-side rendering is for building Search Engine Optimization (SEO)-friendly applications. There were a couple of hacks used in the past for making the AngularJS applications indexable by the search engines. One such practice, for instance, is the traversal of the application with a headless browser, which executes the scripts on each page and caches the rendered output into HTML files, making it accessible by the search engines.
Although this workaround for building SEO-friendly applications works, server-side rendering solves both of the above-mentioned issues, improving the user experience and allowing us to build SEO-friendly applications much more easily and far more elegantly.
The decoupling of Angular with the DOM allows us to run our Angular applications outside the context of the browser. We will take a further look at it in
Chapter 8, Tooling and Development Experience.
MVW has been the default choice for building single-page applications since Backbone.js appeared. It allows separation of concerns by isolating the business logic from the view, allowing us to build well-designed applications. Taking advantage of the observer pattern, MVW allows listening for model changes in the view and updating it when changes are detected. However, there are some explicit and implicit dependencies between these event handlers, which make the data flow in our applications not obvious and hard to reason about. In AngularJS, we are allowed to have dependencies between the different watchers, which requires the digest loop to iterate over all of them a couple of times until the expressions' results get stable. The new Angular makes the data flow one-directional; this has a number of benefits:
- More explicit data flow.
- No dependencies between bindings, so no time to live (TTL) of the digest.
- Better performance of the framework:
- The digest loop is run only once.
- We can create apps, which are friendly to immutable or observable models, that allow us to make further optimizations.
The change in the data flow introduces one more fundamental change in AngularJS architecture.
We may take another perspective on this problem when we need to maintain a large codebase written in JavaScript. Although JavaScript's duck typing makes the language quite flexible, it also makes its analysis and support by IDEs and text editors harder. Refactoring of large projects gets very hard and error-prone because in most cases, the static analysis and type inference are impossible. The lack of compiler makes typos all too easy, which are hard to notice until we run our test suite or run the application.
The Angular core team decided to use TypeScript because of the better tooling possible with it and the compile-time type checking, which help us to be more productive and less error-prone. As the following diagram shows, TypeScript is a superset of ECMAScript; it introduces explicit type annotations and a compiler:
Figure 1
The TypeScript language is compiled to plain JavaScript, supported by today's browsers. Since version 1.6, TypeScript implements the ECMAScript 2016 decorators, which makes it the perfect choice for Angular.
The usage of TypeScript allows much better IDE and text editors' support with static code analysis and type checking. All this increases our productivity dramatically by reducing the mistakes we make and simplifying the refactoring process. Another important benefit of TypeScript is the performance improvement we implicitly get by the static typing, which allows runtime optimizations by the JavaScript virtual machine.
We'll be talking about TypeScript in detail in
Chapter 3, TypeScript Crash Course.
Templates are one of the key features in AngularJS. They are simple HTML and do not require any intermediate translation, unlike most template engines, such as mustache. Templates in Angular combine simplicity with power by allowing us to extend HTML by creating an internal domain-specific language (DSL) inside it, with custom elements and attributes.
This is one of the main purposes of Web Components as well. We already mentioned how and why Angular takes advantage of this new technology. Although AngularJS templates are great, they can still get better! The new Angular templates took the best parts of the ones in the previous release of the framework and enhanced them by fixing some of their confusing parts.
For example, let's say we have a directive and we want to allow the user to pass a property to it using an attribute. In AngularJS, we can approach this in the following three different ways:
<user name="literal"></user>
<user name="expression"></user>
<user name="{{interpolate}}"></user>
In the user
directive, we pass the name
property using three different approaches. We can either pass a literal (in this case, the string "literal"
), a string, which will be evaluated as an expression (in our case "expression"
), or an expression inside, {{ }}
. Which syntax should be used completely depends on the directive's implementation, which makes its API tangled and hard to remember.
It is a frustrating task to deal with a large amount of components with different design decisions on a daily basis. By introducing a common convention, we can handle such problems. However, in order to have good results and consistent APIs, the entire community needs to agree with it.
The new Angular deals with this problem by providing special syntax for attributes, whose values need to be evaluated in the context of the current component, and a different syntax for passing literals.
Another thing we're used to, based on our AngularJS experience, is the microsyntax in template directives, such as ng-if
and ng-for
. For instance, if we want to iterate over a list of users and display their names in AngularJS, we can use:
<div ng-for="user in users">{{user.name}}</div>
Although this syntax looks intuitive to us, it allows limited tooling support. However, Angular 2 approached this differently by bringing a little bit more explicit syntax with richer semantics:
<template ngFor let-user [ngForOf]="users">
{{user.name}}
</template>
The preceding snippet explicitly defines the property, which has to be created in the context of the current iteration (user
), and the one we iterate over (users
).
Since this syntax is too verbose for typing, developers can use the following syntax, which later gets translated to the more verbose one:
<li *ngFor="let user of users">
{{user.name}}
</li>
The improvements in the new templates will also allow better tooling for advanced support by text editors and IDEs. We will discuss Angular's templates in
Chapter 4, Getting Started with Angular Components and Directives.
In the Web Workers section, we already mentioned the opportunity to run the digest loop in the context of a different thread, instantiated as Web Worker. However, the implementation of the digest loop in AngularJS is not quite as memory-efficient and prevents the JavaScript virtual machine from doing further code optimizations, which allows for significant performance improvements. One such optimization is the inline caching (
http://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html
).
The Angular team did a lot of research in order to discover different ways the performance and efficiency of the change detection could be improved. This led to the development of a brand new change detection mechanism.
As a result, Angular performs change detection in code that the framework directly generates from the components' templates. The code is generated by the Angular compiler. There are two built-in code generation (also known as compilation) strategies:
- Just-in-Time (JiT) compilation: At runtime, Angular generates code that performs change detection on the entire application. The generated code is optimized for the JavaScript virtual machine, which provides a great performance boost.
- Ahead-of-Time (AoT) compilation: Similar to JiT, with the difference that the code is being generated as part of the application's build process. It can be used for speeding the rendering up by not performing the compilation in the browser and also in environments that disallow
eval()
, such as CSP (Content-Security-Policy) and Chrome extensions. We will discuss it further in the next sections of the book.
We will take a look at the new change detection mechanisms and how we can configure them in
Chapter 4, Getting Started with Angular Components and Directives.