英雄指南——路由

版本:4.0.0+2

有一些英雄指南应用的新需求:

  • 添加一个仪表盘 视图。
  • 添加在英雄 视图和 仪表盘 视图之间导航的功能。
  • 用户无论在哪个视图中点击一个英雄,都会导航到所选英雄的详情视图。
  • 用户在邮件中点击一个深链接,会打开一个特定英雄的详情视图。

完成时,用户就能像这样在应用中导航:

把 Angular 路由加入到应用中,以满足这些需求。

更多关于路由器的信息,请看 Routing and Navigation

当完成本章的学习,应用看起来这样——在线示例 (查看源码)。

我们离开的地方

在继续英雄指南之前,检查你是否有如下结构。

如果应用不运行了,启动应用。当你做出修改时,通过刷新浏览器保持继续运行。

行动计划

下面是我们的计划:

  • AppComponent变成应用程序的“壳”,它只处理导航。
  • 把现在由AppComponent关注的英雄移到一个独立的HeroesComponent中。
  • 添加路由。
  • 创建一个新的DashboardComponent组件。
  • 仪表盘 加入导航结构中。

路由导航 的另一个名字。路由器 是从一个视图导航到另一个视图的机制。

拆分 AppComponent

现在的应用载入AppComponent后立刻显示出英雄列表。修改后的应用应该呈现一个选择视图(仪表盘和英雄)的壳,然后默认显示其中之一。

AppComponent组件应该只处理导航,所以你要把英雄列表的显示,从AppComponent移到它自己的HeroesComponent组件中。

HeroesComponent

AppComponent现在的功能已经专注于英雄数据了。与其把AppComponent中所有的东西都移出去,不如索性把它改名为HeroesComponent,然后创建一个单独的AppComponent壳。

按如下来做:

  • 重命名并移动app_component.*文件到src/heroes_component.*
  • 移除导入路径中的src/前缀。
  • 重命名AppComponent类为HeroesComponent(本地重命名,只在这个文件中)。
  • 重命名选择器my-appmy-heroes
  • 改变模板 URL 为heroes_component.html,以及样式文件heroes_component.css
// lib/src/heroes_component.dart (showing renamings only)

@Component(
  selector: 'my-heroes',
  templateUrl: 'heroes_component.html',
  styleUrls: const ['heroes_component.css'],
)
class HeroesComponent implements OnInit {
  HeroesComponent(
      this._heroService,
      );
}

创建 AppComponent

新的AppComponent是应用的“壳”。它将在顶部放一些导航链接,并在下面有个显示区域。

执行这些步骤:

  • 创建lib/app_component.dart文件。
  • 定义一个AppComponent类。
  • 在类的上方添加一个带有my-app选择器的@Component注解。
  • 从 heroes 组件中移动下面的东西到AppComponent
    • title 类属性。
    • @Component 的模板中包含了title绑定的 <h1> 元素。
  • 在应用模板紧跟标题的下面添加<my-heroes>元素,以便你仍然能看到英雄。
  • HeroesComponent添加到AppComponentdirectives列表中,以便 Angular 能够识别<my-heroes>标签。
  • HeroService添加到AppComponentproviders列表中,因为在其它每个视图中你都需要它。
  • HerosComponentproviders列表中移除HeroService,因为它已经被提升到AppComponent了。
  • AppComponent添加import语句。

第一稿看起来这样:

// lib/app_component.dart

import 'package:angular/angular.dart';

import 'src/hero_service.dart';
import 'src/heroes_component.dart';

@Component(
  selector: 'my-app',
  template: '''
    <h1>{{title}}</h1>
    <my-heroes></my-heroes>
  ''',
  directives: const [HeroesComponent],
  providers: const [HeroService],
)
class AppComponent {
  final title = 'Tour of Heroes';
}

刷新浏览器。应用仍然运行,并显示英雄列表。

添加路由

英雄列表应该在用户点击按钮之后显示,而不是自动显示。换句话说,用户应该能够导航到英雄列表。

