第3章
建立一个待办事项应用
这一章我们会建立一个更复杂的待办事项应用,当然,登录功能也还保留,这样应用就有了多个相对独立的功能模块。以往的Web应用根据不同的功能跳转到不同的功能页面。但目前前端的趋势是开发一个SPA(Single Page Application,单页应用),所以其实我们应该把这种跳转叫视图切换:根据不同的路径显示不同的组件。那我们怎么处理这种视图切换呢?幸运的是,我们无需寻找第三方组件,Angular官方内建了自己的路由模块。我们会在接下来的学习中逐渐了解这个路由是怎么使用的。
同时本章会介绍Angular提供的一套内存仿真Web API,这套API对于有后端依赖的开发者是极大的福音,我们可以不用等待后台开发人员开发完毕就可以自行进行前端开发了。
3.1 建立routing的步骤
由于我们要以路由形式显示组件,因此建立路由前,让我们先把src\app\app.component.html中的<app-login></app-login>删掉。
第一步:在src/index.html中指定基准路径,即在<header>中加入<base href="/">,它指向你的index.html所在的路径,浏览器也会根据这个路径下载css、图像和js文件,所以请将这个语句放在header的最顶端。
第二步:在src/app/app.module.ts中引入RouterModule:
import { RouterModule } from '@angular/router';
第三步:定义和配置路由数组,我们暂时只为login定义路由,仍然在src/app/app.module.ts中的imports中:
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot([
{
path: 'login',
component: LoginComponent
}
])
],
注意,这个形式和其他的比如BrowserModule、FormModule和HTTPModule的表现形式好像不太一样。这里解释一下,forRoot其实是一个静态的工厂方法,它返回的仍然是Module。下面的是Angular API文档给出的RouterModule.forRoot的定义:
forRoot(routes: Routes, config?: ExtraOptions) : ModuleWithProviders
为什么叫forRoot呢?因为这个路由定义是应用在应用根部的,你可能猜到了还有一个工厂方法叫forChild,后面会详细讲述它。接下来,我们看一下forRoot接受的参数,参数看起来是一个数组,每个数组元素是一个形如{path: 'xxx', component: XXXComponent}的对象。这个数组就叫做路由定义(RouteConfig)数组,每个数组元素就叫路由定义,目前我们只有一个路由定义。路由定义这个对象包括若干属性:
path:路由器会用它来匹配路由中指定的路径和浏览器地址栏中的当前路径,如 /login 。
component:导航到此路由时,路由器需要创建的组件,如 LoginComponent 。
redirectTo:重定向到某个path,使用场景的话,比如在用户输入不存在的路径时重定向到首页。
pathMatch:路径的字符匹配策略。
children:子路由数组。
3.1.1 路由插座
运行一下,我们会发现出错了,如图3.1所示。
图 3.1 没有路由插座导致的报错
这个错误看上去应该是没有找到匹配的route,这是由于我们只定义了一个'login',我们再试试在浏览器地址栏输入:http://localhost:4200/login。这次仍然出错,但错误信息变成了下面的样子,意思是我们没有找到一个插头(outlet)去加载LoginComponent。于是,这就引出了router outlet的概念,如果要显示对应路由的组件,我们需要一个插头来装载组件:
error_handler.js:48EXCEPTION: Uncaught (in promise): Error: Cannot find primary outlet to load 'LoginComponent'
Error: Cannot find primary outlet to load 'LoginComponent'
at getOutlet (http://localhost:4200/main.bundle.js:66161:19)
at ActivateRoutes.activateRoutes (http://localhost:4200/main.bundle.js:66088:30)
at http://localhost:4200/main.bundle.js:66052:19
at Array.forEach (native)
at ActivateRoutes.activateChildRoutes (http://localhost:4200/main.bundle.js:66051:29)
at ActivateRoutes.activate (http://localhost:4200/main.bundle.js:66046:14)
at http://localhost:4200/main.bundle.js:65787:56
at SafeSubscriber._next (http://localhost:4200/main.bundle.js:9000:21)
at SafeSubscriber.__tryOrSetError (http://localhost:4200/main.bundle.js:42013:16)
at SafeSubscriber.next (http://localhost:4200/main.bundle.js:41955:27)
下面我们把<router-outlet></router-outlet>写在src\app\app.component.html的末尾,在地址栏中输入http://localhost:4200/login重新看看浏览器中的效果,应用应该正常显示了。但如果输入http://localhost:4200仍然是有异常出现的,我们需要添加一个路由定义来处理。输入http://localhost:4200时相对于根路径的path应该是空,即' '。而我们这时希望将用户仍然引导到登录页面,这就是redirectTo: 'login'的作用。pathMatch: 'full'的意思是必须完全符合路径的要求,也就是说,http://localhost:4200/1是不会匹配到这个规则的,必须严格是http://localhost:4200:
RouterModule.forRoot([
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: LoginComponent
}
])
注意,路径配置的顺序是非常重要的,Angular 2使用“先匹配优先”的原则,也就是说,如果一个路径可以同时匹配几个路径配置的规则,以第一个匹配的规则为准。现在打开浏览器试验一下,功能又恢复了正常。
3.1.2 分离路由定义
但是现在还有一点小不爽,就是直接在app.modules.ts中定义路径并不是很好的方式,因为随着路径定义的复杂,这部分最好还是用单独的文件来定义。现在,新建一个文件src\app\app.routes.ts,将上面在app.modules.ts中定义的路径删除并在app.routes.ts中重新定义:
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { ModuleWithProviders } from '@angular/core';
export const routes: Routes = [
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: LoginComponent
}
];
export const routing: ModuleWithProviders = RouterModule.forRoot(routes);
接下来,在app.modules.ts中引入routing,代码是import { routing } from './app.routes';,
然后在imports数组里添加routing。现在,app.modules.ts看起来是下面这个样子:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { AuthService } from './core/auth.service';
import { routing } from './app.routes';
@NgModule({
declarations: [
AppComponent,
LoginComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
routing
],
providers: [
{provide: 'auth', useClass: AuthService}
],
bootstrap: [AppComponent]
})
export class AppModule { }
现在我们来规划一下根路径'',如果对于根路径我们想建立一个Todo组件,那么我们使用ng g c todo来生成组件,然后在app.routes.ts中加入路由定义。对于根路径,我们不再需要重定向到登录了,我们把它改写成重定向到Todo:
export const routes: Routes = [
{
path: '',
redirectTo: 'todo',
pathMatch: 'full'
},
{
path: 'todo',
component: TodoComponent
},
{
path: 'login',
component: LoginComponent
}
];
在浏览器中键入http://localhost:4200可以看到自动跳转到了todo路径,并且Todo组件也显示出来了,如图 3.2所示。
图3.2 Todo组件