神刀安全网

Angular Router

Managing state transitions is one of the hardest parts of building applications. This is especially true on the web, where you also need to ensure that the state is reflected in the URL. In addition, we often want to split applications into multiple bundles and load them on demand. Doing this transparently isn’t trivial.

The Angular router is designed to solve these problems. Using the router, you can declaratively specify application state, manage state transitions while taking care of the URL, and load components on demand. In this article I will discuss the API of the router, as well as the mental model and the design principles behind it.

Let’s get started.

Outline

  • What Do Routers Do?
  • URL Parsing and Serialization
    • Accessing URL Tree
  • Route Recognition
    • Powerful Matching Syntax
    • Component Instantiation
    • Using Params
    • QueryParams and Fragment
    • Using Snapshots
  • Navigation
    • Imperative Navigation
    • RouterLink
    • More on Syntax
    • Navigation is URL Based, not Route Based
  • E2E Example
  • Summary

What Do Routers Do?

Before we jump into the specifics of the Angular Router, let’s talk about what routers do in general.

As you might know, an Angular 2 application is a tree of components. Some of these components are reusable UI components (e.g., list, table), and some are application components. The router cares about application components, or, to be more specific, about their arrangements. Let’s call such component arrangements router states. A router state defines what is visible on the screen.

The router configuration defines all the potential router states an application can be in. Let’s look at an example.

[   {path: '/team/:id', component: TeamCmp, children: [     {path: '/details', component: DetailsCmp},     {path: '/help', component: HelpCmp, outlet: 'aux'}   ]},   {path: '/summary', component: SummaryCmp},   {path: '/chat', component: ChatCmp, outlet: 'aux'} ] 

which can be depicted as follows:

Angular Router

An outlet is the location where a component will be placed. If a node has multiple children of the same color, i.e., of the same outlet type, only one of them can be active at a time. Consequently, the team and summary components cannot be displayed together.

A router state is a subtree of the configuration tree. For instance, the example below has the summary component activated.

Angular Router

The router’s primary job is to manage navigations between states, which includes updating the component tree. A navigation is essentially the act of transitioning from one activated subtree to another. Say we perform a navigation, and this is the result:

Angular Router

Because the summary is no longer active, the router will remove it. Instead, it will instantiate the details component and display it inside the team component, with the chat component visible on the side.

That’s it. The router simply allows us to express all the potential states in which our application can be, and provides a mechanism for navigating from one state to another.

So now we’ve learned what routers do in general. It’s time to talk about the Angular router.

Angular Router

The Angular router takes a URL, parses it into a URL tree, recognizes router states, instantiates all the needed components, and, finally, manages navigation. Let’s look at each one of these operations in detail.

URL Parsing and Serialization

Angular Router

The URL bar provides a huge advantage for web applications over native applications. It allows you to reference states, bookmark them, and share them with your friends. In a well-behaved web application, any application state transition results in a URL change, and any URL change results in a state transition. In other words, a URL is a serialized router state.

The first thing the router does is parse the URL string into a URL tree. The router does not need to know anything about your application or its components to do that. In other words, the parse operation is application-independent. To get a feel of how this works, let’s look at a few examples.

Let’s start with a simple URL consisting of three segments.

/team/3/details  // is parsed into the following URL tree:  UrlSegment(path: 'team', parameters: {}, outlet: primary)   -> UrlSegment(path: '3', parameters: {}, outlet: primary)     -> UrlSegment(path: 'details', parameters: {}, outlet: primary) 

As you can see, a URL tree consists of URL segments. And each URL segment contains its parameters and its outlet name.

Now look at this example, where the first segment has the extra parameter set to true.

/team;extra=true/3  // is parsed into the following URL tree:  UrlSegment(path: 'team', parameters: {extra: true}, outlet: primary)   -> UrlSegment(path: '3', parameters: {}, outlet: primary) 

Finally, let’s see the result when the team segment has two children instead of one.

/team/3(aux:/chat;open=true)  // is parsed into the following URL tree:  UrlSegment(path: 'team', parameters: {}, outlet: primary)   -> UrlSegment(path: '3', parameters: {}, outlet: primary)   -> UrlSegment(path: 'chat', parameters: {open:true}, outlet: aux) 

As you can see, the router uses parenthesis to serialize nodes with multiple children, the colon syntax to specify the outlet, and the ’;parameter=value’ syntax (e.g., open=true ) to specify route specific parameters .