更新 pubspec 文件

使用 Angular 路由(angular_router)使导航成为可能。由于路由在它自己的包里,首先添加包到应用的 pubspec 文件:

// {toh-4 → toh-5}/pubspec.yaml

dependencies:            
       angular: ^4.0.0            
       angular_forms: ^1.0.0            
+     angular_router: ^1.0.2

不是所有的应用都需要路由,这就是为什么 Angular 路由是在一个独立的、可选的包中。

导入库

Angular 路由是多个服务 (ROUTER_PROVIDERS)
、指令(ROUTER_DIRECTIVES)
和配置类的组合。通过导入路由库来获取它们:

// lib/app_component.dart (router import)

import 'package:angular_router/angular_router.dart';

使路由可用

在应用的 bootstrap 函数中指定 ROUTER_PROVIDERS 来告诉 Angular 你的应用使用了路由。

// web/main.dart

import 'package:angular/angular.dart';
import 'package:angular_router/angular_router.dart';
import 'package:angular_tour_of_heroes/app_component.dart';

void main() {
  bootstrap(AppComponent, [
    ROUTER_PROVIDERS,
    // Remove next line in production
    provide(LocationStrategy, useClass: HashLocationStrategy),
  ]);
}
使用哪个位置策略

默认的 LocationStrategyPathLocationStrategy,所以在生产环境中,你可以使用没有 LocationStrategy 提供器覆写的ROUTER_PROVIDERS。在开发过程中,由于pub serve不支持深链接,使用 HashLocationStrategy 更方便。详细信息请看附录:位置策略和浏览器 URL 模式

接下来,添加 ROUTER_DIRECTIVES@Component注解,并移除HeroesComponent

// lib/app_component.dart (directives)

directives: const [ROUTER_DIRECTIVES],

由于AppComponent没有直接显示英雄,那是路由的工作,你可以从指令列表中移除HeroesComponent。很快你将从模板中移除<my-heroes>

<base href>

打开index.html并确保在<head>部分的顶部有一个<base href="...">元素(或一个动态设置这个元素的 script 标签)。

正如在路由和导航章节的 Set the base href 部分所述,示例应用使用了下面的脚本:

// web/index.html (base-href)

<head>
  <script>
    // WARNING: DO NOT set the <base href> like this in production!
    // Details: https://webdev.dartlang.org/angular/guide/router
    (function () {
      var m = document.location.pathname.match(/^(\/[-\w]+)+\/web($|\/)/);
      document.write('<base href="' + (m ? m[0] : '/') + '" />');
    }());
  </script>

配置路由

当用户点击链接或者把 URL 粘贴到浏览器地址栏时,路由定义 告诉路由器应该显示哪个视图。

创建一个路由配置(RouteConfig)来保存应用路由定义 的列表。定义第一个路由作为 heroes 组件的路由:

//  lib/app_component.dart (Heroes route)

@RouteConfig(const [
  const Route(path: '/heroes', name: 'Heroes', component: HeroesComponent)
])

路由定义是一个包含如下命名参数的 Route 对象:

  • path:路由器把这个字符串和浏览器地址栏中的URL(/heroes)进行匹配。
  • name:路由名(Heroes)。必须以大写字母开头,避免与路径相混淆。
  • component:当路由被导航到(HeroesComponent)时组件将会被激活。

更多关于路由定义的内容请看——路由和导航

路由插座

如果你访问 localhost:8080/#/heroes,路由器会把 URL 匹配到 heroes 路由,并显示HeroesComponent。然而,你必须告诉路由器在哪里显示这个组件。

为此,在模板的底部添加一个<router-outlet>元素。 RouterOutletROUTER_DIRECTIVES中的一个。当用户在应用中导航时,路由器会立刻在<router-outlet>下面显示每个组件。

刷新浏览器,然后访问 localhost:8080/#/heroes。应该能看到英雄列表。

路由器链接

用户不应该往地址栏中粘贴一个路由地址,而应该在模板中添加一个锚标签。当点击时,触发到HeroesComponent的导航。

