技术栈
- 框架: Angular
- UI: NG-ZORRO
- server: json-server + JWT权限认证
项目搭建步骤
- 创建项目
ng new demo-hmr
cd demo-hmr
- 使用antd
ng add ng-zorro-antd
ng serve --open
配置路由
单页面应用程序,要先去配置路由。先理清楚,组件之间的关系。
-
创建要使用的组件
$ ng g c login $ ng g c home
-
在
app.module.ts
引入路由,设置路由规则,配置路由模块。在
html
中指定路由出口指定导航链接。 -
为了方便后期的管理。创建一个路由的模块。
$ ng g m app-routing
-
基本就是将
app.module.ts
中的引入路由,设置路由规则,配置路由模块复制至app-routing.module.ts
中。 -
报错
报错以下信息,就是在
app-routing.module.ts
中没有进行导出理由模块。compiler.js:1021 Uncaught Error: Template parse errors: 'router-outlet' is not a known element: 1. If 'router-outlet' is an Angular component, then verify that it is part of this module. 2. If 'router-outlet' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. (" <a routerLink = '/login'>login</a>
-
代码如下
app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { NgZorroAntdModule, NZ_I18N, zh_CN } from 'ng-zorro-antd'; import { registerLocaleData } from '@angular/common'; import zh from '@angular/common/locales/zh'; import { LoginComponent } from './login/login.component'; import { HomeComponent } from './home/home.component' import { AppRoutingModule } from './app-routing/app-routing.module' registerLocaleData(zh); @NgModule({ declarations: [ AppComponent, LoginComponent, HomeComponent ], imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, HttpClientModule, NgZorroAntdModule, AppRoutingModule ], providers: [{ provide: NZ_I18N, useValue: zh_CN }], bootstrap: [AppComponent] }) export class AppModule { }
app-routing/app-routing.module.ts
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; import { LoginComponent } from '../login/login.component'; import { HomeComponent } from '../home/home.component'; const appRoutes: Routes = [ { path: 'home', component: HomeComponent }, { path: 'login', component: LoginComponent } ] @NgModule({ imports: [ CommonModule, RouterModule.forRoot(appRoutes) ], exports: [ RouterModule ], declarations: [] }) export class AppRoutingModule { }
登陆功能实现
① 表单结构
报错
Uncaught Error: Template parse errors: Can't bind to 'formGroup' since it isn't a known property of 'form'. ....
就是没有导入
ReactiveFormsModule
,需要在app.module.ts
中进行导入导出。import { ReactiveFormsModule } from '@angular/forms'; ... @NgModule({ declarations....., imports: [ ReactiveFormsModule ] })
代码:
login/login.component.html
<div nz-row class="losin-wrapper" nzType="flex" nzJustify="space-around" nzAlign="middle">
<form nz-form [formGroup]="validateForm" class="login-form" (ngSubmit)="submitForm()">
<nz-form-item>
<nz-form-control>
<nz-input-group [nzPrefix]="prefixUser">
<input type="text" nz-input formControlName="userName" placeholder="账号">
</nz-input-group>
<nz-form-explain *ngIf="validateForm.get('userName').dirty && validateForm.get('userName').errors">Please input your username!</nz-form-explain>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<nz-input-group [nzPrefix]="prefixLock">
<input type="password" nz-input formControlName="password" placeholder="密码">
</nz-input-group>
<nz-form-explain *ngIf="validateForm.get('password').dirty && validateForm.get('password').errors">Please input your Password!</nz-form-explain>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<label nz-checkbox formControlName="remember">
<span>记住我</span>
</label>
<a class="login-form-forgot" class="login-form-forgot">忘记密码</a>
<button nz-button class="login-form-button" [nzType]="'primary'">登陆</button>
或者
<a href="">注册</a>
</nz-form-control>
</nz-form-item>
</form>
<ng-template #prefixUser><i nz-icon type="user"></i></ng-template>
<ng-template #prefixLock><i nz-icon type="lock"></i></ng-template>
</div>
login/login.component.ts
import { Component, OnInit } from '@angular/core';
import {
AbstractControl,
FormBuilder,
FormGroup,
Validators
} from '@angular/forms';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
validateForm: FormGroup;
submitForm(): void {
for (const i in this.validateForm.controls) {
this.validateForm.controls[ i ].markAsDirty();
this.validateForm.controls[ i ].updateValueAndValidity();
}
}
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.validateForm = this.fb.group({
userName: [ null, [ Validators.required ] ],
password: [ null, [ Validators.required ] ],
remember: [ true ]
});
}
}
######② 登陆样式
login/login.component.css
.losin-wrapper {
width: 100%;
height: 100%;
background-image: url(https://ws3.sinaimg.cn/large/006tNc79ly1g02hyg0a1kj31lm0u075z.jpg);
background-size: cover;
}
.login-form {
max-width: 300px;
}
.login-form-forgot {
float: right;
}
.login-form-button {
width: 100%;
}
③ 表单验证
-
在ts与html中,将
validateForm
更换为loginForm
-
不需要
记住我
的功能ngOnInit(): void { this.loginForm = this.fb.group({ userName: [ null, [ Validators.required ] ], password: [ null, [ Validators.required ] ], // delete remember: [ true ] }); }
<!-- delete formControlName="remember" --> <label nz-checkbox formControlName="remember"> <span>记住我</span> </label>
-
进行账户验证
- 用户不输入内容显示’请输入用户名’
- 用户长度为3-6
进行密码验证
- 用户不输入内容显示’请输入账户’
- 密码长度为6-8,只能由数字/字母组成
代码
login/login.component.ts
ngOnInit(): void { this.loginForm = this.fb.group({ userName: [ null, [ Validators.required, Validators.minLength(3), Validators.maxLength(6) ] ], password: [ null, [ Validators.required, Validators.pattern(/^[a-zA-Z0-9]{6,8}$/) ] ] }); }
login/login.component.html
<nz-form-item> <nz-form-control> <nz-input-group [nzPrefix]="prefixUser"> <input type="text" nz-input formControlName="userName" placeholder="账号"> </nz-input-group> <nz-form-explain *ngIf="loginForm.get('userName').dirty && loginForm.get('userName').hasError('required')">请输入账户</nz-form-explain> <nz-form-explain *ngIf="loginForm.get('userName').dirty && loginForm.get('userName').hasError('minlength') || loginForm.get('userName').hasError('maxlength')">长度为3-6位</nz-form-explain> </nz-form-control> </nz-form-item> <nz-form-item> <nz-form-control> <nz-input-group nzPrefixIcon="anticon anticon-lock"> <input type="password" nz-input formControlName="password" placeholder="密码"> </nz-input-group> <nz-form-explain *ngIf="loginForm.get('password').dirty && loginForm.get('password').hasError('required')">请输入密码</nz-form-explain> <nz-form-explain *ngIf="loginForm.get('password').dirty && loginForm.get('password').hasError('pattern')">密码只能由数字、字母组成,长度为6-8位</nz-form-explain> </nz-form-control> </nz-form-item>
-
判断是否验证成功
submitForm(): void { for (const i in this.loginForm.controls) { this.loginForm.controls[ i ].markAsDirty(); this.loginForm.controls[ i ].updateValueAndValidity(); } // 添加验证 // if(this.loginForm.valid) { // console.log('验证成功') // }else { // console.log('验证失败') // } if(!this.loginForm.valid) { console.log('验证失败') return } console.log('验证成功', this.loginForm.value) }
当什么都不输入的时候,就会验证失败。
输入一个正确的,就会验证成功。
-
因有重复的变量,简化
submitForm(): void { // 设置变量,简化代码 const loginForm = this.loginForm const { controls } = loginForm for (const i in controls) { controls[ i ].markAsDirty(); controls[ i ].updateValueAndValidity(); } if(!loginForm.valid) { console.log('验证失败') return } console.log('验证成功', loginForm.value) }
④ 登陆功能
- 创建服务文件
$ ng g s login/login
- 导入HttpClient
- 根据文档写登陆的login,记得写interface
import { Injectable } from '@angular/core';
// 导入HttpClient
import { HttpClient } from '@angular/common/http';
// interface
interface loginForm {
username: string,
password: string
}
@Injectable({
providedIn: 'root'
})
export class LoginService {
constructor(private http: HttpClient) { }
// 登陆的login
login(loginForm: loginForm) {
return this.http.post('http://localhost:2080/tokens', loginForm)
}
}
- 在
login/login.component.ts
中导入登陆服务
import { LoginService } from './login.service';
.....
export class LoginComponent implements OnInit {
constructor(private fb: FormBuilder, private LoginService: LoginService) {
}
loginForm: FormGroup;
submitForm(): void {
...
if(!loginForm.valid) {
console.log('验证失败')
return
}
console.log('验证成功', loginForm.value)
const { userName, password } = loginForm.value
const loginParams = {
username: userName,
password
}
this.LoginService.login(loginParams)
.subscribe(res=> {
this.router.navigate(['/home'])
})
}
}
- 测试,输入账号zqran密码123456
- 报错下图是username格式读取错误
⑤ 抽离类型和配置
类型
-
新建文件
login.type.ts
export interface LoginForm { username: string password: string }
-
在
login.service.ts
中替换interface LoginForm -
在
login.component.ts
中的loginParams添加类型检查
配置
-
在app目录创建config.js
export const URL = 'http://localhost:2080'
-
在
login.service.ts
导入配置,进行相应的更改login(loginForm: LoginForm) { return this.http.post(`${URL}/tokens`, loginForm) }
登陆访问控制
① 路由守卫
-
应用情景
- CanActivate ==> 导航到某路由的情况
- CanActivateChild ==> 导航到某子路由的情况
- CanDeactivate ==> 从当前路由离开的情况
- Resolve ==> 在路由激活之前获取路由数据
- CanLoad ==> 异步导航到某特性模块的情况
-
路由守卫实现访问控制思路
-
登录成功后,将 token 存储在 localStorage 中
-
在路由守卫CanActivate中判断是否有token
-
如果有直接放行。如果没有,跳转到 login 页
注意:不要校验 login 页
-
-
路由守卫使用步骤
- 创建AuthGuard
ng g guard auth
- 在路由模块中添加导入
auth guard
- 给
home
路由添加canActivate
守卫
- 创建AuthGuard
② 路由导航守卫
-
存储token,记得添加一个关于token的类型检查
login/login.component.ts
..... // token 类型检查 interface Token { token: string } .... export class LoginComponent implements OnInit { ...... this.LoginService.login(loginParams) // token 类型检查 .subscribe((res: Token)=> { // console.log(res) // 存储token localStorage.setItem('login-token', res.token) this.router.navigate(['/home']) }) ........ } }
-
创建AuthGuard,删除掉不需要的功能
$ ng g guard auth
auth.guard.ts
import { Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { canActivate(): boolean { console.log('路由守卫') return true; } }
-
添加理由守卫
app-routing/app-routing.module.ts
const appRoutes: Routes = [ { path: 'home', component: HomeComponent, canActivate: [AuthGuard],}, { path: 'login', component: LoginComponent } ]
-
测试。
将
auth.guard.ts
的AuthGuard中的return true
更改为return false
,访问http://localhost:4200/home
会自动跳转到http://localhost:4200/
. -
获取token
-
跳转到登录页 login页面
代码
auth.guard.ts
import { Injectable } from '@angular/core'; import { CanActivate } from '@angular/router'; import { Router } from '@angular/router' @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor(private router: Router) {} canActivate(): boolean { console.log('路由守卫') // 获取token const token = localStorage.getItem('login-token') if (!!token) { return true } // 跳转到登录页 login页面 this.router.navigate(['/login']) return false } }
首页
① 结构layout
CSS中
:host
表示的是宿主元素
② 左侧内容Menu
③ 右侧内容Avatar,Popconfirm
退出功能
① 基本实现
-
基本结构
<a href="#" class="logout" (click)="logout($event)"> <i class="anticon anticon-logout"></i> 退出 </a>
-
创建服务
$ ng g s home/home
-
在服务中导入HttpClent
-
A标签被当做哈希值,进行了路由跳转
服务
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http' import { URL } from '../config.js' @Injectable({ providedIn: 'root' }) export class HomeService { constructor(private http: HttpClient) { } logout() { // return this.http.delete(`${URL}/tokens`) return this.http.delete(`http://localhost:2080/tokens`) } }
组件
import { Component, OnInit } from '@angular/core'; import { HomeService } from './home.service'; ..... export class HomeComponent implements OnInit { constructor(private homeService: HomeService) {} isCollapsed = false; logout() { this.homeService.logout().subscribe( res=> console.log('res', res) , (err) => {console.log('err', err )}) } ngOnInit() { } }
-
增加
e.preventDefault()
来组织浏览器的默认行为 -
根据文档增加请求头信息
... export class HomeService { .... logout() { const token = localStorage.getItem('login-token') return this.http.delete(`http://localhost:2080/tokens`, { headers: { Authorization: `Bearer ${token}` } }) } }
-
退出之后需要调转到登录页,并且清除本地token
..... import { Router } from '@angular/router' ... export class HomeComponent implements OnInit { constructor(private nzMessageService: NzMessageService, private homeService: HomeService, private router: Router ) {} isCollapsed = false; logout(e) { e.preventDefault() this.homeService.logout().subscribe( res => { console.log('退出成功了', res) // 先清除本地的token localStorage.removeItem('itcast-token') // 再跳转到登录页 this.router.navigate(['/login']) }, err => { console.log('退出失败,出错了:', err) } ) } ngOnInit() { } }
-
给一个全局的提示Message
.... import { NzMessageService } from 'ng-zorro-antd' ..... export class HomeComponent implements OnInit { constructor(private nzMessageService: NzMessageService, private homeService: HomeService, private router: Router ) {} ..... logout(e) { e.preventDefault() this.homeService.logout().subscribe( res => { console.log('退出成功了', res) // 先清除本地的token localStorage.removeItem('itcast-token') // 再跳转到登录页 this.router.navigate(['/login']) }, err => { console.log('退出失败,出错了:', err) this.nzMessageService.create('warning',`稍后再试`) } ) } createMessage(type: string): void { this.message.create(type, `This is a message of ${type}`); } ngOnInit() { } }
-
② 确认退出增加用户体验
-
错误提示Message
-
防止误操作气泡确认框
home.component.ts
.... import { NzMessageService } from 'ng-zorro-antd' ..... export class HomeComponent implements OnInit { constructor(private nzMessageService: NzMessageService, private homeService: HomeService, private router: Router ) {} ..... logout(e) { // 气泡确认框会报错 // e.preventDefault() this.homeService.logout().subscribe( res => { localStorage.removeItem('itcast-token') this.router.navigate(['/login']) }, err => { console.log('退出失败,出错了:', err) // 错误提示 this.nzMessageService.create('warning',`稍后再试`) } ) } // 气泡确认框 cancel(): void { this.nzMessageService.info('已取消'); } ngOnInit() { } }
home.component.html
<div nz-col nzSpan="4"> <nz-avatar nzIcon="user" nzSrc="//zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"></nz-avatar> <a nz-popconfirm nzTitle="是否确认退出?" (nzOnConfirm)="logout()" (nzOnCancel)="cancel()"> 退出</a> </div>
③ 退出功能样式调整
> 适当修改一下细节
异步路由
为什么不进入应用就加载所有功能模块?
- 增加了初始加载包的体积
- 降低了首屏加载速度
- 长时间白屏,用户体验差
- 对服务器造成额外的压力
特点
用户请求某个模块的时候,才加载这个模块
优势
减小了初始加载包体积,提高了首屏加载速度
使用
-
创建带有路由的模块
$ ng g m employees --routing
-
创建员工列表组件:
$ ng g c employees/employee-list
-
创建员工添加组件
$ ng g c employees/employee-add
-
在 employees-routing 中配置路由规则
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { EmployeeListComponent } from './employee-list/employee-list.component'; import { EmployeeAddComponent } from './employee-add/employee-add.component'; const routes: Routes = [ { path: 'employee-list', component: EmployeeListComponent }, { path: 'employee-add', component: EmployeeAddComponent }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class EmployeesRoutingModule { }
-
在 app-routing 通过 loadChildren 异步加载employees模块
异步加载的路由,实际上加载的是模块
{ path: 'employee', // 要将异步加载模块的路径以及模块名称配置在此处 // 语法: 异步加载模块的路径#模块名称 loadChildren: './employees/employees.module#EmployeesModule' }
const appRoutes: Routes = [ { path: 'home', component: HomeComponent, canActivate: [AuthGuard], children: [ { path: 'employee', loadChildren: './employees/employees.module#EmployeesModule' } ]}, { path: 'login', component: LoginComponent } ]
注意:不要在根模块中加载 employees 模块
员工管理-列表
###### ① 结构
- UI样式使用NG-ZORRO的Table组件
报错以下内容就需要在
employees.module.ts
中导入一下NgZorroAntdModule
ERROR Error: Uncaught (in promise): Error: Template parse errors: Can't bind to 'nzData' since it isn't a known property of 'nz-table'. 1. If 'nz-table' is an Angular component and it has 'nzData' input, then verify that it is part of this module. 2. If 'nz-table' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. 3. To allow any property add 'NO_ERRORS_SCHEMA' to the '@NgModule.schemas' of this component. ("<nz-table #basicTable [ERROR ->][nzData]="dataSet"> ...
- 模板引用变量用来获取DOM对象或者组件
<!-- #basicTable 和 #txt 是angular中的模板引用变量,用来获取DOM对象或者组件 -->
<input type="text" value="我是文本框的值" #txt>
- 表格会进行数据的处理,最好使用
basicTable.data
....
<tr *ngFor="let data of basicTable.data">
<td></td>
<td></td>
<td></td>
<td>
<a>Action 一 </a>
<nz-divider nzType="vertical"></nz-divider>
<a>Delete</a>
</td>
</tr>
.....
② 数据获取
- 创建服务
$ ng g s employees/employees
- 获取数据 ```TS import { Injectable } from ‘@angular/core’; import { HttpClient } from ‘@angular/common/http’ import { URL } from ‘../config’
@Injectable({ providedIn: ‘root’ }) export class EmployeesService {
constructor(private http: HttpClient) { }
// 获取数据
fetchData() {
return this.http.get({URL}/employees
)
}
}
3. 组件获取数据
```ts
....
import { EmployeesService } from '../employees.service'
....
export class EmployeeListComponent implements OnInit {
constructor(private employeesService: EmployeesService) { }
// 数据
dataSet
ngOnInit() {
this.employeesService.fetchData().subscribe(res => { console.log(res) })
}
}
会报错
显示没有权限。需要在服务里面添加一个请求头,就可以获取到数据
....
export class EmployeesService {
constructor(private http: HttpClient) { }
// 获取数据
fetchData() {
const token = localStorage.getItem('login-token')
return this.http.get(`{URL}/employees`,{
headers: {
Authorization: `Bearer ${token}`
}
})
}
}
- 在组件中使
this.employeesList = res
就可以获取到数据
③ TS类型约束
- 新建
employee.type.ts
文件
export interface Employee {
id: number
name: string
gender: string
phoneNumber: string
joinDate: number
}
- 在组件
employee-list.component.ts
中引用
....
import { Employee } from '../employee.type'
export class EmployeeListComponent implements OnInit {
....
// 数据
employeesList: Employee[] = []
ngOnInit() {
this.employeesService.fetchData()
.subscribe((res: Employee[]) => { this.employeesList = res })
}
}
- 封装获取数据的方法
.....
export class EmployeeListComponent implements OnInit {
constructor(private employeesService: EmployeesService) { }
// 数据
employeesList: Employee[] = []
// 封装获取数据的方法
fetchData() {
this.employeesService.fetchData()
.subscribe((res: Employee[]) => { this.employeesList = res })
}
ngOnInit() { this.fetchData() }
}
④ angular管道/表格数据处理
-
索引
-
性别
-
时间
是angular中的管道,用来进行数据格式化
<nz-table #basicTable [nzData]="employeesList">
<thead>...</thead>
<tbody>
<tr *ngFor="let data of basicTable.data; let i = index">
<!--索引-->
<td></td>
<td></td>
<!--性别-->
<td></td>
<td></td>
<td></td>
<!--时间-->
<td></td>
<td>...</td>
</tr>
</tbody>
</nz-table>
⑤ 分页获取
- 在服务中添加参数
....
export class EmployeesService {
constructor(private http: HttpClient) { }
// 获取数据
fetchData(curPage :number, pageSize) {
const token = localStorage.getItem('login-token')
const employeeUrl = `${URL}/employees?_page=${curPage}&_limit=${pageSize}`
return this.http.get<Employee[]>(employeeUrl,{
headers: {
Authorization: `Bearer ${token}`
}
})
}
}
- 在组件中传入参数
....
export class EmployeeListComponent implements OnInit {
constructor(private employeesService: EmployeesService) { }
...
// 分页的数据
curPage = 2
pageSize = 3
// 封装获取数据的方法
fetchData() {
// 传入参数
this.employeesService.fetchData(this.curPage, this.pageSize)
.subscribe((res: HttpResponse<Employee[]>) => {
console.log(res)
this.employeesList = res.body
})
}
....
}
页面可以进行展示
- 在服务中获取其响应体
....
export class EmployeesService {
constructor(private http: HttpClient) { }
// 获取数据
fetchData(curPage :number, pageSize) {
const token = localStorage.getItem('login-token')
const employeeUrl = `${URL}/employees?_page=${curPage}&_limit=${pageSize}`
return this.http.get<Employee[]>(employeeUrl,{
headers: {
// 获取完整的响应体
observe: 'response',
Authorization: `Bearer ${token}`
}
})
}
}
- 获取总条数
在控制台Network
中http://localhost:2080/employees?_page=2&_limit=3
里面获取到响应头
....
export class EmployeeListComponent implements OnInit {
....
fetchData() {
this.employeesService.fetchData(this.curPage, this.pageSize)
.subscribe((res: HttpResponse<Employee[]>) => {
console.log(res.headers.get('X-Total-Count'))
this.employeesList = res.body
})
}
....
}
在控制台可以得到一个是字符串5
在组件设置一个total
- 根据NG-ZORRO的表格控制API更改html格式,页面就可以进行分页的展示。
<nz-table #basicTable [nzData]="employeesList" [nzFrontPagination]="false" [nzTotal]="total" [(nzPageIndex)]="curPage" [nzPageSize]="pageSize" (nzPageIndexChange)="fetchData()">
<thead>...</thead>
<tbody>...</tbody>
</nz-table>
⑥ TS类型约束
员工管理-删除
-
UI样式:气泡确认框
employee-list.component.html
.... <a nz-popconfirm nzTitle="确定要删除吗?" nzPlacement="bottom" (nzOnConfirm)="handleDelete(data.id)" (nzOnCancel)="handleDelcancel()">删除</a> ...
employee-list.component.ts
import { NzMessageService } from 'ng-zorro-antd'; // 删除 ... handleDelete(): void { this.nzMessageService.info('已删除'); } handleDelcancel(): void { this.nzMessageService.info('取消删除'); } ...
-
给
handleDelete
传一个删除的id,控制台可显示出id,可与Network
接口中的id相对应employee-list.component.ts
// 删除 handleDelete(id: number): void { this.nzMessageService.info('已删除'); this.employeesService.delEmployee(id) .subscribe(res =>{ // 方法一 过滤一下 this.employeesList = this.employeesList.filter( employee => employee.id !== id ) // 方法二 重新加载 // this.fetchData() }) } handleDelcancel(): void { this.nzMessageService.info('取消删除'); }
employees.service.ts
// 删除数据 delEmployee(id: number) { const token = localStorage.getItem('login-token') return this.http.delete(`${URL}/employees/${id}`, { headers: { Authorization: `Bearer ${token}` } }) }
HTTP拦截器
① 了解
-
问题:每个请求都要在请求头中添加 Authorization,太繁琐
-
解决方式:使用 HttpClient 拦截器
-
作用:用它们监视和转换从应用发送到服务器的 HTTP 请求
② 使用
-
手动创建拦截器服务 app/auth-interceptors/auth.interceptor.ts
-
继承接口 HttpInterceptor 并实现 intercept() 方法
- 参数一:**req: HttpRequest
** 请求头对象 - 参数二:next: HttpHandler 下一个拦截器,如果只有一个,则会把请求发给服务器,并接收服务器的响应
- req的属性是只读(readonly)
修改请求方式
- 先克隆
- 修改这个克隆体
- 然后把克隆体传给 next.handle()
- 参数一:**req: HttpRequest
app/auth-interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core'
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'
import { Observable } from 'rxjs'
@Injectable()
export class AuthInterceptors implements HttpInterceptor{
// 拦截使用 HttpClient 方法的请求
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('http 拦截器')
return next.handle(req)
}
}
app/app.module.ts
.....
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptors } from './auth-interceptors/auth.interceptor'
....
@NgModule({
// providers: [{ provide: NZ_I18N, useValue: zh_CN }],
providers: [
{ provide: NZ_I18N, useValue: zh_CN },
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptors,
multi: true
}
]..
})
添加一下token ,headers
export class AuthInterceptors implements HttpInterceptor{
// 拦截使用 HttpClient 方法的请求
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem('login-token')
// 统一添加请求头
const anthReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
})
console.log('http 拦截器')
return next.handle(anthReq)
}
}
就可以把之前在home,login服务中添加的token ,headers删除掉了,在测试页面看看是否可以
让登陆页面不走这个请求头
在login.service.ts
中添加一个自定义的请求头
export class LoginService {
constructor(private http: HttpClient) { }
login(loginForm: LoginForm) {
return this.http.post(`${URL}/tokens`, loginForm, {
headers: {
'No-Auth': 'TRUE'
}
})
}
}
在auth.interceptor.ts
中添加判断条件
.....
export class AuthInterceptors implements HttpInterceptor{
// 拦截使用 HttpClient 方法的请求
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 如果是登录,不需要添加 Authorization
if(req.headers.get('No-Auth')==='TRUE') { return next.handle(req)}
// 非登录请求,都要添加 Authorization
const token = localStorage.getItem('login-token')
// 统一添加请求头
const anthReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${token}`)
})
console.log('http 拦截器')
return next.handle(anthReq)
}
}
③ 处理错误信息
// 管道
import { tap } from 'rxjs/operators'
...
export class AuthInterceptors implements HttpInterceptor{
constructor(private router: Router) { }
// 拦截使用 HttpClient 方法的请求
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
..........
return next.handle(anthReq).pipe(
tap(
// 成功的回调
ok => {},
// 失败的回调
error => {
console.log('捕捉到错误', error)
// 遇到401报错,移出token,并且跳转到登陆页面
if (error.status === 401) {
localStorage.removeItem('login-token')
this.router.navigate(['/login'])
}
}
)
)
}
}
员工管理-添加
① 结构
② 添加数据及表单验证
-
会进行报错,要先记得在
employees.module.ts
中导入ReactiveFormsModule模块之间不能共享指令等
ERROR Error: Uncaught (in promise): Error: Template parse errors:
No provider for ControlContainer ("[ERROR ->]<form nz-form (ngSubmit)="submitForm($event,validateForm.value)">
<!-- usernam -->
<nz-form-i"): ng:///EmployeesModule/EmployeeAddComponent.html@0:0
Error: Template parse errors:
No provider for ControlContainer ("[ERROR ->]<form nz-form (ngSubmit)="submitForm($event,validateForm.value)">
<!-- usernam -->
.....
import { ReactiveFormsModule } from '@angular/forms'
...
@NgModule({
imports: [
CommonModule,
EmployeesRoutingModule,
NgZorroAntdModule,
ReactiveFormsModule
],
declarations: [EmployeeListComponent, EmployeeAddComponent]
})
export class EmployeesModule { }
- 更改数据,在
employee-add.component.ts
中设置初始数据,在employee-add.component.html
进行相应的改变
....
export class EmployeeAddComponent implements OnInit {
constructor(private fb: FormBuilder) {
this.employeeAddForm = this.fb.group({
name: [''],
gender: ['0'],
email: [''],
phoneNumber: [''],
joinDate: ['']
});
}
.....
}
- 添加表单校验
- name: 必填,长度不能少于2
- gender: 必填,互斥(在html已体现)
- email: 必填,email格式
- phoneNumber: 匹配手机号码格式
- joinDate: 日期格式,入职日期不能早于今天
....
// 手机号码的正则
const PHONE_NUMBER_REGEXP = /^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\d{8}$/
....
export class EmployeeAddComponent implements OnInit {
constructor(private fb: FormBuilder) {
this.employeeAddForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
gender: ['0'],
email: ['',[Validators.required, Validators.email]],
phoneNumber: ['', [PHONE_NUMBER_REGEXP]],
joinDate: ['', [this.joinDateValidate]]
});
}
// 日期的自定义校验规则
joinDateValidate(control: FormControl) {
const selectDate = +control.value
const curDate = +new Date()
if (selectDate > curDate) {
return { date: true }
}
return null
}
.....
}
进行整理,让代码更加合理化
....
// 手机号码的正则
const PHONE_NUMBER_REGEXP = /^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\d{8}$/
....
export class EmployeeAddComponent implements OnInit {
constructor(private fb: FormBuilder) { }
employeeAddForm: FormGroup;
ngOnInit() {
this.employeeAddForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
gender: ['0'],
email: ['',[Validators.required, Validators.email]],
phoneNumber: ['', [Validators.pattern(PHONE_NUMBER_REGEXP)]],
joinDate: ['', [this.joinDateValidate]]
});
}
// 日期的自定义校验规则
joinDateValidate(control: FormControl) {
const selectDate = +control.value
// const selectDate = control.value
// console.log(selectDate)
const curDate = +new Date()
if (selectDate > curDate) { return { date: true } }
return null
}
.....
}
③ 按钮功能
- 按钮–添加/重置
employee-add.component.html
<nz-form-item>
<nz-form-control [nzOffset]="7" [nzSpan]="12">
<button nz-button nzType="primary" [disabled]="!employeeAddForm.valid">添加</button>
<button nz-button (click)="resetForm($event)">重置</button>
</nz-form-control>
</nz-form-item>
employee-add.component.ts
....
export class EmployeeAddComponent implements OnInit {
.....
submitForm = ($event, value) => {
$event.preventDefault();
for (const key in this.employeeAddForm.controls) {
this.employeeAddForm.controls[ key ].markAsDirty();
this.employeeAddForm.controls[ key ].updateValueAndValidity();
}
console.log(value);
};
resetForm(e: MouseEvent): void {
e.preventDefault()
const employeeAddForm = this.employeeAddForm
const { controls } = employeeAddForm
// 添加一个 gender的默认值 将之前的for删除
this.employeeAddForm.reset({
gender: '0'
});
for (const key in controls) {
controls[ key ].markAsPristine();
controls[ key ].updateValueAndValidity();
}
if(!employeeAddForm.valid){ return }
}
}
- 添加功能
① 在employee-add.component.ts
中,发起请求,添加员工
....
export class EmployeeAddComponent implements OnInit {
.....
submitForm = ($event, value) => {
$event.preventDefault();
for (const key in this.employeeAddForm.controls) {
this.employeeAddForm.controls[ key ].markAsDirty();
this.employeeAddForm.controls[ key ].updateValueAndValidity();
}
if(!employeeAddForm.valid){ return }
// console.log(value);
// 发起请求,添加员工
let { joinDate } = employeeAddForm.value
if(!joinDate) { joinDate = +new Date() }
const params = {...employeeAddForm.value, joinDate: +joinDate}
console.log(params)
};
}
② 在employees.service.ts
服务中,
.....
// 添加数据
addEmployee(employee: Employee) {
return this.http.post(`${URL}/employees`, employee)
}
....
在employee-add.component.ts
中添加服务。
....
export class EmployeeAddComponent implements OnInit {
.....
submitForm = ($event, value) => {
.....
// 发起请求,添加员工
let { joinDate } = employeeAddForm.value
if(!joinDate) { joinDate = +new Date() }
const params = {...employeeAddForm.value, joinDate: +joinDate}
// console.log(params)
this.employeesService.addEmployee(params)
.subscribe(res => {
console.log(res)
// 提示消息
this.nzMessageService.create('success', '添加员工成功')
// 重新加载表格
this.resetEmployeeForm()
// 跳转到list页面
this.router.navigate(['/home/employee'])
})
};
// 更改为重新加载EmployeeForm
resetEmployeeForm(): void {
const employeeAddForm = this.employeeAddForm
const { controls } = employeeAddForm
this.employeeAddForm.reset({
gender: '0'
});
for (const key in controls) {
controls[ key ].markAsPristine();
controls[ key ].updateValueAndValidity();
}
}
// 添加参数, 阻止默认行为
resetForm(e: MouseEvent): void {
e.preventDefault()
this.resetEmployeeForm()
}
}
员工管理-编辑
① 结构
- UI样式使用NG-ZORRO的对话框组件
employee-list.component.html
<nz-table >
<thead>.....</thead>
<tbody>
<tr>
....
<td>
<a (click)="showEditEmployeeModal()"><span>修改</span></a>
....
</td>
</tr>
</tbody>
</nz-table>
<!-- 修改的样式 -->
<nz-modal [(nzVisible)]="isShowEmployeeModal" nzTitle="编辑员工" (nzOnCancel)="handleEditEmployeeCancel()" (nzOnOk)="editEmployee()">
<p>第一行</p>
</nz-modal>
employee-list.component.ts
...
// 修改
isVisible = false;
showEditEmployeeModal(): void {
this.isVisible = true;
}
// 确定修改
handleEditEmployeeOK(): void {
console.log('确定修改');
this.isVisible = false;
}
// 取消修改
handleEditEmployeeCancel(): void {
console.log('取消修改');
this.isVisible = false;
}
- 将添加的表单复制过来
employee-list.component.html
<nz-table >
<thead>.....</thead>
<tbody>
<tr>
....
<td>
<a (click)="showEditEmployeeModal()"><span>修改</span></a>
....
</td>
</tr>
</tbody>
</nz-table>
<!-- 修改的样式 -->
<nz-modal [(nzVisible)]="isShowEmployeeModal" nzTitle="编辑员工" (nzOnCancel)="handleEditEmployeeCancel()" (nzOnOk)="editEmployee()">
<form nz-form [formGroup]="employeeEditForm">
<!-- usernam -->
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>姓名</nz-form-label>
<nz-form-control [nzSpan]="12" nzHasFeedback>
<input nz-input formControlName="name" placeholder="请输入姓名">
<nz-form-explain *ngIf="employeeEditForm.get('name').dirty && employeeEditForm.get('name').errors">
<ng-container *ngIf="employeeEditForm.get('name').hasError('required')">
请填写姓名
</ng-container>
<ng-container *ngIf="employeeEditForm.get('name').hasError('minlength')">
姓名长度最少为2位
</ng-container>
</nz-form-explain>
</nz-form-control>
</nz-form-item>
<!-- gender -->
<nz-form-item>
<nz-form-label [nzSpan]="7">性别</nz-form-label>
<nz-form-control [nzSpan]="12">
<nz-radio-group formControlName="gender">
<label nz-radio nzValue="0">男</label>
<label nz-radio nzValue="1">女</label>
</nz-radio-group>
</nz-form-control>
</nz-form-item>
<!-- email -->
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired>E-mail</nz-form-label>
<nz-form-control [nzSpan]="12" nzHasFeedback>
<input nz-input formControlName="email" placeholder="请输入邮箱" type="email">
<nz-form-explain *ngIf="employeeEditForm.get('email').dirty&&employeeEditForm.get('email').errors">
<ng-container *ngIf="employeeEditForm.get('email').hasError('email')">
E-mail输入格式不正确
</ng-container>
<ng-container *ngIf="employeeEditForm.get('email').hasError('required')">
请输入E-mail
</ng-container>
</nz-form-explain>
</nz-form-control>
</nz-form-item>
<!-- phone -->
<nz-form-item>
<nz-form-label [nzSpan]="7">手机号码</nz-form-label>
<div>
<nz-form-control [nzSpan]="12" nzHasFeedback>
<input nz-input placeholder="请输入手机号码" formControlName="phoneNumber">
<nz-form-explain *ngIf="employeeEditForm.get('phoneNumber').dirty&&employeeEditForm.get('phoneNumber').hasError('pattern')">手机号码格式不正确</nz-form-explain>
</nz-form-control>
</div>
</nz-form-item>
<!-- joinDate -->
<nz-form-item>
<nz-form-label [nzSm]="7" [nzXs]="24">入职时间</nz-form-label>
<nz-form-control [nzSm]="16" [nzXs]="24">
<nz-date-picker formControlName="joinDate"></nz-date-picker>
<nz-form-explain *ngIf="employeeEditForm.get('joinDate').dirty&&employeeEditForm.get('joinDate').errors">
<ng-container *ngIf="employeeEditForm.get('joinDate').hasError('date')">
入职时间不能早于今天
</ng-container>
</nz-form-explain>
</nz-form-control>
</nz-form-item>
<!-- add button -->
<nz-form-item>
<nz-form-control [nzOffset]="7" [nzSpan]="12">
<button nz-button nzType="primary" [disabled]="!employeeEditForm.valid">添加</button>
</nz-form-control>
</nz-form-item>
</form>
</nz-modal>
employee-list.component.ts
....
import {
FormBuilder,
FormControl,
FormGroup,
ValidationErrors,
Validators
} from '@angular/forms'
// 手机号码的正则
const PHONE_NUMBER_REGEXP = /^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\d{8}$/
.....
export class EmployeeListComponent implements OnInit {
constructor(private employeesService: EmployeesService,
private nzMessageService: NzMessageService,
private fb: FormBuilder) { }
ngOnInit() {
this.fetchData()
this.employeeEditForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
gender: ['0'],
email: ['',[Validators.required, Validators.email]],
phoneNumber: ['', [Validators.pattern(PHONE_NUMBER_REGEXP)]],
joinDate: ['', [this.joinDateValidate]]
});
}
// 日期的自定义校验规则
joinDateValidate(control: FormControl) {
const selectDate = +control.value
// const selectDate = control.value
// console.log(selectDate)
const curDate = +new Date()
if (selectDate > curDate) { return { date: true } }
return null
}
// 修改
isShowEmployeeModal = true
employeeEditForm: FormGroup;
showEditEmployeeModal(): void {
this.isShowEmployeeModal = true;
}
// 确定修改
editEmployee(): void {
console.log('确定修改');
this.isShowEmployeeModal = false;
}
// 取消修改
handleEditEmployeeCancel(): void {
console.log('取消修改');
this.isShowEmployeeModal = false;
}
}
② 展示员工数据
- 需要拿到需要修改数据的ID
- 根据ID获取员工的数据
- 去
employees.service.ts
服务中
// 根据id查询员工信息
getEmployeeById(id: number) {
return this.http.get<Employee>(`${URL}/employees/${id}`)
}
- 在
employee-list.component.html
中的showEditEmployeeModal
方法中添加参数ID
<a (click)="showEditEmployeeModal(data.id)"><span>修改</span></a>
- 在
employee-list.component.ts
中的showEditEmployeeModal
方法中去根据ID获取数据
showEditEmployeeModal(id: number): void {
this.isShowEmployeeModal = true;
// 根据id获取员工的数据
this.employeesService.getEmployeeById(id)
.subscribe((employee: Employee) => {
console.log(employee)
})
}
- 将获取到的数据展示于表单中,发现由于日期没有显示出来,会进行报错(时间的类型不一样)
showEditEmployeeModal(id: number): void {
this.isShowEmployeeModal = true;
// 根据id获取员工的数据
this.employeesService.getEmployeeById(id)
.subscribe((employee: Employee) => {
// 将获取到的数据展示于表单中
this.employeeEditForm.patchValue(employee)
})
}
- 将日期进行类型转换
showEditEmployeeModal(id: number): void {
this.isShowEmployeeModal = true;
// 根据id获取员工的数据
this.employeesService.getEmployeeById(id)
.subscribe((employee: Employee) => {
// console.log(employee)
// 将获取到的数据展示于表单中
const { joinDate } = employee
this.employeeEditForm.patchValue({
...employee,
joinDate: new Date(joinDate)
})
})
}
③ 更新员工数据
- 先进行数据的验证
// 确定修改
editEmployee(): void {
// console.log('确定修改');
this.isShowEmployeeModal = false;
const employeeEditForm = this.employeeEditForm
const { controls } = employeeEditForm
for (const key in controls) {
controls[ key ].markAsDirty();
controls[ key ].updateValueAndValidity();
}
console.log(employeeEditForm.valid)
if(!employeeEditForm.valid) { return }
}
- 去
employees.service.ts
服务中
// 根据员工id更新员工
updateEmployeeById(id: number, params: Employee) {
return this.http.patch<Employee>(${URL}/employees/${id}, params)
}
- 在
employee-list.component.html
中的editEmployee
方法中发起请求,更新员工信息,可以在控制台打印出编辑
// 确定修改
editEmployee(): void {
....
let { joinDate } = employeeEditForm.value
if(!joinDate) { joinDate = +new Date() }
const params = { ...employeeEditForm.value, joinDate: +joinDate}
this.employeesService.updateEmployeeById(this.editEmployeeId, params)
.subscribe(res => {
console.log('编辑成功', res)
})
}
在
editEmployee
方法中是拿不到ID的, 需要在showEditEmployeeModal
方法中获取ID的时候进行一些保存 。... editEmployeeId: number ... showEditEmployeeModal(id: number): void { this.editEmployeeId = id .... }
- 使其编辑项在列表展示出来
// 确定修改
editEmployee(): void {
......
this.employeesService.updateEmployeeById(this.editEmployeeId, params)
.subscribe((res: Employee) => {
console.log('编辑成功', res)
// 将关闭窗口的功能移下来
this.isShowEmployeeModal = false
const index = this.employeesList.findIndex(employee => employee.id === res.id)
this.employeesList[index] = res
})
}
④ 关闭编辑窗口数据重置
把添加的重置取过来。在editEmployee
和handleEditEmployeeCancel
方法中调用一下
resetEmployee() {
const employeeEditForm = this.employeeEditForm
const { controls } = employeeEditForm
this.employeeEditForm.reset()
Object.keys(controls).forEach(key => {
controls[key].markAsPristine()
controls[key].updateValueAndValidity()
})
}
完
本文首次发布于 Azr的博客, 作者 @azrrrrr ,转载请保留原文链接.
原文链接: http://amor9.cn/2019/02/02/ng7/