第八章 多用户与路由守卫
点此处可以下载本章完成后的 示例代码 。运行方法为:
- 解压缩文件,进入
heroes
项目根目录。- 使用
npm install
补上依赖包。也可以把之前备份的node_modules
目录复制一份在项目根目录下。- 使用
json-server data.json
启动后端服务。- 使用
npm start
运行项目,打开浏览器访问http://localhost:4200/
进入项目。
1. 概述
我们需要这么一个机制
- 建立一种召唤师机制,让每个英雄有自己的召唤师,召唤师可以控制多个英雄
- 做好隐私保护工作:每个召唤师只能更改自己的英雄信息
- 非召唤师是不能进入某些页面的
(1)验证用户账户流程
一个完整的验证流程如下:
- 存储要访问的URL
- 根据本地的用户已登录标识判断用户是否已登录,如果已登录就放行,如果未登录,转入登录页面
- 在登录页面中,用户输入用户名后系统根据填入的用户名在用户表中查找,若没找到,报错
- 在用户输入密码后判断密码与用户名是否匹配,若不匹配,报错
- 若匹配,存储已登录标识至本地,导航到原本要访问的URL即第一步中存储的URL,删掉本地存储的URL
看上去我们需要实现以下三个Service
- UserService:里面有对对象User所有的查找select,新增create,删除delete,修改update操作
- AuthService:里面有所有用于用户认证相关的方法,其中有些需要利用到UserService中的方法
- AuthGuard:路由拦截器,用于对目标路由进行拦截后通过AuthService来看此用户是否有权限访问该路由,根据判断结果导航到不同路径
流程图:
(2)路由守卫
Angular提供了路由守卫机制来处理以下需求:
- 判断该用户是否有权导航到目标组件
- 在显示目标组件前,我们可能得先得到某些数据
- 在离开组件前,我们可能要先保存修改,或放弃更改 ...
我们可以在路由那一章中的app-routing.module.ts中给一些需要路由守卫的地方添加守卫:
- CanActivate能处理导航到某路由的情况,这也是作为本章范例的路由
- CanActivate能处理导航到某子路由的情况
- CanDeactivate能处理从当前路由离开的情况
- Resoulve能在路由激活之前获取路由数据
在路由树(分层路由)的每一层上,我们都能设置一些路由守卫。路由器会先按照从子路由到根路由的顺序来检查CanDeactivate守护条件,然后会按照从根路由到子路由的顺序检查CanActivate条件,如果任何 守卫返回dalse,其它尚未完成的守卫会被取消,这样整个导航就被取消了。
路由守卫图示
2. 增加召唤师角色
(1) 修改domain文件
我们需要给英雄们添加召唤师id(ownerId)这一属性来对应每个召唤师:
src/app/domain/hero.ts
export class Hero {
id: number;
name: string;
gender: string;
desc:string;
ownerId: number;
}
我们还需要有对应的召唤师对象,在domain中新建user.ts对象文件
src/app/domain/user.ts
export class User {
id: number;
username: string;
password: string;
name: string;
}
(2) 修改json文件
相信大家在前面已经学会了用json模拟数据库的方法,我们修改一下data.json文件,首先人为的给已有英雄添加ownerId属性,表示这一位英雄属于哪个召唤师。然后我们再在data.json中加上user对象的json表现形式:
src/app/data.json
{
"heroes": [
{
"id": 1,
"name": "The Monkey King",
"gender":"MALE",
"desc": "孙悟空",
"ownerId" :"1"
},
{
"id": 2,
"name": "Monk Pig",
"gender":"MALE",
"desc": "猪八戒",
"ownerId" :"1"
},
{
"id": 3,
"name": "Friar Sand",
"gender":"MALE",
"desc": "沙和尚",
"ownerId" :"1"
},
{
"id": 4,
"name": "Harry Potter",
"gender":"MALE",
"desc": "哈利·波特",
"ownerId" :"2"
},
{
"id": 5,
"name": "Ron Weasley",
"gender":"MALE",
"desc": "罗恩·韦斯莱",
"ownerId" :"2"
},
{
"id": 6,
"name": "Hermione Granger",
"gender":"FEMALE",
"desc": "赫敏·格兰杰",
"ownerId" :"2"
}
],
"users":[
{
"id":1,
"username": "user1",
"password": "123456",
"name": "The Tang Monk"
},
{
"id":2,
"username": "user2",
"password": "123456",
"name": "Albus Dumbledore"
}
]
}
我们首先要先实现路由守卫(不该看的地方不能看)以及登录功能,所以就先在json文件里模拟了一条user数据,需要注意的是这些数据都是明文,既简陋也不安全,具体的安全问题会在以后的章节讲述,在这里我们先不管它。
3. 增加核心模块core
下面我们来吧与认证(Auth)相关的代码组织在一个新的模块下,这个模块可以随便命名,我们将其命名为core,
使用 ng g m core
命令,结果会在src/app下新建一个core目录,然后在core目录下新建一个core.module.ts。
src/app/core/core.module.ts(v1)
import { NgModule, ModuleWithProviders, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [
CommonModule
]
})
export class CoreModule {
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'CoreModule is already loaded.');
}
}
}
有可能有这么一个服务,它会在网页的很多部分中被引用,但我们不希望它被创建多次。也有可能某些只应用于AppComponent模板的一次性组件,没必要共享它们,然而把它们放在根模块下显得太乱。我们就可以把这样的服务放在core模块下,然后通过AppModule导入CoreModule获得服务。
以上代码我们只在启动时导入它一次,注意到在constructor构造器中我们会把一个类型为CoreModule的parentModule注入自身,这看起来像是一个危险的循环,但@SkipSelf装饰器可以在当前注入器所有祖先注入器中寻找CoreModule。如果该构造函数在我们所期望的AppModule中运行,就没有任何祖先注入器能够提供CoreModule的实例,于是注入器会放弃查找。默认情况下,当注入器找不到想找的提供商时,会抛出一个错误。 但@Optional装饰器表示找不到该服务也无所谓。 于是注入器会返回null,parentModule参数也就被赋成了空值,而构造函数没有任何异常。
别忘了在app.module.ts中导入core模块
src/app/app.module.ts
import { CoreModule } from './core/core.module';
...
imports: [
...
CoreModule,
...
],
...
4. 增加一个导航栏组件 NavbarComponent
我们希望用户未登录的时候不能访问自己的个人中心,并且导航到登录页面,那么就需要用到CanActivate。 先增加一个导航栏组件 NavBarComponent:
ng g c navbar
在导航栏组件NavbarComponent.html下添加一个路由到个人中心的button。
src/app/navbar/navbar.component.html
<nav>
<a routerLink="/heroes" routerLinkActive="active" id="Heroes">Heroes</a>
<a routerLink="/profile" routerLinkActive="active" id="Profile">Profile</a>
</nav>
<div>
<router-outlet></router-outlet>
</div>
src/app/navbar/navbar.component.css
/* Navigation link styles */
nav a {
padding: 5px 10px;
text-decoration: none;
margin-right: 10px;
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.active {
color: #039be5;
}
修改 src/app/app.component.html ,增加navbar
src/app/app.component.html(v1)
<h1 class="title">Hello My Heroes</h1>
<app-navbar></app-navbar>
<router-outlet></router-outlet>
5. 个人中心 profile
接下来,新建一个个人中心模块ProfileModule,别忘记在AppModule中引入ProfileModule:
ng g m profile
修改 app.module.ts ,增加 profile.module
src/app/app.module.ts (v1)
import { ProfileModule } from './profile/profile.module';
imports: [
ProfileModule
],
我们还需要与Profile模块配套的组件profile.component,输入:
ng g c profile
建好组件之后我们还需要一个profile-routing.module来控制profile内部的路由,在profile目录下新建profile-routing.module.ts文件:
src/app/profile/profile-routing.module.ts (v1)
//import angular core
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
//import my components
import { ProfileComponent } from './profile.component';
const routes: Routes = [
{
path: '',
component: ProfileComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProfileRoutingModule { }
别忘记把profile-routing模块引入profile.module中。
src/app/profile/profile.module.ts (v1)
//import angular core
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
//import my modules
import { ProfileRoutingModule } from './profile-routing.module';
//import my components
import { ProfileComponent } from './profile.component';
@NgModule({
imports: [
CommonModule,
ProfileRoutingModule,
FormsModule
],
declarations: [
ProfileComponent
]
})
export class ProfileModule { }
6. 增加路由守卫
增加一个auth-guard服务
ng g s service/auth-guard
来新建AuthGuardService,注意angular-cli对于Camel写法的文件名是才用-来分割每个首字母要大写的单词
下面在app-routing.module.ts中填入这些组件和服务
src/app/app-routing.module.ts(v1)
//import angular core
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
//import my mudules
import { HeroesModule } from './heroes/heroes.module';
import { ProfileModule } from './profile/profile.module';
//import my components
//import my service
import { AuthGuardService } from './service/auth-guard.service';
const routes: Routes = [
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
{
path: 'profile',
canActivate: [AuthGuardService],
loadChildren: 'app/profile/profile.module#ProfileModule'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
下面我们实现AuthGuardService
src/app/service/auth-guard.service.ts
import { Injectable, Inject } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
@Injectable()
export class AuthGuardService implements CanActivate {
constructor(private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
let url: string = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
//已登录
if (localStorage.getItem('userId') !== null) {
//返回true,放行
return true;
}
//未登录
else {
//首先将要访问的URL暂存
localStorage.setItem('redirectUrl', url);
//然后导航到登录页面
this.router.navigate(['/login']);
//返回false,取消本次导航
return false;
}
}
}
我们注意上面代码中
(localStorage.getItem('userId') !== null)
这里使用检测本地是否存储userId来判断用户是否登录,这是一个漏洞百出的实现,本例中先这样使用,而在实际的项目开发中,一定要选用其它实现方式或者搭配后端代码来提升安全性。
7. 增加 AuthService
下面我们还需要一个为登录功能服务的service,来判断用户名密码是否输入正确,如果正确那就把登录状态写入localStorage。以后也会用它来为注册功能进行服务 在命令行或终端中输入:
ng g s service/auth
打开auth.service.ts
src/app/service/auth.service.ts
import { Injectable, Inject } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Auth } from '../domain/auth';
@Injectable()
export class AuthService {
constructor(private http: Http, @Inject('user') private userService) { }
loginWithCredentials(username: string, password: string): Promise<Auth> {
return this.userService.getUserByUsername(username) //调用UserService中的方法来查找user
.then(user => {
let auth = new Auth();
localStorage.removeItem('userId'); //首先移除当前本地存储的userId
let redirectUrl = (localStorage.getItem('redirectUrl') === null) ?
'/' : localStorage.getItem('redirectUrl');
auth.redirectUrl = redirectUrl; //存储原本要访问的Url
if (null === user) {
//没找到user
auth.hasError = true;
auth.errMsg = 'user not found';
} else if (password === user.password) {
//找到user并与密码匹配成功
auth.user = Object.assign({}, user);
auth.hasError = false;
localStorage.setItem('userId', user.id);
} else {
//密码错误
auth.hasError = true;
auth.errMsg = 'password not match';
}
return auth;
})
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error);
return Promise.reject(error.message || error);
}
}
我们注意到这里面用到了一个新的对象Auth与新的服务UserService Auth对象:
- 我们需要存储用户最初要导航到的页面URL
- 用户对象
- 如果发生错误,存储错误信息
所以我们还需要声明Auth对象:
src/app/domain/auth.ts
import { User } from './user';
export class Auth {
user: User;
hasError: boolean;
errMsg: string;
redirectUrl: string;
}
8. 增加 UserService
我们还需要实现UserService,在终端或命令行中输入:
ng g s service/user
src/app/service/user.service.ts
import { Injectable } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { User } from '../domain/user';
import { ApiService } from './api.service';
@Injectable()
export class UserService {
private api_url : string ;
private headers : Headers ;
constructor(private http: Http, private apiService: ApiService) {
this.api_url = apiService.getUrl() + '/users';
this.headers = apiService.getHeaders();
}
//查询所有User
getUsers(): Promise<User[]> {
const url = `${this.api_url}`;
return this.http.get(url, { headers: this.headers })
.toPromise()
.then(res => res.json() as User[])
.catch(this.handleError);
}
//按id查询User
getUserById(id: number): Promise<User> {
const url = `${this.api_url}/${id}`;
return this.http.get(url, { headers: this.headers })
.toPromise()
.then(res => res.json() as User)
.catch(this.handleError);
}
//按username查询User
getUserByUsername(username: string): Promise<User> {
const url = `${this.api_url}/?username=${username}`;
return this.http.get(url, { headers: this.headers })
.toPromise()
.then(res => {
let users = res.json() as User[];
return (users.length > 0) ? users[0] : null;
})
.catch(this.handleError);
}
//创建一个User
createUser(user: User): Promise<User> {
const url = `${this.api_url}`;
return this.http
.post(url, JSON.stringify(user), { headers: this.headers })
.toPromise()
.then(res => res.json() as User)
.catch(this.handleError);
}
//修改某个User
updateUser(user: User): Promise<User> {
const url = `${this.api_url}/${user.id}`;
return this.http
.put(url, JSON.stringify(user), { headers: this.headers })
.toPromise()
.then(() => user)
.catch(this.handleError);
}
//删除某个User
deleteUser(user: User): Promise<void> {
const url = `${this.api_url}/${user.id}`;
return this.http.delete(url, { headers: this.headers })
.toPromise()
.then(() => null)
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error);
return Promise.reject(error.message || error);
}
}
以上UserService已经实现了查找所有User,按id查找某个User,按username查找某个User,还有插,改,删的操作,在这里我们先只用了按username查找某个User的方法
9. 增加 login 组件
下面我们来实现login组件 依旧在命令行或终端中输入:
ng g c login
src/app/login/login.component.html
<br>
<div>
<form #formRef="ngForm" (ngSubmit)="onSubmit(formRef.value)">
<fieldset ngModelGroup="login">
<div>
<label class ="mark">Username: </label>
<input name="username" type="text" [(ngModel)]="username" #usernameRef="ngModel" required minlength="3">
<label *ngIf="usernameRef.errors?.required">this is required</label>
<label *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</label>
<label *ngIf="auth?.hasError">{{auth.errMsg}}</label>
</div>
<div>
<label class ="mark">Password: </label>
<input name="password" type="password" [(ngModel)]="password" #passwordRef="ngModel" required>
<label *ngIf="passwordRef.errors?.required">this is required</label>
</div>
<button type="submit">Login</button>
</fieldset>
</form>
</div>
src/app/login/login.component.ts
import { Component, OnInit, Inject } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { Auth } from '../domain/auth';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
username = '';
password = '';
auth: Auth;
constructor(@Inject('auth') private service, private router: Router) { }
ngOnInit() {
}
onSubmit(formValue){
this.service
.loginWithCredentials(formValue.login.username, formValue.login.password)
.then(auth => {
let redirectUrl = (auth.redirectUrl === null)? '/': auth.redirectUrl;
if(!auth.hasError){
this.router.navigate([redirectUrl]);
localStorage.removeItem('redirectUrl');
location.reload();
} else {
this.auth = Object.assign({}, auth);
}
});
}
}
src/app/login/login.component.css
label {
display: inline-block;
width: 10em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
.mark{
display: inline-block;
width: 6em;
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;
}
在导航栏组件中加入login按钮:
src/app/navbar/navbar.component.html
<nav>
<a routerLink="/heroes" routerLinkActive="active" id="Heroes">Heroes</a>
<a routerLink="/profile" routerLinkActive="active" id="Profile">Profile</a>
<a routerLink="/login" routerLinkActive="active" id="Login">Login</a>
</nav>
<div>
<router-outlet></router-outlet>
</div>
在core模块中声明我们所需要的服务
src/app/core/core.module.ts(v2)
//import angular core
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
//import service
import { AuthService } from '../service/auth.service';
import { UserService } from '../service/user.service';
import { AuthGuardService } from '../service/auth-guard.service';
@NgModule({
imports: [
CommonModule
],
providers: [
{ provide: 'auth', useClass: AuthService },
{ provide: 'user', useClass: UserService },
AuthGuardService
]
})
export class CoreModule {
constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'CoreModule is already loaded.');
}
}
}
给app-routing.module.ts添加上login路由组件
app-routing.module.ts(v2)
//import angular core
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
//import my mudules
import { HeroesModule } from './heroes/heroes.module';
import { ProfileModule } from './profile/profile.module';
//import my components
import { LoginComponent } from './login/login.component';
//import my service
import { AuthGuardService } from './service/auth-guard.service';
const routes: Routes = [
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
{
path: 'profile',
canActivate: [AuthGuardService],
loadChildren: 'app/profile/profile.module#ProfileModule'
},
{
path: 'login',
component: LoginComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
在有关登录的最后,我们再来检查一下app.module.ts,看看有没有哪些模块忘记导入了
src/app/app.module.ts
//import angular core
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { CoreModule } from './core/core.module';
import { ProfileModule } from './profile/profile.module';
//import my modules
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
//import my components
import { AppComponent } from './app.component';
import { NavbarComponent } from './navbar/navbar.component';
import { LoginComponent } from './login/login.component';
//import my service
import { ApiService } from './service/api.service';
@NgModule({
declarations: [
AppComponent,
NavbarComponent,
LoginComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule,
HeroesModule,
CoreModule,
ProfileModule
],
providers: [
ApiService
],
bootstrap: [AppComponent]
})
export class AppModule { }
现在启动json server,在命令行或终端中输入:
json-server data.json
观察网页,点击profile按钮,会自动跳入到登录页面,输入用户名:user1,密码:123456 后成功路由到了profile页面,现在大体功能已经实现,稍后我们会对页面进行美化
10. 有登录就有注销
现在需要分清两种存储方式,一种是基于本地的localStorage存储,也就是我们当前用的,还有一种是基于窗口的sessioStorage存储
- localStorage:除非你调用方法移除,否则它就会永远存在你的电脑里(等会我们就要实现这个方法)
- sessionStorage:基于窗口,当窗口关闭时,存储在它里面的数据全部删除 当然还有更加完善的存储方式,也可以自定义,这就要视情况而定了 (到底用哪一种,就看个人喜好和项目要求啦)
给app.component.html添加注销按钮
src/app/navbar/navbar.component.html(v2)
<nav>
<a routerLink="/hero" routerLinkActive="active" id="Hero">Hero</a>
<a routerLink="/profile" routerLinkActive="active" id="Profile">Profile</a>
<a routerLink="/login" routerLinkActive="active" id="Login">Login</a>
<button (click)="logout()" id="Logout">Logout</button>
</nav>
<div>
<router-outlet></router-outlet>
</div>
实现其功能
src/app/navbar/navbar.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-navbar',
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.css']
})
export class NavbarComponent implements OnInit {
userId: number;
constructor() { }
ngOnInit(){
const divLogin = document.getElementById('Login');
const divLogout = document.getElementById('Logout');
this.userId = -1;
if(localStorage.getItem('userId') !== null){
//已登录
divLogin.style.display = 'none';
this.userId = <number><any>localStorage.getItem('userId');
}
else{
//未登录
divLogout.style.display = 'none';
}
}
logout() {
localStorage.removeItem('userId');
this.userId = -1;
location.reload();
}
}
这里稍稍做了一些调整:已登录就不显示Login按钮而只显示Logout,未登录反之
注销还是蛮简单的,只需要把本地存储的验证去掉即可。
11. 注册
现在能登录是json文件中有一个现成的user,我们不能只靠手动操作数据库来增加用户,一定要实现用户自主注册的功能。
本例的注册方式十分简单:直接用UserService向json文件中写入User信息。但一般项目中不会被用到,因为安全性极差,用户名和密码都是明文存的,这种网站黑客都懒得攻击,现实项目中大家一定不能用这种方式,更为具体的方法在以后的章节会有所涉及。
首先新建注册Register组件,在命令行或终端中输入:
ng g c register
并在app-routing.module.ts导入它:
src/app/app-routing.module.ts(v3)
//import angular core
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
//import my mudules
import { HeroModule } from './hero/hero.module';
import { ProfileModule } from './profile/profile.module';
//import my components
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
//import my service
import { AuthGuardService } from './service/auth-guard.service';
const routes: Routes = [
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
{
path: 'profile',
canActivate: [AuthGuardService],
loadChildren: 'app/profile/profile.module#ProfileModule'
},
{
path: 'login',
component: LoginComponent
},
{
path: 'register',
component: RegisterComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
下面我们完善一下注册的组件:
src/app/register.component.html
<div>
<h2>Register</h2>
<div>
<label class="mark">username: </label>
<input [(ngModel)]="user.username" placeholder="username" />
</div>
<div>
<label class="mark">password: </label>
<input type="password" [(ngModel)]="user.password" placeholder="password" />
</div>
<button (click)="onSubmit()">Submit</button>
</div>
src/app/register.component.ts
import { Component, OnInit } from '@angular/core';
import { User } from '../domain/user';
import { UserService } from '../service/user.service';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css'],
providers: [UserService]
})
export class RegisterComponent implements OnInit {
user= new User();
id: number;
constructor(private userService: UserService) { }
ngOnInit() {
}
onSubmit(){
this.userService.createUser(this.user)
.then(user => {
console.log(user);
});
}
}
src/app/register.component.css
label {
display: inline-block;
width: 6em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
.mark {
display: inline-block;
width: 6em;
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;
}
然后我们在NavbarComponent.html中添加到注册的路由:
src/app/navbar/navbar.component.html(v3)
<nav>
<a routerLink="/hero" routerLinkActive="active" id="Hero">Hero</a>
<a routerLink="/profile" routerLinkActive="active" id="Profile">Profile</a>
<a routerLink="/login" routerLinkActive="active" id="Login">Login</a>
<a routerLink="/register" routerLinkActive="active" id="Register">Register</a>
<a (click)="logout()" id="Logout">Logout</a>
</nav>
<div>
<router-outlet></router-outlet>
</div>
最后来看一下navbar.component.ts的最终情况:在登录时取消显示注册,未登录时显示:
src/app/navbar/navbar.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-navbar',
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.css']
})
export class NavbarComponent implements OnInit {
userId: number;
constructor() { }
ngOnInit(){
const divLogin = document.getElementById('Login');
const divLogout = document.getElementById('Logout');
const divRegister = document.getElementById('Register');
this.userId = -1;
if(localStorage.getItem('userId') !== null){
//已登录
divLogin.style.display = 'none';
divRegister.style.display='none';
this.userId = <number><any>localStorage.getItem('userId');
}
else{
//未登录
divLogout.style.display = 'none';
}
}
logout() {
localStorage.removeItem('userId');
this.userId = -1;
location.reload();
}
}
12. 修改个人信息
下面我们要可以修改自己的个人信息,首先,增加个人信息组件 profile-detail 。
ng g c profile/profile-detail
在profile-routing.module.ts中填入这三个组件的路由信息
//import angular core
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
//import my components
import { ProfileComponent } from './profile.component';
import { ProfileDetailComponent } from './profile-detail/profile-detail.component';
const routes: Routes = [
{
path: '',
component: ProfileComponent,
children: [
{
path:'',
component: ProfileDetailComponent
},
{
path:'profile-detail',
component: ProfileDetailComponent
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProfileRoutingModule { }
在profile.component.html中加入通向profile-detail与my-hero-list的路由连接:
src/app/profile/profile.component.html
<nav>
<a routerLink="/profile-detail" routerLinkActive="active" id="ProfileDetail">ProfileDetail</a>
</nav>
<div>
<router-outlet></router-outlet>
</div>
并给它添上样式:
src/app/profile/profile.component.css
nav a {
padding: 5px 10px;
text-decoration: none;
margin-right: 10px;
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.active {
color: #039be5;
}
以下是给 profile-detail 组件写代码。
src/app/profile/profile-detail/profile-detail.component.html
<div *ngIf="user">
<h3> ID: {{user.id}} </h3>
<h3> Username: {{user.username}} </h3>
<h3> Name: {{user.name}} </h3>
<div class = "items">
<label>Name:</label>
<input [(ngModel)]="user.name" placeholder="Name" />
</div>
<button (click)="goBack()">Back</button>
<button (click)="save()">Save</button>
</div>
src/app/profile/profile-detail/profile-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;
}
src/app/profile/profile-detail/profile-detail.component.ts
//import angular core
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
//import third package
import 'rxjs/add/operator/switchMap';
//import service
import { UserService } from '../../service/user.service';
//import class
import { User } from '../../domain/user';
@Component({
selector: 'app-profile-detail',
templateUrl: './profile-detail.component.html',
styleUrls: ['./profile-detail.component.css'],
providers: [UserService]
})
export class ProfileDetailComponent implements OnInit {
user: User;
constructor(
private route: ActivatedRoute,
private router: Router,
private location: Location,
private userService: UserService) { }
ngOnInit() {
this.userService.getUserById(<number><any>localStorage.getItem('userId'))
.then(user => this.user = user);
}
save(): void{
this.userService.updateUser(this.user)
.then(() => this.goBack());
}
goBack(): void {
this.location.back();
}
}
再检查一下profile.module.ts中有没有添加FormsModule:
src/app/profile/profile.module.ts
//import angular core
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
//import my modules
import { ProfileRoutingModule } from './profile-routing.module';
//import my components
import { ProfileComponent } from './profile.component';
import { ProfileDetailComponent } from './profile-detail/profile-detail.component';
@NgModule({
imports: [
CommonModule,
ProfileRoutingModule,
FormsModule
],
declarations: [
ProfileComponent,
ProfileDetailComponent,
]
})
export class ProfileModule { }
13. 访问我的英雄
最后目标:每个召唤师,可以查看所有的英雄列表,可以新增英雄,新增英雄的主人就是这个召唤师。但是每个召唤师只能修改自己的英雄,包括查看自己的英雄详情,修改自己的英雄和删除自己的英雄。
首先分别在终端输入
ng g c heroes
ng g c heroes/hero-search
ng g c heroes/my-hero-list
ng g c heroes/my-hero-detail
来分别代表总的英雄组件、英雄查询组件、自己的英雄列表和自己的某个英雄的详细信息这四个组件。这四个组件会自动注册进 src/app/heroes/heroes.module.ts 当中。
在heroes-routing.module.ts中填入这四个组件的路由信息,并修改原来的路由为子路由形式。
//import angular core
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
//import my components
import { HeroesComponent } from './heroes.component';
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { MyHeroListComponent } from './my-hero-list/my-hero-list.component';
import { MyHeroDetailComponent} from './my-hero-detail/my-hero-detail.component';
const routes: Routes = [
{
path: '',
component: HeroesComponent,
children: [
{
path: '',
component: HeroListComponent
},
{ path: 'hero/:id',
component: HeroDetailComponent
},
{
path:'myheroes',
component: MyHeroListComponent
},
{
path:'myhero/:id',
component: MyHeroDetailComponent
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HeroesRoutingModule { }
在heroes.component.html中加入通向myheroes的路由连接:
src/app/heroes/heroes.component.html
<nav>
<a routerLink="/heroes" routerLinkActive="active" id="heroes">All Heroes</a>
<a routerLink="/myheroes" routerLinkActive="active" id="myheroes">My Heroes</a>
</nav>
<div>
<router-outlet></router-outlet>
</div>
并给它添上样式:
src/app/heroes/heroes.component.css
nav a {
padding: 5px 10px;
text-decoration: none;
margin-right: 10px;
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.active {
color: #039be5;
}
以下是分别为 my-hero-list 和 my-hero-detail 编写代码。我们需要把原来原来heroes模块下有关新建与修改的功能移到这里来,因为只有召唤师可以召唤修改自己的英雄,而别的召唤师则只能查看别人的英雄,因为相关知识在先前章节都已讲过,这里就不做更详细的说明了,直接列出两个组件所需要的代码:
下面是 my-hero-list
src/app/heroes/my-hero-list/my-hero-list.component.html
<h2>My Heroes</h2>
<div>
<label>Hero name:</label> <input #heroName />
<button (click)="addHero(heroName.value); heroName.value=''">
Add
</button>
</div>
<ul class="heroes">
<li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero">
<span class="badge">{{hero.id}}</span>
<span>{{hero.name}}</span>
<button class="delete" (click)="deleteHero(hero); $event.stopPropagation()">x</button>
</li>
</ul>
src/app/heroes/my-hero-list/my-hero-list.component.css
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
float:right;
margin-top: 2px;
margin-right: .8em;
background-color: gray !important;
color:white;
}
src/app/heroes/my-hero-list/my-hero-list.component.ts
//import angualr core
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
//import third package
import 'rxjs/add/operator/switchMap';
//import service
import { HeroService } from '../../service/hero.service';
//import class
import { Hero } from '../../domain/hero';
@Component({
selector: 'app-my-hero-list',
templateUrl: './my-hero-list.component.html',
styleUrls: ['./my-hero-list.component.css'],
providers: [HeroService]
})
export class MyHeroListComponent implements OnInit {
myId: number;
heroes: Hero[];
selectedHero: Hero;
constructor(
private heroService: HeroService,
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit() {
this.myId = <number><any>localStorage.getItem('userId');
this.heroService.getHeroesByOwnerId(this.myId)
.then(heroes => this.heroes = heroes);
}
addHero(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.createHeroByNameUserId(name,this.myId)
.then(hero => {
this.heroes.push(hero);
this.selectedHero = null;
});
}
deleteHero(hero: Hero): void {
this.heroService
.deleteHeroById(hero.id)
.then(() => {
this.heroes = this.heroes.filter(h => h !== hero);
if (this.selectedHero === hero) { this.selectedHero = null; }
});
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
this.router.navigate(['/myhero', this.selectedHero.id]);
}
}
下面是 my-hero-detail
src/app/heroes/my-hero-detail/my-hero-detail.component.html
<div *ngIf="hero">
<h2>{{hero.id}}. {{hero.name}}</h2>
<div class = "items">
<label>Name:</label>
<input [(ngModel)]="hero.name" placeholder="Name" />
</div>
<div class = "items">
<label>Gender:</label>
<select *ngIf="heroGenders" [(ngModel)]="hero.gender" name = "gender" placeholder="gender">
<option *ngFor="let heroGender of heroGenders ">{{heroGender}}</option>
</select>
</div>
<div class = "items">
<label>Desc:</label>
<input [(ngModel)]="hero.desc" placeholder="Description" />
</div>
<button (click)="goBack()">Back</button>
<button (click)="save()">Save</button>
</div>
src/app/heroes/my-hero-detail/my-hero-detail.component.css
label {
display: inline-block;
width: 4em;
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;
}
src/app/heroes/my-hero-detail/my-hero-detail.component.ts
//import angular core
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
//import third package
import 'rxjs/add/operator/switchMap';
//import service
import { HeroService } from '../../service/hero.service';
//import class
import { Hero } from '../../domain/hero';
@Component({
selector: 'app-my-hero-detail',
templateUrl: './my-hero-detail.component.html',
styleUrls: ['./my-hero-detail.component.css'],
providers: [HeroService]
})
export class MyHeroDetailComponent implements OnInit {
hero: Hero;
heroGenders: string[] = ['MALE', 'FEMALE', 'OTHER'];
constructor(
private route: ActivatedRoute,
private router: Router,
private heroService: HeroService,
private location: Location) { }
ngOnInit(): void {
this.route.params
.switchMap((params: Params) => this.heroService.getHeroById(+params['id']))
.subscribe(hero => this.hero = hero);
}
save(): void {
this.heroService.updateHero(this.hero)
.then(() => this.goBack());
}
goBack(): void {
this.location.back();
}
}
然后删除原hero模块下所有有关新建英雄修改英雄的组件,方法:
src/app/hero/hero-list/hero-list.component.html
<h2>All Heroes</h2>
<app-hero-search></app-hero-search>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="isSelected(hero)"
(click)="onSelect(hero)">
<span class="badge">{{ hero.id }}</span> {{ hero.name }}
</li>
</ul>
src/app/hero/hero-list/hero-list.component.ts
//import angular core
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
//import third package
import 'rxjs/add/operator/switchMap';
//import service
import { HeroService } from '../../service/hero.service';
//import class
import { Hero } from '../../domain/hero';
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
styleUrls: ['./hero-list.component.css'],
providers: [HeroService]
})
export class HeroListComponent implements OnInit {
heroes: Hero[];
selectedId : number;
constructor(
private heroService: HeroService,
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit() {
this.heroService.getHeroes()
.then(heroes => this.heroes = heroes);
}
isSelected(hero: Hero){
return hero.id === this.selectedId;
}
onSelect(hero: Hero){
this.router.navigate(['/hero',hero.id]);
}
}
src/app/hero/hero-detail/hero-detail.component.html
<div *ngIf="hero">
<h3>{{ hero.id }}.{{ hero.name }}</h3>
<div class="items">
<label>Id: </label>{{ hero.id }}
</div>
<div class="items">
<label>Name: </label>{{ hero.name }}
</div>
<div class="items">
<label>Gender: </label>{{ hero.gender }}
</div>
<div class="items">
<label>Desc: </label>{{ hero.desc }}
</div>
<div class="items" *ngIf="user">
<label>Owner: </label>{{ user.name }}
</div>
</div>
<div>
<button (click)="goBack()">Back</button>
</div>
src/app/hero/hero-detail/hero-detail.component.ts
//import angular core
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { Location } from '@angular/common';
//import third package
import 'rxjs/add/operator/switchMap';
//import service
import { HeroService } from '../../service/hero.service';
import { UserService } from '../../service/user.service';
//import class
import { Hero } from '../../domain/hero';
import { User } from '../../domain/user';
@Component({
selector: 'hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css'],
providers: [HeroService, UserService]
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
user: User;
constructor(
private route: ActivatedRoute,
private router: Router,
private heroService: HeroService,
private userService: UserService,
private location: Location) { }
ngOnInit(): void {
this.route.params
.switchMap((params: Params) => this.heroService.getHeroById(+params['id']))
.subscribe(hero => {
this.hero = hero;
this.userService.getUserById(hero.ownerId)
.then(user => this.user = user);
});
}
goBack(): void {
this.location.back();
}
}
下面是为新增的 hero-search 组件写的代码。
src/app/heroes/hero-search/hero-search.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
// Observable class extensions
import 'rxjs/add/observable/of';
// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { HeroService } from '../../service/hero.service';
import { Hero } from '../../domain/hero';
@Component({
selector: 'app-hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ],
providers: [HeroService]
})
export class HeroSearchComponent implements OnInit {
heroes: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(
private heroService: HeroService,
private router: Router) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes = this.searchTerms
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
? this.heroService.searchHero(term)
// or the observable of empty heroes if there was no search term
: Observable.of<Hero[]>([]))
.catch(error => {
// TODO: add real error handling
console.log(error);
return Observable.of<Hero[]>([]);
});
}
gotoDetail(hero: Hero): void {
let link = ['/hero', hero.id];
this.router.navigate(link);
}
}
src/app/heroes/hero-search/hero-search.component.html
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<div>
<div *ngFor="let hero of heroes | async"
(click)="gotoDetail(hero)" class="search-result" >
{{hero.name}}
</div>
</div>
</div>
src/app/heroes/hero-search/hero-search.component.css
.search-result{
border-bottom: 1px solid gray;
border-left: 1px solid gray;
border-right: 1px solid gray;
width:195px;
height: 16px;
padding: 5px;
background-color: white;
cursor: pointer;
}
.search-result:hover {
color: #eee;
background-color: #607D8B;
}
#search-box{
width: 200px;
height: 20px;
}
因为有每个英雄都有了一个召唤师id(ownerId)的属性,我们需要一个按召唤师id查询英雄的方法,在新建英雄的时候,作为必填项,不仅要传入英雄的名字,还要传入英雄的召唤师id: 现在的hero.service.ts变成了这样:
src/app/service/hero.service.ts
//import angular core
import { Injectable } from '@angular/core';
import { Http, Headers, Response } from '@angular/http';
//import third package
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/toPromise';
//import my services
import { ApiService } from './api.service';
//import class
import { Hero } from '../domain/hero';
@Injectable()
export class HeroService {
private api_url : string ;
private headers : Headers ;
constructor(private http: Http, private apiService: ApiService) {
this.api_url = apiService.getUrl() + '/heroes';
this.headers = apiService.getHeaders();
}
//查询所有Hero
getHeroes(): Promise<Hero[]> {
const url = `${this.api_url}`;
return this.http.get(url, { headers: this.headers })
.toPromise()
.then(res => res.json() as Hero[])
.catch(this.handleError);
}
//按id查询Hero
getHeroById(id: number): Promise<Hero> {
const url = `${this.api_url}/${id}`;
return this.http.get(url, { headers: this.headers })
.toPromise()
.then(res => res.json() as Hero)
.catch(this.handleError);
}
//按ownerId查询Hero
getHeroesByOwnerId(ownerId: number): Promise<Hero[]> {
const url = `${this.api_url}/?ownerId=${ownerId}`;
return this.http.get(url, { headers: this.headers })
.toPromise()
.then(res => res.json() as Hero[])
.catch(this.handleError);
}
//search hero
searchHero(term: string): Observable<Hero[]> {
return this.http
.get(`${this.api_url}?name_like=${term}`)
.map(response => response.json() as Hero[]);
}
//新建Hero
createHero(hero: Hero): Promise<Hero> {
const url = `${this.api_url}`;
return this.http
.post(url, JSON.stringify(hero), { headers: this.headers })
.toPromise()
.then(res => res.json() as Hero)
.catch(this.handleError);
}
//按name与userId新建Hero
createHeroByNameUserId(name: string, ownerId: number): Promise<Hero> {
let hero = {
name: name,
ownerId: ownerId
}
const url = `${this.api_url}`;
return this.http
.post(url, JSON.stringify(hero), { headers: this.headers })
.toPromise()
.then(res => res.json() as Hero)
.catch(this.handleError);
}
//修改Hero
updateHero(hero: Hero): Promise<Hero> {
const url = `${this.api_url}/${hero.id}`;
return this.http
.put(url, JSON.stringify(hero), { headers: this.headers })
.toPromise()
.then(res => res.json() as Hero)
.catch(this.handleError);
}
//删除某个Hero
deleteHero(hero: Hero): Promise<void> {
const url = `${this.api_url}/${hero.id}`;
return this.http.delete(url, { headers: this.headers })
.toPromise()
.then(() => null)
.catch(this.handleError);
}
//按id删除某个Hero
deleteHeroById(id: number): Promise<void> {
const url = `${this.api_url}/${id}`;
return this.http.delete(url, { headers: this.headers })
.toPromise()
.then(() => null)
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error);
return Promise.reject(error.message || error);
}
}
至此,我们的召唤师终于可以为所欲为的召唤他们的英雄们了。