修改过的模板看起来是这样的:

// lib/app_component.dart (template)

template: '''
  <h1>{{title}}</h1>
  <a [routerLink]="['Heroes']">Heroes</a>
  <router-outlet></router-outlet>
''',

注意,锚点标签中的[routerLink]绑定。当用户点击这个链接时, RouterLink 指令告诉路由器应该导航到哪里。

你定义了一个带链接参数列表路由指示。在我们的小例子中,列表只有一个元素,用引号引起来的路由的 name 。回去查看路由配置,证实'Heroes'HeroesComponent路由的 name。

更多关于链接参数列表内容请看路由章节。

刷新浏览器。浏览器显示了应用标题和英雄链接,没有英雄列表。点击 Heroes 导航链接。地址栏变成/#/heroes(或是等价的/#heroes),并且英雄列表显示出来了。

AppComponent 现在看起来这样:

// lib/app_component.dart

import 'package:angular/angular.dart';
import 'package:angular_router/angular_router.dart';

import 'src/hero_service.dart';
import 'src/heroes_component.dart';

@Component(
  selector: 'my-app',
  template: '''
    <h1>{{title}}</h1>
    <a [routerLink]="['Heroes']">Heroes</a>
    <router-outlet></router-outlet>
  ''',
  directives: const [ROUTER_DIRECTIVES],
  providers: const [HeroService],
)
@RouteConfig(const [
  const Route(path: '/heroes', name: 'Heroes', component: HeroesComponent)
])
class AppComponent {
  final title = 'Tour of Heroes';
}

AppComponent 有一个路由器,并且显示路由视图。因此,为了和其它类型的组件区分,这种组件类型称为路由器组件

添加一个仪表盘

只有当多个视图存在的时候,路由才有意义。为了添加其它视图,先创建DashboardComponent占个位置。

// lib/src/dashboard_component.dart (v1)

import 'package:angular/angular.dart';

@Component(
  selector: 'my-dashboard',
  template: '<h3>My Dashboard</h3>',
)
class DashboardComponent {}

稍后你将会使这个组件更有用。

配置仪表盘路由

添加一个和英雄路由相似的仪表盘路由:

// lib/app_component.dart (Dashboard route)

const Route(
  path: '/dashboard',
  name: 'Dashboard',
  component: DashboardComponent,
),

添加重定向路由

当前,浏览器启动时地址栏是/。当应用开始时,它应该显示仪表盘,并在地址栏中显示路径/#/dashboard

添加一个重定向路由来实现这一点:

// lib/app_component.dart (Redirect route)

const Redirect(path: '/', redirectTo: const ['Dashboard']),

或者,你可以定义Dashboard默认路由。更多关于默认路由重定向的内容请看路由与导航章节。

添加导航到仪表盘

在模板中添加一个仪表盘导航链接,就放在Heroes链接的上方。

// lib/app_component.dart (template)

template: '''
  <h1>{{title}}</h1>
  <nav>
    <a [routerLink]="['Dashboard']">Dashboard</a>
    <a [routerLink]="['Heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
''',

<nav>标签现在什么也不做,但稍后,当你对这些链接添加样式时,它们会很有用。

在浏览器中打开,进入到应用的根(/)路径并重新加载。应用显示了仪表盘,并且你可以在仪表盘和英雄列表之间导航。

给仪表盘添加英雄

为了使仪表盘更有趣,你会一眼就能看到四个顶级英雄。

使用templateUrl属性替换template元数据,它将指向一个新的模板文件,同时添加如下所示的指令(很快你会添加必要的导入):

// lib/src/dashboard_component.dart (metadata)

@Component(
  selector: 'my-dashboard',
  templateUrl: 'dashboard_component.html',
  directives: const [CORE_DIRECTIVES, ROUTER_DIRECTIVES],
)

templateUrl的值可以是这个包或其它包的资源。当使用另一个包的资源时,使用完整的包引用,例如:'package:some_other_package/dashboard_component.html'

