Standards of writing web applications are continually changing over time. The web started as kind of static pages without any client-oriented logic on-board. The user had to fill out the form and click the submit button to send the data to the server. To update the page, user had to reload the page from the server. The first web-chats did exactly like that – they were updating current page every 10 seconds. After, AJAX came to life and pages began to acquire dynamic nature. We sent requests to the server to update only a small part of the application, and that was a significant step forward. Against this background, jQuery came in handy since it helped to easily place the updated data on the page.
But jQuery is a library that perfectly solves its narrow task – to manipulate the DOM. However, as it is always the case, we need to get more than we have. We want to change not only a part of the page; we want a whole new page! We need access control, state storage, client navigation, and much more. Here, various frameworks start their journey in order to build a one-page application. One of the examples is SPA. It’s the most popular and promising framework we'll talk about. In this article I intend to describe and emphasize the opportunities that Angular2 provides for building a full and comprehensive SPA.
SPA is a single-page application; however, the definition does not mean that the application is created only for "a single page”. No. This means that the entire structure of application – sometimes only a main part of it that is needed for displaying the first page – is loaded on the first visit. All the data needed to display a particular open page could be retrieved after a single page load, if required. Let’s go into more detail.
As I said before, first of all, when requesting a page from the server, the client gets a certain structure. In Angular2, there is always the root module (app.module), and the root component (app.component).
Modern applications often consist of dozens of modules which leads to super long load times. In an attempt to fix this, a so-called lazy loading module (lazy load) was developed. The lazy load allows you to define some modules as downloadable-on-demand, and afterwards, to request them only when it's necessary. This helps to significantly decrease the time and the amount of data received during the first load. We are talking about dynamic modules as a part of our application.
It's quite easy to create a dynamic-loadable module – it could take a few steps:
1. Installing the loadernpm install angular2-router-loader — save-dev
2. Configuring the webpack
// webpack.config.js
loaders: [
{
test: /.ts$/,
loaders: [
// ...
‘angular2-router-loader’]
},
// ...
]
3. Configuring the path to the module in app.routes
// app.routing.ts
const routes: Routes = [
...
{ path: ‘lazy’, loadChildren: ‘./+lazy-module/lazy.module#LazyModule' }
];
Well, that's it. This should be enough to load and initiate the module when it is needed or when the user is directed to the /lazy address. At this point, a request will be made for a separate script file from the server, initializing it, checking its own routing, and possibly redirecting the user to 404 if the requested router was not found in the module.
At the time of the static web, the address of the page was determined by its location regarding the www_root directory on the server. So, the /contracts.html address says to the web server that it should give the client a file from www_root / contracts.html. Then, for example, on Apache + PHP, a php script was compiled with the request on the same address and this script could form a dynamic page. That means you have to comply the requests to the database, print them to a page and render the generated HTML document to the client. In Django, the address of the page is checked within the regular expression ‘r'^contracts/’. When matching is found, the request processing is returned to the view-function that forms the response. The address never reflects directory structures.
In Angular2, Router is answering for routing, who would have thought! The configuration looks like this: (here I will skip some points that are related to the initial routing configuration. For more details, you can read documentation where the section is described quite fully.
const APP_ROUTES = [
{path: '', component: Home},
{path: 'contracts', loadChildren: './components/+contracts/contracts.module#ContractsModule'},
]
Here we see that our application has 2 root states, and contacts refer to the dynamically loaded ContractsModule, I wrote about this earlier. The list of APP_ROUTES we specify in the AppModule.
@NgModule({
bootstrap: [ App ],
declarations: [
// ...
],
imports: [
// import Angular's modules
// ...
RouterModule.forRoot(APP_ROUTES)
],
providers: [
// ...
]
})
export class AppModule {}
When we open our application page, Router splits the requested address into segments separated with '/', and then, it successively searches for the matches in the Routes configuration. If it finds them, it initiates the component or the component tree (about this later), and passes control to it.
In ContractsModule, as well as in AppModule, we've specified the list of this module Routes.
@NgModule({
imports: [
RouterModule.forChild(CONTRACTS_ROUTES),
],
declarations: [
// ...
],
exports: [],
providers: [
// ...
]
})
export class ContractsModule {}
The list of CONTRACTS_ROUTES looks like this:
export const CONTRACTS_ROUTES = [
{ path: "", component: ContractsListComponent },
{ path: "templates", children: TEMPLATES_ROUTES },
{
path: ":id",
resolve: {
contract: ContractResolver,
},
children: [
{
path: "",
component: ContractEditComponent,
children: [{ path: "foo", component: ContractFooComponent }],
},
{ path: "send", component: ContractSendComponent },
],
},
];
Now let's analyze in details how it all works.
This is a great tool for the developer, as it allows you to make the code much cleaner, and often you can get rid of the extra code in the components.
In our example, resolve is specified for the ‘/contracts/:id’ router. It has 2 routers in the children: '' and 'send'.
I'm sure you are familiar with the situation when you need to write a component that displays the data of some object. In our case, this is a contract. Therefore, most often the code that gets the contract data would look something like this:
ngOnInit() {
this.route.params
.switchMap((params: { id: string }) => this.contractService.get(params.id))
.subscribe((contract: Contract) => {
this.contract = contract;
});
}
Looks familiar? And something like that in the template
<div class="contract-page">
<h1>{{ contract.name }}</h1>
</div>
But this code will definitely give an error cannot read property 'name' of undefined
.
What are the solutions? For example, use Elvis Operator
<div class="contract-page">
<h1>{{ contract?.name }}</h1>
</div>
Perfect solution? Not really!
Yes, it will save us from the previous error, but what if you have to derive a couple of dozen fields from the contract? Do we have to use Elvis each time? :) Yes, and with this approach, you will see empty items on the page, which, after loading the contract data from the server, will suddenly appear. Not what we expected. Yes, you can assign an initial contract value.
private contract = {};
However, here you still have to write conditions and Elvis Operator when describing the attached contract data, if you have them, for example - contract.client? .name.
There is another solution:
<div class="contract-page" *ngIf="contract">
<h1>{{ contract.name }}</h1>
</div>
Also familiar? Many developers come with the decision to hide the whole block and get rid of the Elvis Operator. And it works in general, but not perfectly. For example, if you need to place the block to render the child routes inside this block, which hides the ngIf = "contract
condition, then you will be disappointed – this will not work. Like in the CONTRACTS_ROUTES
scenario.
{
path: ':id',
children: [
{
path: '', component: ContractEditComponent,
children: [
{path: foo, component: ContractFooComponent}
]
},
]
},
<!-- contract-edit.componet.html -->
<div class="contract-page" ngIf="contract">
<h1>{{ contract.name }}</h1>
<router-outlet></router-outlet>
</div>
When you request /contracts/1/foo, you will certainly get a Can not find a primary outlet to load ContractFooComponent. Why? Because ContractEditComponent is already created. The template is rendered and working, and there is no contract data yet. The !!contract === false
condition and Router have nowhere to place ContractFooComponent. So, you cannot do this either. You have to wait for the server response first, and only then, you can instantiate the component. How do we do that? That's where Resolver comes to the rescue.
@Injectable()
export class ContractResolver implements Resolve {
constructor(private contractService: ContractService) {
}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.contractService.get(route.params.id);
}
}
This code runs once before the ngOnInit component, and in our case, the contract will be accessible via Subject route.data, route: ActivatedRoute
ngOnInit() {
this.route.data
.subscribe((data: { contract: Contract }) => {
this.contract = data.contract;
});
}
With this approach, we can clean our template of parasitic code and it will look like this:
<div class="contract-page" >
<h1>{{ contract.name }}</h1>
<router-outlet></router-outlet>
</div>
Moreover, here is another killer feature! Notice where we have resolve in CONTRACTS_ROUTES. I'll duplicate:
{
path: ':id',
resolve: {
contract: ContractResolver
},
children: [
{
path: '', component: ContractEditComponent,
children: [
{path: 'foo', component: ContractFooComponent}
]
},
{path: 'send', component: ContractSendComponent},
]
},
This means that it is immediately available in each of the children's routes:
Moreover, with a further transition, there will be no contract data request to the server, as it was with the case when we separately requested a contract in each component. Well, it will be as long as its ID in the URL does not change. And that is great!
Of course, Angular2 Router provides a mechanism for controlling access to the routs. This mechanism is called Guards. There are only 4 types of them:
CanActivate
– it enables/disables the activation of Route and all its children Routes.CanDeactivate
- it enables/disables Route deactivation, for example, if there are unsaved data on the form.CanActivateChild
- it enables/disables the activation of only children Routes.CanLoad
- it enables/disables the lazy loading modules.There is no sense in describing them in details. They all act the same way. They have almost the same interface. Let's take a look at the example of Guard which simply forbids us to access some of the Route and all of its children Routes.
@Injectable()
export class AccessDeniedGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean {
return false;
}
}
and connect it to our contract/:id/foo – just for a temporarily turn-off.
// CONTRACTS_ROUTES
{
path: ':id',
children: [
{
path: '', component: ContractEditComponent,
children: [
{
path: foo,
component: ContractFooComponent,
canActivate: [AccessDeniedGuard]
}
]
},
]
},
The canActivate method has to return one of the Observable | Promise | Boolean
.
I should note that all guards work before Resolver, and we can not use the received object to check access (for example, if we wanted to forbid opening contracts with the 'sent' status). To do this, we have to use the tricks inside Resolver.
Yes, search bots aren't able to comply javascript of your site yet. And when you try to index a page, the index page will only get what will came back with the server response, and in the case of Angular2 it will be mostly a blank page. To solve this problem, developers use some ingenious tricks related to the page rendering to the server.
In Angular < 2 we use PhantomJS to which the request is redirected when the client appears from the list of the search bots. First, you need to add in the HEAD page to make it clear to the search engine that this page contains content generated by JavaScript, but accessible by another, special link. You just need to replace #!
with ?_escaped_fragment =?
and send the request again. The overall appearance of PhantomJS looks as follows:
var server = require("webserver").create();
var port = 19003;
var getPage = function (url, callback) {
var page = require("webpage").create();
page.open(url, function () {
setTimeout(function () {
page.evaluate(function () {
$("meta[name=fragment], script").remove();
});
callback(page.content);
page.close();
}, 200);
});
};
server.listen(port, function (request, response) {
response.headers = {
"Content-Type": "text/html",
};
var regexp = /_escaped_fragment_=(.*)$/;
var fragment = request.url.match(regexp);
var url = "http://localhost:19002/#!" + decodeURIComponent(fragment[1]);
getPage(url, function (content) {
response.statusCode = 200;
response.write(content);
response.close();
});
});
Then we run the PhantomJS server in a separate process.
$ phantomjs server_render_script.js
After that, we are configuring nginx:
if ($args ~* _escaped_fragment_) {
proxy_pass http://localhost:19003; # phantomjs
}
proxy_pass http://127.0.0.1:19002; # original
This is the easiest way to render server pages. Suitable for most cases.
There is also the Angular 2 Universal project that allows not only to render Angular2 applications on the server for SEO, but also render the requested page for a regular client. The requested page for a regular client will not only lost its SPA functionality, but will significantly reduce the time for the initial page load.
SPA can only concede when the application is downloaded first time. All further communication with the server occurs through the REST API, then getting small portions of data and rendering them to HTML on the client side. This is always faster than full load of a new page, even if using the cache for JS, CSS.
The SPA will work much faster with the usage of the lazy loading, right approach and proper organization of the application structure.
This opens up the widest horizons for building responsive and fast applications.