作者:王芃 wpcfan@gmail.com
第一节:Angular2 从0到1 (一)
第二节:Angular2 从0到1 (二)
第三节:建立一个待办事项应用
这一章我们会建立一个更复杂的待办事项应用,当然我们的登录功能也还保留,这样的话我们的应用就有了多个相对独立的功能模块。以往的web应用根据不同的功能跳转到不同的功能页面。但目前前端的趋势是开发一个SPA(Single Page Application 单页应用),所以其实我们应该把这种跳转叫视图切换:根据不同的路径显示不同的组件。那我们怎么处理这种视图切换呢?幸运的是,我们无需寻找第三方组件,Angular官方内建了自己的路由模块。
建立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:子路由数组
运行一下,我们会发现出错了
这个错误看上去应该是对于”没有找到匹配的route,这是由于我们只定义了一个’login’,我们再试试在浏览器地址栏输入:http://localhost:4200/login
。这次仍然出错,但错误信息变成了下面的样子,意思是我们没有找到一个outlet去加载LoginComponent。对的,这就引出了router outlet的概念,如果要显示对应路由的组件,我们需要一个插头(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
}
])
注意路径配置的顺序是非常重要的,Angular2使用“先匹配优先”的原则,也就是说如果一个路径可以同时匹配几个路径配置的规则的话,以第一个匹配的规则为准。
但是现在还有一点小不爽,就是直接在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';
export const routes: Routes = [
{
path: '',
redirectTo: 'login',
pathMatch: 'full'
},
{
path: 'login',
component: LoginComponent
}
];
export const routing = 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 { Routes, RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { AuthService } from './services/auth.service';
import { routing } from './app.routes';
@NgModule({
declarations: [
AppComponent,
LoginComponent,
TodoComponent
],
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组件也显示出来了。
我们希望的Todo页面应该有一个输入待办事项的输入框和一个显示待办事项状态的列表。那么我们先来定义一下todo的结构,todo应该有一个id用来唯一标识,还应该有一个desc用来描述这个todo是干什么的,再有一个completed用来标识是否已经完成。好了,我们来建立这个todo模型吧,在todo文件夹下新建一个文件todo.model.ts
export class Todo {
id: number;
desc: string;
completed: boolean;
}
然后我们应该改造一下todo组件了,引入刚刚建立好的todo对象,并且建立一个todos数组作为所有todo的集合,一个desc是当前添加的新的todo的内容。当然我们还需要一个addTodo方法把新的todo加到todos数组中。这里我们暂且写一个漏洞百出的版本。
import { Component, OnInit } from '@angular/core';
import { Todo } from './todo.model';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
todos: Todo[] = [];
desc = '';
constructor() { }
ngOnInit() {
}
addTodo(){
this.todos.push({id: 1, desc: this.desc, completed: false});
this.desc = '';
}
}
然后我们改造一下src\app\todo\todo.component.html
<div>
<input type="text" [(ngModel)]="desc" (keyup.enter)="addTodo()">
<ul>
<li *ngFor="let todo of todos">{
{ todo.desc }}</li>
</ul>
</div>
如上面代码所示,我们建立了一个文本输入框,这个输入框的值应该是新todo的描述(desc),我们想在用户按了回车键后进行添加操作((keyup.enter)="addTodo()
)。由于todos是个数组,所以我们利用一个循环将数组内容显示出来(<li *ngFor="let todo of todos">{
{ todo.desc }}</li>
)。好了让我们欣赏一下成果吧
如果我们还记得之前提到的业务逻辑应该放在单独的service中,我们还可以做的更好一些。在todo文件夹内建立TodoService:ng g s todo\todo
。上面的例子中所有创建的todo都是id为1的,这显然是一个大bug,我们看一下怎么处理。常见的不重复id创建方式有两种,一个是搞一个自增长数列,另一个是采用随机生成一组不可能重复的字符序列,常见的就是UUID了。我们来引入一个uuid的包:npm i --save angular2-uuid
,由于这个包中已经含有了用于typescript的定义文件,这里就执行这一个命令就足够了。
然后修改service成下面的样子:
import { Injectable } from '@angular/core';
import {Todo} from './todo.model';
import { UUID } from 'angular2-uuid';
@Injectable()
export class TodoService {
todos: Todo[] = [];
constructor() { }
addTodo(todoItem:string): Todo[] {
let todo = {
id: UUID.UUID(),
desc: todoItem,
completed: false
};
this.todos.push(todo);
return this.todos;
}
}
当然我们还要把组件中的代码改成使用service的
import { Component, OnInit } from '@angular/core';
import { Todo } from './todo.model';
import { TodoService } from './todo.service';
@Component({
selector: 'app-todo',
templateUrl: './todo.component.html',
styleUrls: ['./todo.component.css'],
providers:[TodoService]
})
export class TodoComponent implements OnInit {
todos: Todo[] = [];
desc = '';
constructor(private service:TodoService) { }
ngOnInit() {
}
addTodo(){
this.todos = this.service.addTodo(this.desc);