创建的模板文件包含以下内容:

// lib/src/dashboard_component.html

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <div *ngFor="let hero of heroes">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </div>
</div>

再次使用*ngFor来遍历一个英雄列表,并显示它们的名字。额外的<div>元素,有助于稍后的样式美化。

共享 HeroService

你可以再次使用HeroService来填充组件的heroes列表。

早先,从HeroesComponentproviders列表中移除了HeroService,并把它添加到AppComponentproviders列表中。这个移动创建了一个单独的HeroService实例,它对应用中的所有组件都有效。Angular 会注入HeroService,然后你可以在DashboardComponent中使用它了。

获取英雄数据

dashboard_component.dart添加如下import语句。

// lib/src/dashboard_component.dart (imports)

import 'dart:async';

import 'package:angular/angular.dart';
import 'package:angular_router/angular_router.dart';

import 'hero.dart';
import 'hero_service.dart';

现在创建DashboardComponent类,像这样:

// lib/src/dashboard_component.dart (class)

class DashboardComponent implements OnInit {
  List<Hero> heroes;

  final HeroService _heroService;

  DashboardComponent(this._heroService);

  Future<Null> ngOnInit() async {
    heroes = (await _heroService.getHeroes()).skip(1).take(4).toList();
  }
}

这种逻辑也用于HeroesComponent

  • 定义一个heroes列表属性。
  • HeroService注入到构造函数,并且把它保存在一个私有的_heroService字段中。
  • 在 Angular 的ngOnInit()生命周期钩子里,调用服务获取英雄。

在这个仪表盘中,指定了四个英雄(第 2 、 3 、 4 、 5 个)。

刷新浏览器,在这个新的仪表盘中会看到四个英雄。

导航到英雄详情

虽然所选英雄的详情显示在了HeroesComponent的底部,但用户应该能够通过以下额外的方式导航到HeroDetailComponent

  • 从仪表盘到所选英雄。
  • 从英雄列表到所选英雄。
  • 从粘贴到浏览器地址栏的“深链接” URL。

路由到一个英雄详情

AppComponent中定义其它路由的地方,添加一个到HeroDetailComponent的路由。

这个新路由的不寻常之处在于,必须告诉HeroDetailComponent该显示哪个英雄。你不需要告诉 HeroesComponentDashboardComponent任何事情。

现在,父组件HeroesComponent使用如下绑定设置组件的hero属性到一个英雄对象:

<hero-detail [hero]="selectedHero"></hero-detail>

但这种绑定在任意路由场景中都无法工作。

参数化路由

你可以添加英雄的id到路由路径中。当路由到一个id为 11 的英雄时,你可能期望看到像这样的路径:

/detail/11

/detail/部分是固定不变的。但后面跟着的数字id部分会随着英雄的不同而变化。你需要使用代表英雄id参数 来表示路由的可变部分。

添加带参数的路由

首先,导入英雄详情组件:

import 'src/hero_detail_component.dart';

然后,添加如下路由:

// lib/app_component.dart (HeroDetail route)

const Route(
  path: '/detail/:id',
  name: 'HeroDetail',
  component: HeroDetailComponent,
),

路径中的冒号(:)表明:id是一个导航到HeroDetailComponent时,特定英雄id的占位符。

你已经完成了本应用的路由。

你没有往模板中添加一个英雄详情链接,这是因为用户不会直接点击一个导航链接 去查看一个特定的英雄;他们只会点击英雄名,不论是显示在仪表盘上的名字还是在英雄列表中的名字。但这并不工作,直到HeroDetailComponent被修改好并且能够被导航过去。

修改 HeroDetailComponent

这里是HeroDetailComponent现在的样子:

// lib/src/hero_detail_component.dart (current)

