Angular 2+ 监听路由变化动态设置页面标题

如今不少web网站都采用了SPA单页应用,单页面有不少优势:用户体验好、应用响应快、对服务器压力小 等等。同时也有一些缺点:首次加载资源太多,不利于SEO,前进、后退、地址栏须要手动管理。今天咱们实现Angular单页面应用中路由变化设置页面标题,来优化用户的用户体验。能够先去掘金看下效果。稀土掘金html

在AngularJS(1.x)中动态设置页面标题一般是经过一个全局$rootScope对象来完成的,经过$rootScope对象监听路由变化获取当前路由信息并映射到页面标题。在Angular(v2 +)中,解决起来要比1.x容易得多,咱们能够经过注入一个provider,在路由变化事件中使用provider提供的API来动态更新页面标题。react

Title Service

在angular中,咱们能够经过Title来设置页面标题。咱们从platform-browser导入Title, 同时也导入Routergit

import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router';

导入以后,咱们在组件的构造函数中注入他们github

@Component({
  selector: 'app-root',
  templateUrl: `
    <div>
      Hello world!
    </div>
  `
})
export class AppComponent {
  constructor(private router: Router, private titleService: Title) {}
}

在使用Title以前,咱们先看下Title是如何定义的web

export class Title {
  /**
   * Get the title of the current HTML document.
   * @returns {string}
   */
  getTitle(): string { return getDOM().getTitle(); }

  /**
   * Set the title of the current HTML document.
   * @param newTitle
   */
  setTitle(newTitle: string) { getDOM().setTitle(newTitle); }
}

Title类有两个方法,一个用来获取页面标题getTitle, 一个是用来设置页面标题的setTitleapi

要更新页面标题,咱们能够简单的调用setTitle方法:服务器

@Component({...})
export class AppComponent implements OnInit {
  constructor(private router: Router, private titleService: Title) {}
  ngOnInit() {
    this.titleService.setTitle('My awesome app');
  }
}

这样就能够设置咱们的页面标题了,可是很不优雅。咱们接着往下看。app

在AngularJS中,咱们可使用ui-router为每一个路由添加一个自定义对象,自定义的对象在路由器的状态链中继承:ide

// AngularJS 1.x + ui-router
.config(function ($stateProvider) {
  $stateProvider
    .state('about', {
      url: '/about',
      component: 'about',
      data: {
        title: 'About page'
      }
    });
});

在Angular2+中,咱们也能够为每一个路由定义一个data对象,而后再在监听路由变化时作一些额外的逻辑处理就能够实现动态设置页面标题。首先,咱们定义一个基本的路由:函数

const routes: Routes = [{
  path: 'calendar',
  component: CalendarComponent,
  children: [
    { path: '', redirectTo: 'new', pathMatch: 'full' },
    { path: 'all', component: CalendarListComponent },
    { path: 'new', component: CalendarEventComponent },
    { path: ':id', component: CalendarEventComponent }
  ]
}];

在这里定义一个日历应用,他有一个路由/calendar, 还有三个子路由, /all对应日历列表页,new对应新建日历,:id对应日历详情。如今,咱们定义一个data对象而后设置一个title属性来做为每一个页面的标题。

const routes: Routes = [{
  path: 'calendar',
  component: CalendarComponent,
  children: [
    { path: '', redirectTo: 'new', pathMatch: 'full' },
    { path: 'all', component: CalendarListComponent, data: { title: 'My Calendar' } },
    { path: 'new', component: CalendarEventComponent, data: { title: 'New Calendar Entry' } },
    { path: ':id', component: CalendarEventComponent, data: { title: 'Calendar Entry' } }
  ]
}];

好了,路由定义完了,如今咱们看下如何监听路由变化

Routing events

Angular路由配置很是简单,可是路由经过Observables使用起来也很是强大。
咱们能够在根组件中全局监听路由的变化:

ngOnInit() {
  this.router.events
    .subscribe((event) => {
      // example: NavigationStart, RoutesRecognized, NavigationEnd
      console.log(event);
    });
}

咱们要作的就是在导航结束时获取到定义的数据而后设置页面标题,能够检查 NavigationStart, RoutesRecognized, NavigationEnd 哪一种事件是咱们须要的方式,理想状况下NavigationEnd,咱们能够这么作:

this.router.events
  .subscribe((event) => {
    if (event instanceof NavigationEnd) { // 当导航成功结束时执行
      console.log('NavigationEnd:', event);
    }
  });

这样咱们就能够在导航成功结束时作一些逻辑了,由于Angular路由器是reactive响应式的,因此咱们可使用 RxJS 实现更多的逻辑,咱们来导入如下操做符:

import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';

如今咱们已经添加了 filtermapmergeMap 三个操做符,咱们能够过滤出导航结束的事件:

this.router.events
  .filter(event => event instanceof NavigationEnd)
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

其次,由于咱们已经注入了Router类,咱们可使用 routerState 来获取路由状态树获得最后一个导航成功的路由:

this.router.events
  .filter(event => event instanceof NavigationEnd)
  .map(() => this.router.routerState.root)
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

然而,一个更好的方式就是使用 ActivatedRoute 来代替 routerState.root, 咱们能够将其ActivatedRoute注入类中:

import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';

@Component({...})
export class AppComponent implements OnInit {
  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private titleService: Title
  ) {}
  ngOnInit() {
    // our code is in here
  }
}

注入以后咱们再来优化下:

this.router.events
  .filter(event => event instanceof NavigationEnd)
  .map(() => this.activatedRoute)
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

咱们使用 map 转换了咱们观察到的内容,返回一个新的对象 this.activatedRoutestream 流中继续执行。 咱们使用 filter(过滤出导航成功结束)map(返回咱们的路由状态树) 成功地返回咱们想要的事件类型 NavigationEnd

接下来是最有意思的部分,咱们将建立一个while循环遍历状态树获得最后激活的 route,而后将其做为结果返回到流中:

this.router.events
  .filter(event => event instanceof NavigationEnd)
  .map(() => this.activatedRoute)
  .map(route => {
    while (route.firstChild) route = route.firstChild;
    return route;
  })
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

接下来咱们能够经过路由配置的属性来获取相应的页面标题。而后,咱们还须要另外两个运算符:

this.router.events
  .filter(event => event instanceof NavigationEnd)
  .map(() => this.activatedRoute)
  .map(route => {
    while (route.firstChild) route = route.firstChild;
    return route;
  })
  .filter(route => route.outlet === 'primary')  // 过滤出未命名的outlet,<router-outlet>
  .mergeMap(route => route.data)                // 获取路由配置数据
  .subscribe((event) => {
    console.log('NavigationEnd:', event);
  });

如今咱们 titleService 只须要实现:

.subscribe((event) => this.titleService.setTitle(event['title']));

下面看一下最终代码:

import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';

import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser';

@Component({...})
export class AppComponent implements OnInit {
  constructor(
    private router: Router,
    private activatedRoute: ActivatedRoute,
    private titleService: Title
  ) {}
  ngOnInit() {
    this.router.events
      .filter(event => event instanceof NavigationEnd)
      .map(() => this.activatedRoute)
      .map(route => {
        while (route.firstChild) route = route.firstChild;
        return route;
      })
      .filter(route => route.outlet === 'primary')
      .mergeMap(route => route.data)
      .subscribe((event) => this.titleService.setTitle(event['title']));
  }
}

本文翻译自dynamic-page-titles-angular-2-router-events, 本人水平有限,若是有翻译很差的地方欢迎你们联系我

相关文章
相关标签/搜索