There are a couple of reasons why the router provides the URL tree instead of just giving us the URL string. First, the URL tree is a richer data structure, with more affordances to facilitate common operations. Second, it enables us to provide our own custom URL serialization strategy.

bootstrap(MyComponent, [{provider: RouterURLSerializer, useClass: MyCustomSerializer}]); 

Accessing the Current URL

You can get the current URL from the router service.

class TeamCmp {   constructor(router: Router) {     const currentUrlTree: UrlTree = router.urlTree;      // root segment     const root: UrlSegment = currentUrlTree.root;      // you can get the first child or the list of children of a segment     const firstChild: UrlSegment = currentUrlTree.firstChild(root);      // matrix parameters of a segment     const params: {[key:string]:string} = firstChild.parameters;      const path: string = firstChild.path;      // You can also serialize the tree back into a string.     const url: string = router.serializeUrl(currentUrlTree);   } } 

Storing URLs

URL trees and URL segments are immutable, so it is safe to store them. This is particularly handy for implementing interesting navigation and undo patterns.

Router State Recognition

Angular Router

Since the parsing of the URL does not require any knowledge about the application, the produced URL tree does not represent the logical structure of the application. A router state does. To create it, the router uses the provided configuration and matches it against a URL tree. Again, let’s look at a few examples.

Say this is the provided router configuration.

[   { path: '/team/:id', component: TeamCmp,     children: [{ path: '/details', component: DetailsCmp }] },   { path: '/chat', component: ChatCmp, outlet: 'chat' } ] 

And we are navigating to the following URL.

/team/3/details  UrlSegment(path: 'team', parameters: {}, outlet: primary)   -> UrlSegment(path: '3', parameters: {}, outlet: primary)     -> UrlSegment(path: 'details', parameters: {}, outlet: primary) 

First, the router will parse the URL string into a URL tree. Then, it will go through the items in the configuration, one by one, trying to match them against the parsed URL tree. In this particular case, the first entry is a match. Then, it will take the children items and will carry on matching.

If it is impossible to match the whole URL, the navigation will fail. But if it works, a router state representing the future state of the application will be constructed.

For this particular example it will look as follows:

/team/3/details  ActivatedRoute(component: TeamCmp) ------------------------------------------------------------------------------- UrlSegment(path: 'team', parameters: {}, outlet: primary)   -> UrlSegment(path: '3', parameters: {}, outlet: primary) -------------------------------------------------------------------------------  ActivatedRoute(component: DetailsCmp) ------------------------------------------------------------------------------- UrlSegment(path: 'details', parameters: {}, outlet: primary) -------------------------------------------------------------------------------  RouterState:  ActivatedRoute(component: RootCmp)   -> ActivatedRoute(component: TeamCmp)     -> ActivatedRoute(component: DetailsCmp) 

A router state consists of activated routes. And each activated route is associated with a single component. Also, note that we always have an activated route associated with the root component of the application.

Let’s look at one more example.

/team/3(aux:/chat;open=true)  ActivatedRoute(component: TeamCmp) ------------------------------------------------------------------------------- UrlSegment(path: 'team', parameters: {}, outlet: primary)   -> UrlSegment(path: '3', parameters: {}, outlet: primary) -------------------------------------------------------------------------------  ActivatedRoute(component: ChatCmp) ------------------------------------------------------------------------------- UrlSegment(path: 'chat', parameters: {open:true}, outlet: aux) -------------------------------------------------------------------------------  RouterState  ActivatedRoute(component: RootCmp, outlet: primary)   -> ActivatedRoute(component: TeamCmp, outlet: primary)   -> ActivatedRoute(component: ChatCmp, outlet: aux) 

Here we have two siblings with different outlet names, TeamCmp and ChatCmp, which are activated at the same time.

Powerful Matching Syntax

The syntax for matching is very powerful. It supports wildcards (e.g., '**' ), and positional parameters (e.g., /team/:id ).

Component Instantiation

Angular Router

At this point, we have a router state. The router can now match this state by instantiating each component, assembling a component tree, and placing each component into the appropriate router outlet.

To understand how it works, let’s look at the following example.

// Router Configuration: [   { path: '/team/:id', component: TeamCmp,     children: [{ path: '/details', component: DetailsCmp }] },   { path: '/chat', component: ChatCmp } ]  // Components: @Component({   selector: chat,   template: `     Chat   ` }) class ChatCmp {}  @Component({   selector: 'team',   template: `     Team     primary: { <router-outlet></router-outlet> }   ` }) class TeamCmp {}  @Component({   selector: 'team',   template: `     Root     primary: { <router-outlet></router-outlet> }     aux:   { <router-outlet name='aux'></router-outlet> }   ` }) class RootCmp {} 

Say we are navigating to this URL '/team/3(aux:/chat)/details , which corresponds to the following router state.

ActivatedRoute(component: RootCmp)   -> ActivatedRoute(component: TeamCmp, parameters: {id: 3}, outlet: primary)     -> ActivatedRoute(component: DetailsCmp, parameters: {}, outlet: primary)   -> ActivatedRoute(component: ChatCmp, parameters: {}, outlet: aux) 

First, the router will instantiate TeamCmp and place it into the primary outlet of the root component. Then, it will place a new instance of ChatCmp into the ‘aux’ outlet. Finally, it will instantiate a new instance of DetailsCmp and place it in the primary outlet of the team component.

Using Params

Often components rely on the state captured in the URL. For instance, the team component probably need to access the id parameter. We can get the parameters by injecting the ActivatedRoute object.

@Component({   selector: 'team',   template: `     Team Id: {{id | async}}     primary: { <router-outlet></router-outlet> }   ` }) class TeamCmp {   id: Observable<string>;   constructor(r: ActivatedRoute) {     //r.params is an observable     this.id = r.params.map(r => r.id);   } } 

If we navigate from /team/3/details to /team/4/details , the params observable will emit a new map of parameters. So the team component will display Team Id: 4 .

Now, say the details component also needs to react to the team id changes. This is how we can do it:

@Component({   selector: 'team',   template: `     Team Id: {{id | async}}     <router-outlet></router-outlet>   ` }) class TeamCmp {   id:Observable<string>;   constructor(r: ActivatedRoute) {     this.id = r.params.map(r => r.id);   } }  @Component({   selector: 'details',   template: `     Details for Team Id: {{teamId | async}}   ` }) class DetailsCmp {   teamId:Observable<string>;   constructor(r: ActivatedRoute, router: Router) {     const teamActivatedRoute = router.routerState.parent(r);     this.teamId = teamActivatedRoute.params.map(r => r.id);   } } 

We have the ability to move up and down the router state tree. We also can see that every activated route has its own parameters.

It is easy to imagine how much flexibility we get once we start combining multiple observables from different activated routes.

QueryParams and Fragment

The router state has the following things that are not associated with a particular activated route: query parameters and fragment .

Here’s an example URL with both query ( id=3 ) and fragment ( open=true ) parameters: /team?id=3#open=true .

@Component({   selector: 'team',   template: ` ` }) class MyCmp {   constructor(r: Router) {     const q: Observable<{[k:string]:string}> = r.routerState.queryParams;     const f: Observable<string> = r.routerState.fragment;   } } 

Using Snapshots

As you can see the router exposes route and query parameters as observables, which is convenient most of the time. But not always. Sometimes what we want is a snapshot of the state that we can examine at once.

This is how you can get it:

@Component({   selector: 'team',   template: `     Team Id: {{id}}   ` }) class TeamCmp {   id:string;   constructor(r: ActivatedRoute, router: Router) {     const s: ActivatedRouteSnapshot = r.snapshot;     // matrix params of a particular route     this.id = s.params.id;      const ss: RouterStateSnapshot = router.routerState.snapshot;     // query params are shared     const q: {[k:string]:string} = ss.queryParams;   } } 

Navigation

Angular Router

So at this point the router has parsed the URL, recognized the router state, and instantiated the components. Next, we need to be able to navigate from this router state to another one. There are two ways to accomplish this: by calling router.navigate or by using the RouterLink directive.

Imperative Navigation

To navigate imperatively, inject the Router service and call navigate .

class TeamCmp {   teamId: number;   userName: string;   constructor(private router: Router) {}    onClick(e) {     this.router.navigate(['/team', this.teamId, 'user', this.userName]).then(_ => {        //navigation is done     }); //e.g. /team/3/user/victor   } } 

This code works, but it has a problem–the navigation is absolute. This makes this component harder to reuse and test. To fix it, we just need to pass an activated route to the navigate method, as follows:

class TeamCmp {   private teamId;   private userName;   constructor(private router: Router, private r: ActivatedRoute) {}    onClick(e) {     this.router.navigate(['../, this.teamId, 'user', this.userName], {relativeTo: this.r});   } } 

RouterLink

Another way to navigate around is by using the RouterLink directive.

@Component({   selector: 'team',   directives: ROUTER_DIRECTIVES,   template: `      <a [routerLink]="['../', this.teamId, 'user', this.userName]">Navigate</a>   ` }) class TeamCmp {   private teamId;   private userName; } 

This directive will also update the href attribute when applied to an <a> link element, so it is SEO friendly and the right-click, open-in-new-browser-tab behavior we expect from regular links will work.

More on Syntax

Just to get a feel of what can be passed in to navigate and routerLink, let’s look at the following examples.

// absolute navigation this.router.navigate(['/team', this.teamId, 'details']);  // you can collapse static parts into a single element this.router.navigate(['/team/3/details']);  // also set query params and fragment this.router.navigate(['/team/3/details'], {queryParams: newParams, fragment: 'fragment'});  // e.g., /team/3;extra=true/details this.router.navigate(['/team/3', {extra: true}, 'details']);  // relative navigation to /team/3/details this.router.navigate(['./details'], {relativeTo: this.route});  // relative navigation to /team/3 this.router.navigate(['../', this.teamId], {relativeTo: this.route}); 

Navigation is URL Based, not Route Based

Navigation is URL based, and not route based. Meaning that '../' skips one segment in the URL tree. To see why this is the case, let’s look at the following link:

<a [routeLink]='['../chat', this.chatId, 'details']'>Chat</a> 

Let’s say the link navigates into the ChatCmp and DetailsCmp components, and we would like to load these components lazily, only when the user clicks on the link. At the same time, the href attribute of the link should be set on page load.

The route configuration of ChatCmp and DetailsCmp is not available on page load. This means that we cannot know if '../chat', this.chatId, 'details' are one, two, or three route segments. The only thing we know is that these are three separate URL segments.

This was one of the design constraints when building the router. It should allow deep linking into lazily loaded routes without knowing anything about them. And the links should have the href attribute set. That is why all the navigation in the router is URL based, not route based.

E2E Example

We’ve looked at all the four core operations of the Angular router. Now, let’s look at all of them in action.

Say this is the router configuration of our application.

bootstrap(RootCmp, provideRouter([   { path: '/team/:id', component: TeamCmp,     children: [{ path: '/details', component: DetailsCmp }] } ]); 

When the browser is loading /team/3/details , the router will do the following:

First, it will parse this url string into a URL tree.

UrlSegment(path: 'team', parameters: {}, outlet: primary)   -> UrlSegment(path: '3', parameters: {}, outlet: primary)     -> UrlSegment(path: 'details', parameters: {}, outlet: primary) 

Second, it will use this URL tree to construct a new router state.

ActivatedRoute(component: RootCmp, parameters: {}, outlet: primary)   -> ActivatedRoute(component: TeamCmp, parameters: {id: '3'}, outlet: primary)    -> ActivatedRoute(component: DetailsCmp, parameters: {}, outlet: primary) 

Third, the router will instantiate the team and details components.

Now, let’s say the team component has the following link in its template:

<a [routerLink]="['../', 4, 'details']">Team 4</a> 

And say the user triggers a navigation by clicking on the link.

The router will take this array ['../', 4, 'details'] and will construct a new URL tree.

UrlSegment(path: 'team', parameters: {}, outlet: primary)   -> UrlSegment(path: '4', parameters: {}, outlet: primary)     -> UrlSegment(path: 'details', parameters: {}, outlet: primary) 

Next it will recognize a new router state.

ActivatedRoute(component: RootCmp, parameters: {}, outlet: primary)   -> ActivatedRoute(component: TeamCmp, parameters: {id: '4'}, outlet: primary)     -> ActivatedRoute(component: DetailsCmp, parameters: {}, outlet: primary) 

Finally, it will find that the team and details components are already in place. So it will reuse them and just push a new set of parameters into the params observable of the team component.

Once this is done, the router will take the new URL tree, serialize it into a string, and update the location property.

Angular Router

Summary

We learned quite a few things today. First, we learned what routers are for. They allow us to express all the potential states in which our application can be, and give us a mechanism for navigating from one state to another. We also learned about the four core operations of the Angular router: URL parsing, state recognition, component instantiation, and navigation. Finally, we looked at an e2e example showing the router in action.

We haven’t touched on the following important topics: navigation guards, monitoring, debugging, error handling, and lazy loading. All of these topics will be covered in future blog posts.

You can play with a real application built with the Angular router here . If you want to use it with Angular CLI, make sure to use TypeScript 1.9.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Angular Router

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址