import 'package:angular/angular.dart';
import 'package:angular_forms/angular_forms.dart';
import 'hero.dart';
@Component(
  selector: 'hero-detail',
  template: '''
    <div *ngIf="hero != null">
      <h2>{{hero.name}} details!</h2>
      <div><label>id: </label>{{hero.id}}</div>
      <div>
        <label>name: </label>
        <input [(ngModel)]="hero.name" placeholder="name"/>
      </div>
    </div>
  ''',
  directives: const [CORE_DIRECTIVES, formDirectives],
)
class HeroDetailComponent {
  @Input()
  Hero hero;
}

模板不用修改。英雄名会用同样的方式显示。主要的变化是如何获取英雄名。

你不会再从父组件的属性绑定中接收英雄,所以你可以从hero字段移除 @Input() 注解

// lib/src/hero_detail_component.dart (hero with @Input removed)

class HeroDetailComponent implements OnInit {
  Hero hero;
}

新的HeroDetailComponent会从路由器的RouteParams服务中得到id参数,并使用HeroService,通过这个id来获取英雄。

添加下面的导入:

// lib/src/hero_detail_component.dart (added-imports)

import 'dart:async';

import 'package:angular_router/angular_router.dart';

import 'hero_service.dart';

RouteParamsHeroServiceLocation 服务注入到构造函数中,并将它们的值保存到私有字段:

// lib/src/hero_detail_component.dart (constructor)

final HeroService _heroService;
final RouteParams _routeParams;
final Location _location;

HeroDetailComponent(this._heroService, this._routeParams, this._location);

告诉这个类,要实现OnInit接口。

