在上一篇博客中,我们实现了第一个angular组件,并把它作为了我们的主页面。在这期博客中,我们将实现用户注册功能的前端以及后端功能的实现,真正进入到全栈开发。
八 用户注册功能的开发
这个功能的实现分为两部分:前端部分和后端部分。前端部分包括angular组件的建立以及服务的建立,而后端部分为tornado服务器部分以及对应的数据库表的建立。我们之后的每个功能都会如下分别介绍前端和后端部分。
我们的这个register组件是一个让用户输入各种注册信息的表单。因此,这里有必要介绍一下angular中的表单。
1 angular中表单的介绍
angular提供了两大类表单:响应式表单和模板驱动型表单。响应式表单在后台是依靠表单类来建立的,表单中的每个元素都对应表单类对象的一个成员,类似Django提供的表单;与模板驱动型表单相比,响应式表单更加稳定,且在可复用性上更胜一筹。模板驱动型表单并不是通过类来实现的,而是需要用户自己去处理每个元素所对应的变量。模板驱动型表单不可复用,适用于简单场景。
下面让我们看一下这两种表单在数据绑定上的不同。响应式表单将整个表单视为一个FormGroup对象,将每个表单中的元素视为一个FormControl对象,一个FormGroup对象可以包括多个FormControl对象,这样就简单地将表单和其背后的模型建立了联系。每当我们操作前端表单元素的时候,值的变化会同步更新到其所对应的FormControl对象中;反之,如果我们在后台中修改了FormControl的值,那么对应表单元素的值也会随之改变,如图所示:
而对于模板驱动型表单,我们并没有一个FormGroup类来管理整个表单,我们需要通过ngModel来隐式为每个表单元素来建立FormControl,并且不能直接访问这个隐式的FormControl对象:
通过这两种表单,都可以让我们的前端输入的数据同步到后面的模型中。个人感觉,响应式表单处理起来更加方便,因此这个系列的博客大多采用响应式表单。
2 前端部分
2.1 注册组件的建立
我们打开cmd页面,输入如下命令建立register组件:
ng g c register
稍等片刻,ng工具就会生成register组件。
我们要使用NG-ZORRO提供的表单样式,以及使用angular提供的表单模块。因此,我们在app.module.ts里引入这些模块:
//app.module.ts
//其他模块
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzMessageModule } from 'ng-zorro-antd/message';
//其他模块
@NgModule({
declarations: [
AppComponent,
RegisterComponent,
MainlayoutComponent,
],
imports: [
//其他模块
FormsModule,
ReactiveFormsModule,
NzFormModule,
NzInputModule,
NzButtonModule,
NzMessageModule,
//其他模块
],
providers: [CookieService],
bootstrap: [AppComponent]
})
export class AppModule { }
这里为了篇幅起见,将其他模块隐去。我们现在引入了这些模块:
- FormsModule:angular提供的模板驱动型表单模块,包括一些基础的表单功能。
- ReactiveFormsModule:angular提供的响应式表单模块,是我们这个系列博客中主要使用的表单类型。
- NzButtonModule:NG-ZORRO的按钮模块,提供一些好看的按钮样式。
- NzFormModule:NG-ZORRO的表单模块,需要依赖angular的表单模块,也是提供一些自己的控制逻辑以及样式。
- NzInputModule:NG-ZORRO的输入域模块,提供各种input标签。
- NzMessageModule:NG-ZORRO的消息模块,提供各种弹出信息以及弹出提示。
我们打开register.component.html,开始编写html代码:
<!--register.component.html-->
<form nz-form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<nz-form-item>
<nz-form-label [nzSm]="6" [nzXs]="24" nzFor="username" nzRequired>用户名</nz-form-label>
<nz-form-control [nzSm]="14" [nzXs]="24">
<input
nz-input
type="text"
id="username"
formControlName="username"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSm]="6" [nzXs]="24" nzFor="password" nzRequired>密码</nz-form-label>
<nz-form-control [nzSm]="14" [nzXs]="24">
<input
nz-input
type="password"
id="password"
formControlName="password"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSm]="6" [nzXs]="24" nzFor="checkpassword" nzRequired>确认密码</nz-form-label>
<nz-form-control [nzSm]="14" [nzXs]="24">
<input nz-input type="password" formControlName="checkpassword" id="checkpassword" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="email">E-mail</nz-form-label>
<nz-form-control [nzSm]="14" [nzXs]="24">
<input nz-input formControlName="email" id="email" />
</nz-form-control>
</nz-form-item>
<nz-form-item nz-row style="margin-bottom:8px;">
<nz-form-control [nzSpan]="14" [nzOffset]="6">
<button nz-button nzType="primary">注册</button>
</nz-form-control>
</nz-form-item>
</form>
在这里我们建立了一个名为registerForm的响应式表单,包含以下元素:
- username,用户名
- password,密码
- checkpassword,确认密码
- email,电子邮件
- 一个注册按钮
以上这些名称均为后端的FormControl对象名称,而registerForm为后端的FormGroup对象名称。(ngSubmit)表明当我们提交表单时,要调用哪个函数,这里我们提交表单时调用的是onSubmit函数。
我们打开register.component.css文件,加入以下代码:
[nz-form] {
max-width: 600px;
}
再打开register.component.ts文件,开始编写html背后的逻辑部分:
//register.component.ts
import { Component, OnInit } from '@angular/core';
import { FormControl,FormGroup } from '@angular/forms';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {
registerForm = new FormGroup({
username:new FormControl(''),
password:new FormControl(''),
checkpassword:new FormControl(''),
email:new FormControl('')
});
constructor() {
}
onSubmit(){
console.log(this.registerForm.value)
}
ngOnInit() {
}
}
在这个文件里,我们引入了FormControl和FormGroup两个类,并定义了一个名为registerForm的FormGroup对象,其中包含4个FormControl对象,分别名为username, password, checkpassword和email。注意,这几个对象的名称要和html中的[formGroup]还有formControlName一一对应,否则编译会报错。
然后,我们又实现了一个简单的onSubmit函数,功能为在控制台中印出通过表单得到的值。
这样,我们就建立好了前端的页面,该为它添加一个路由了,以便我们能从主页访问到这个页面。
我们打开app-routing.module.ts,在routes数组中加入以下一行:
//app-routing.module.ts
//...
const routes: Routes = [
{path:'register',component:RegisterComponent},
];
//...
然后打开我们的主页面组件的mainlayout.component.html文件,为菜单栏中的注册菜单项添加路由:
<!--src\app\mainlayout\mainlayout.component.html-->
<!--...-->
<!--两个注册的地方都要加-->
<li nz-menu-item routerLink='/register'>注册</li>
<!--...-->
在angular中,所谓的路由都是指在不同的组件间路由,而不是传统中的在不同html页面中路由。angular提供了routerLink标签来指定组件在angular的url,这也和传统html中a标签的href属性有区别。因此,这行代码的含义为当点击注册菜单项时,会访问到/register这个url,并根据path中的设定在下面的<router-outlet></router-outlet>
标签位置显示我们刚实现的register组件。
下面让我们保存所有修改的文件,输入npm start。待项目跑起来后,点击菜单栏上的注册,会发现弹出了我们的注册表单:
让我们输入一点东西,打开网页控制台,点击注册按钮,会看到我们输入的东西在控制台中显示出来了,表明已经触发了onSubmit函数:
2.2 注册服务的建立
我们已经实现了注册组件的html部分,以及实现了一个假的onSubmit函数来显示我们输入到表单的值。现在我们来建立一个服务,从而让表单数据可以真正提交到我们的tornado server上。
服务(service)是angular中的一个重要概念。angular采用依赖注入的设计模式,所以我们的组件不直接负责与后台server交互,而是将若干个service作为自身的成员变量,让这些service去实现与后台server交互的方法,从而实现组件与后台server的通信。这样的好处是,只需改动service的代码,就可以修改组件的行为,而无需动到组件本身的代码。
我们打开cmd,输入以下命令,建立一个service:
ng g s service/register
稍等片刻,ng工具会为我们建立service目录,并在底下建立register.service.ts文件,这便是我们建立的service了。
这个service我们主要用于访问tornado的server,因此我们需要在app.module.ts中引入HttpClientModule模块:
//app.module.ts
import { HttpClientModule } from '@angular/common/http';
顾名思义,这个模块里的组件可以让我们通过http方式访问服务器。
现在,让我们在register目录下建立一个名为registerUser.ts的文件,并输入以下内容:
//src\app\register\registerUser.ts
export interface registerUserData {
username: string;
password: string;
checkpassword:string;
email:string;
}
我们在这个文件里建立了一个名为registerUserData的接口。angular的接口虽然也叫interface,但它是一种数据类型,而不是抽象类。interface规定了前端要给后端传递什么格式的数据,本质是一种指定了key和value的json数据。
让我们回到register.service.ts文件,编写以下内容:
//src\app\service\register.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { registerUserData } from '../register/registerUser';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class RegisterService {
constructor(private http:HttpClient) { }
registerUser(userData:registerUserData):Observable<registerUserData>{
return this.http.post<registerUserData>('http://localhost:8000/register',userData);
}
}
我们在这个service里定义了一个HttpClient对象,这使得我们的service有能力去给后端的server发送http请求;接下来我们实现了registerUser对象,其接受registerUserData类型的数据,并以post的方式发送到我们的server地址中。
这样,我们的service就写完了,让我们把它放到register组件中去。打开register.component.ts文件,修改如下:
//src\app\register\register.component.ts
import { Component, OnInit } from '@angular/core';
import { FormControl,FormGroup } from '@angular/forms';
import { RegisterService } from '../service/register.service';
import { registerUserData } from './registerUser';
import { Router } from '@angular/router';
import { NzMessageService } from 'ng-zorro-antd/message';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {
registerForm = new FormGroup({
username:new FormControl(''),
password:new FormControl(''),
checkpassword:new FormControl(''),
email:new FormControl('')
});
newUser:registerUserData;
constructor(private registerService:RegisterService,private route:Router,private message: NzMessageService) {
this.newUser = {username:'',password:'',checkpassword:'',email:''}
}
onSubmit(){
console.log(this.registerForm.value)
this.registerUser()
}
registerUser():void{
this.newUser = this.registerForm.value;
if (this.newUser.password != this.newUser.checkpassword)
{
this.message.error('两次输入的密码不一致')
}
else
{
this.registerService.registerUser(this.newUser).subscribe((data:any) =>
{
if (data['result'] == 'Success')
{
this.registerForm.setValue({
username:'',
password:'',
checkpassword:'',
email:''
});
//跳转到首页
this.route.navigateByUrl('')
}
else
{
console.log(data);
this.message.error('注册失败');
}
}
)
}
}
ngOnInit() {
}
}
我们这次引入了RegisterService类、registerUserData接口以及Router和NzMessageService类。Router用于稍后的重定向页面,而NzMessageService类用于弹出提示信息。我们声明了一个registerUserData类型的新对象newUser,用于接收表单的数据;注意到我们修改了组件的构造函数,声明了RegisterService,Router和NzMessageService类的对象各一个,并且在构造函数中初始化了newUser(这里是因为我们开启了严格模式的原因,如果不开启严格模式的话,这里可以不初始化newUser)。
然后,我们开始实现核心函数——registerUser。这个函数的主要目的就是来调用我们的registerService,通过service将表单数据发送给后台server并得到返回值。由于我们这里使用响应式表单,所以我们可以直接通过this.newUser=this.registerForm.value将表单的值赋给newUser对象。然后我们这里做一个简单的验证:如果确认密码和密码两项输入的内容不一样,就弹出错误信息,如果一样的话,就调用service提供的registerUser对象。
调用registerUser对象背后过程比较复杂,限于篇幅将把背后过程放在后端篇介绍,这里只介绍含义。这里的含义是指,前端通过调用registerUser对象,将newUser作为表单数据传递给了后台的server,随后得到了服务器返回的数据data。这里的data也是json格式的数据,其仅包含一个key:result。因此,如果data的result是Success的话,我们就会清空表单数据,并将其跳转到首页;如果data的result不是Success的话,我们会调用NzMessageService去弹出一个错误信息。
最后,我们在onSubmit函数中调用这个新写的registerUser函数。
在这期博客中,我们实现了用户注册功能的全部前端部分的代码,实现了我们的第一个表单和第一个服务。限于篇幅所限,我们将在下一篇博客中实现这个功能的后端部分,以及讲解registerUser对象调用的背后的逻辑,希望大家继续关注~