class HeroDetailComponent implements OnInit {

ngOnInit生命周期钩子中,从RouteParams服务中提取id参数值,并且使用HeroService通过这个id来获取英雄。

// lib/src/hero_detail_component.dart (ngOnInit)

Future<Null> ngOnInit() async {
  var _id = _routeParams.get('id');
  var id = int.parse(_id ?? '', onError: (_) => null);
  if (id != null) hero = await (_heroService.getHero(id));
}

注意我们是怎样通过调用RouteParams.get()方法提取id的。

英雄的id是一个数字。而路由参数总是字符串。所以路由参数的值被转换成了数字。

添加 HeroService.getHero()

ngOnInit()中,你使用了一个HeroService还没有的getHero()方法。打开HeroService并添加一个通过idgetHeroes()过滤英雄列表的getHero()方法。

// lib/src/hero_service.dart (getHero)

Future<Hero> getHero(int id) async =>
    (await getHeroes()).firstWhere((hero) => hero.id == id);

找到回来的路

用户有多种方式导航到HeroDetailComponent

用户可以点击AppComponent中的两个链接,或点击浏览器的“后退”按钮,来导航到其它地方。现在添加第三种方式,一个goBack()方法,它使用之前注入的Location服务在浏览器的历史栈中后退一步。

// lib/src/hero_detail_component.dart (goBack)

void goBack() => _location.back();

回退太多步可能会使用户离开应用。在真实的应用种,你可以使用 routerCanDeactivate() 钩子来阻止这个问题。更多内容请看 CanDeactivate 章节。

在组件模板中添加 Back 按钮,并使用事件绑定绑定到这个方法。

<button (click)="goBack()">Back</button>

把模板移到它自己的hero_detail_component.html文件中:

// lib/src/hero_detail_component.dart (metadata)

<div *ngIf="hero != null">
  <h2>{{hero.name}} details!</h2>
  <div>
    <label>id: </label>{{hero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="hero.name" placeholder="name" />
  </div>
  <button (click)="goBack()">Back</button>
</div>

更新组件的元数据,使用templateUrl指向刚刚创建的模板文件。

// lib/src/hero_detail_component.dart (metadata)

@Component(
  selector: 'hero-detail',
  templateUrl: 'hero_detail_component.html',
  directives: const [CORE_DIRECTIVES, formDirectives],
)

刷新浏览器并访问 localhost:8080/#detail/11。11号英雄的详情会被显示。在仪表盘或是英雄列表中选择英雄还不起作用。接下来你将处理这个问题。

选择一个仪表盘中的英雄

当用户从仪表盘中选择了一位英雄时,应用应该导航到HeroDetailComponent以允许用户查看和编辑所选的英雄。

仪表盘英雄的行为应该像锚标签一样:当鼠标移动到一个英雄上时,目标 URL 应该显示在浏览器的状态栏上,并且用户应该能复制链接或者在新标签页打开英雄详情视图。

要实现这种效果,打开dashboard.component.html,使用锚点代替<div *ngFor...>(子元素保持不变):

// lib/src/dashboard_component.html (repeated <a> tag)

<a *ngFor="let hero of heroes" [routerLink]="['HeroDetail', {id: hero.id.toString()}]" class="col-1-4">
  <div class="module hero">
    <h4>{{hero.name}}</h4>
  </div>
</a>

注意 [routerLink] 绑定。正如本章路由链接 部分所述,AppComponent模板中的顶级导航有两个设置为固定的目标路由名,/dashboard/heroes

这次,你绑定到了一个包含链接参数列表的表达式。该列表有两个元素:目标路由名字和一个设置为当前英雄id值的路由参数

这两个列表项分别对应之前在参数化英雄详情路由定义中添加的name:id

// lib/app_component.dart (HeroDetail route)

const Route(
  path: '/detail/:id',
  name: 'HeroDetail',
  component: HeroDetailComponent,
),

刷新浏览器,并从仪表盘中选择一位英雄;应用就会导航到该英雄的详情。

HeroesComponent 中选择一位英雄

HeroesComponent中,当前模板展示了一个"主从"风格的视图:上方是英雄列表,底下是所选英雄的详情。

// lib/src/heroes_component.html

<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes"
      [class.selected]="hero === selectedHero"
      (click)="onSelect(hero)">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>
<hero-detail [hero]="selectedHero"></hero-detail>

这里你将不再展示完整的HeroesComponent。相反,你会在它自己的页面显示英雄详情,并像在仪表盘中所做的路由到它。做以下改变:

  • 从模板的最后一行删除<hero-detail>元素。
  • directives列表中移除HeroDetailComponent
  • 移除英雄详情导入。

当用户从列表中选择一个英雄时,他们并不会进入详情页。相反,他们会在页看到一个迷你的英雄详情,并且必须点击一个按钮来导航到完整的英雄详情页。

添加迷你 英雄详情

在模板底部原来放<hero-detail>的地方添加下列 HTML 片段:

// lib/src/heroes_component.html (mini detail)

<div *ngIf="selectedHero != null">
  <h2>
    {{selectedHero.name | uppercase}} is my hero
  </h2>
  <button (click)="gotoDetail()">View Details</button>
</div>

HeroesComponent中添加如下方法:

// lib/src/heroes_component.dart (gotoDetail stub)

Future<Null> gotoDetail() => null;

稍后,点击一个英雄(但现在别做,因为它还不工作),用户应该能在英雄列表下方看到像下面的样子:

英雄的名字被显示成大写字母,因为在插值表达式绑定中,管道操作符(|)的后面,包含了uppercase管道。

{{selectedHero.name | uppercase}} is my hero

管道是一个格式化字符串、货币金额、日期和其它显示数据的好方法。Angular 自带了几个管道,并且你可以写自己的管道。

在你能够在模板中使用 Angular 管道之前,你需要在组件的@Component注解的参数pipes中列出要使用的管道。你可以添加单独的管道,或者使用更方便的管道集合如 COMMON_PIPES

//  lib/src/heroes_component.dart (pipes)

@Component(
  selector: 'my-heroes',
  pipes: const [COMMON_PIPES],
)

更多关于管道的内容请看 Pipes

刷新浏览器。从英雄列表中选择一个英雄将会激活迷你详情视图。现在查看详情按钮还不起作用。

更新 HeroesComponent

点击按钮时,HeroesComponent导航到HeroesDetailComponent。该按钮的点击事件被绑定到gotoDetail()方法,它通过告诉路由器应该去哪儿进行命令式地导航。

该方法需要对组件类做以下改变:

  1. 导入 angular_router
  2. HeroService 一起,在构造函数中注入 Router
  3. 通过调用路由器的navigate()方法,实现 gotoDetail()

下面是修改后的HeroesComponent类:

// lib/src/heroes_component.dart (class)

class HeroesComponent implements OnInit {
  final HeroService _heroService;
  final Router _router;
  List<Hero> heroes;
  Hero selectedHero;

  HeroesComponent(
      this._heroService,
      this._router
      );

  Future<Null> getHeroes() async {
    heroes = await _heroService.getHeroes();
  }

  void ngOnInit() => getHeroes();

  void onSelect(Hero hero) => selectedHero = hero;

  Future<Null> gotoDetail() => _router.navigate([
        'HeroDetail',
        {'id': selectedHero.id.toString()}
      ]);
}

gotoDetail()中,你往路由器的navigate()方法中传递了一个有两个元素的链接参数列表——一个路由名和这个路由的参数,就和之前在DashboardComponent中使用[routerLink]绑定所做的一样。

刷新浏览器,并开始点击。用户能在应用中导航:从仪表盘到英雄详情再回来,从英雄列表到迷你英雄详情,再到英雄详情,再回到英雄列表。

你已经满足了在本章开头设定的所有导航需求。

给应用添加样式

应用的功能已经完成了,但它需要添加样式。仪表盘英雄应该显示在一行的矩形中。你会看到大约 60 行 CSS 来实现它,包括一些为响应式设计而写的简单的媒体查询。

正如你所知道的,在组件的styles元数据中添加这些 CSS 会使组件逻辑模糊不清。所以,在一个独立的.css文件中添加这些 CSS。

仪表盘样式

lib/src目录下创建一个dashboard_component.css 文件,并在组件元数据的styleUrls列表属性中引用它,就像这样:

// lib/src/dashboard_component.dart (styleUrls) 

@Component(
  selector: 'my-dashboard',
  templateUrl: 'dashboard_component.html',
  styleUrls: const ['dashboard_component.css'],
  directives: const [CORE_DIRECTIVES, ROUTER_DIRECTIVES],
)
// lib/src/dashboard_component.css

[class*='col-'] {
  float: left;
  text-decoration: none;
  padding-right: 20px;
  padding-bottom: 20px;
}
[class*='col-']:last-of-type {
  padding-right: 0;
}
*, *:after, *:before {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}
h3 {
  text-align: center; margin-bottom: 0;
}
h4 {
  position: relative;
}
.grid {
  margin: 0;
}
.col-1-4 {
  width: 25%;
}
.module {
    padding: 20px;
    text-align: center;
    color: #eee;
    max-height: 120px;
    min-width: 120px;
    background-color: #607D8B;
    border-radius: 2px;
}
.module:hover {
  background-color: #EEE;
  cursor: pointer;
  color: #607d8b;
}
.grid-pad {
  padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
  padding-right: 20px;
}
@media (max-width: 600px) {
    .module {
      font-size: 10px;
      max-height: 75px; }
}
@media (max-width: 1024px) {
    .grid {
      margin: 0;
    }
    .module {
      min-width: 60px;
    }
}

英雄详情样式

lib/src目录下创建一个hero_detail_component.css文件,并且在组件元数据的styleUrls列表中引用它:

// lib/src/hero_detail_component.dart(styleUrls)

@Component(
  selector: 'hero-detail',
  templateUrl: 'hero_detail_component.html',
  styleUrls: const ['hero_detail_component.css'],
  directives: const [CORE_DIRECTIVES, formDirectives],
)
// lib/src/hero_detail_component.css

label {
  display: inline-block;
  width: 3em;
  margin: .5em 0;
  color: #607D8B;
  font-weight: bold;
}
input {
  height: 2em;
  font-size: 1em;
  padding-left: .4em;
}
button {
  margin-top: 20px;
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer; cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #ccc;
  cursor: auto;
}

给导航链接添加样式

lib目录下创建一个app_component.css文件,并且在组件元数据的styleUrls列表中引用它:

// lib/app_component.dart(styleUrls)

styleUrls: const ['app_component.css'],
// lib/app_component.css

h1 {
  font-size: 1.2em;
  color: #999;
  margin-bottom: 0;
}
h2 {
  font-size: 2em;
  margin-top: 0;
  padding-top: 0;
}
nav a {
  padding: 5px 10px;
  text-decoration: none;
  margin-top: 10px;
  display: inline-block;
  background-color: #eee;
  border-radius: 4px;
}
nav a:visited, a:link {
  color: #607D8B;
}
nav a:hover {
  color: #039be5;
  background-color: #CFD8DC;
}
nav a.router-link-active {
  color: #039be5;
}

提供的 CSS 使得在AppComponent中的导航链接看起来更像是可选按钮。早前,你使用了一个<nav>元素包围着这些链接:

router-link-active CSS 类

Angular 路由器添加router-link-activeCSS 类到那些路由匹配激活的路由的 HTML 导航元素。你唯一要做的就是为它定义样式。

应用的全局样式

当你给一个组件添加样式时,你要使组件所需的一切——HTML、CSS、程序代码,都集中放在一个方便的地方。这样,无论是把它们打包起来还是在别的其它地方复用这个组件都会很容易。

你也可以在所有组件之外创建应用级别的样式。

设计师提供了一些基本的样式,适用于贯穿整个应用的元素。这些和之前在配置开发环境中安装的全套主样式一致。下面是摘录:

// web/styles.css(excerppt)

@import url(https://fonts.googleapis.com/css?family=Roboto);
@import url(https://fonts.googleapis.com/css?family=Material+Icons);

/* Master Styles */
h1 {
  color: #369;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 250%;
}
h2, h3 {
  color: #444;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[text], button {
  color: #888;
  font-family: Cambria, Georgia;
}
/* ··· */
/* everywhere else */
* {
  font-family: Arial, Helvetica, sans-serif;
}

如有必要的话,创建web/styles.css文件。确保文件中包含 master styles provided here 的内容。并且编辑web/index.html来引用这个样式。

// web/index.html(link ref)

<link rel="stylesheet" href="styles.css">

现在看看这个应用。仪表盘、英雄和导航链接都被应用了样式。

应用结构和代码

在线示例 (查看源码)中检查本章的示例源代码。验证你是否已经有如下结构:

angular_tour_of_heroes/
|___lib/
|   |___app_component.{css,dart}
|___src/
|   |   |___dashboard_component.{css,dart,html}
|   |   |___hero.dart
|   |   |___hero_detail_component.{css,dart,html}
|   |   |___hero_service.dart
|   |   |___heroes_component.{css,dart,html}
|   |   |___mock_heroes.dart
|___test/
|   |___app_test.dart
|   |___...
|___web/
|   |___index.html
|   |___main.dart
|   |___styles.css
|___pubspec.yaml

走过的路

以下是你在本章中完成的:

  • 添加 Angular 路由,在不同组件之间导航。
  • 学会了如何创建路由链接来表示导航栏的菜单项。
  • 使用路由链接参数来导航到用户所选英雄的详情。
  • 在多个组件之间共享HeroService服务。
  • 添加uppercase管道来格式化数据。

你的应用看起来应该是这样在线示例 (查看源码)。

下一步

HTTP

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 版本:4.0.0+2 在本章,你会做以下改进。 从一个服务器获取英雄数据。 让用户添加、编辑和删除英雄。 保存改变...
    soojade阅读 949评论 0 3
  • 版本:4.0.0+2 随着英雄指南应用的进化,你将会添加更多的需要访问英雄数据的组件。 你将创建一个单独的可复用的...
    soojade阅读 468评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • #Eric爱分享-1分钟职场智慧# 该如何向领导表达自己的不满呢?直说,做个耿直的人,只要对事不对人就行。这样做当...
    行业观察小朋友阅读 511评论 0 0