几个月前,我发布了一个教程,介绍如何使用模拟后端在Angular 2中构建用户注册和登录功能,它包括我最近为悉尼律师事务所开发的安全Web应用程序的样板前端代码。 在这篇文章中,我将扩展它,添加一个建立在Node,Express和Mongo上的真正后端API,使其成为具有Angular 2/4(MEAN Stack 2/4)应用程序的完整MEAN Stack。
项目代码可在GitHub上获取。
如果您使用AngularJS / Angular 1构建的类似应用,您可以查看MEAN Stack 1.0 - User Registration and Login Example & Tutorial
如果您使用ASP.NET Core Web API开发,请查看ASP.NET Core + Angular 2/4 - User Registration and Login Tutorial & Example
Update History:
- 25 May 2017 - Updated to Angular 4.1.0
- 17 Mar 2017 - Updated to Angular 2.4.9
- 22 Feb 2017 - Built with Angular 2.2.1
在本地运行Angular 2/4 - 用户注册和登录示例
注意: 若要在本地运行本示例,您需要安装NodeJS并运行MongoDB。请参阅Setup the MEAN Stack on Windows 。
该项目包含两个应用程序,一个用于客户端,另一个用于服务器api,以下是每个应用程序设置和运行的步骤:
运行MEAN Stack 服务端API
- 确认MongoDB已经安装并运行
- 打开命令行并定位到项目根文件夹下的
/server
文件夹下 - 运行
npm install
以安装在package.json文件中定义的所有必需的npm软件包 - 运行
node server.js
启动服务端,服务端默认地址:http://localhost:4000
运行Angular 2/4客户端
- 打开命令行窗口并导航到项目根目录下的
/client
文件夹 - 运行
npm install
以安装在package.json文件中定义的所有必需的npm软件包 - 运行
npm start
启动客户端,浏览器会自动打开http://localhost:3000
MEAN with Angular 2/4 项目结构
-
- app
- _directives
- alert.component.html
- alert.component.ts
- index.ts
- _guards
- auth.guard.ts
- index.ts
- _helpers
- custom-http.ts
- index.ts
- _models
- user.ts
- index.ts
- _services
- home
- home.component.html
- home.component.ts
- index.ts
- login
- index.ts
- login.component.html
- login.component.ts
- register
-
-
-
-
-
-
- _directives
- app.css
- index.html
- package.json
- system.config.js
- tsconfig.json
- app
-
- controllers
- services
-
- package.json
-
MEAN with Angular 2/4 客户端
我使用Angular 2 quickstart项目作为客户端应用程序的基础,他使用TypeScript
编写并使用systemjs
加载模块。如果你是Angular 2的新手,我建议你去看一下Angular 2 quickstart
,因为他详细的介绍了项目工具和配置文件,本文不再赘述。
该项目和代码结构主要遵循官方Angular 2 style guide中的建议,我在其基础上做了一些调整。
Angular 2/4 Alert Component 模板
alert component 模板包含在页面顶部显示alert信息的html
<div *ngIf="message" [ngClass]="{ 'alert': message, 'alert-success': message.type === 'success', 'alert-danger': message.type === 'error' }">{{message.text}}</div>
Angular 2/4 Alert Component
每当从警报服务接收到消息时,警报组件都会将警报消息传递给模板。 它通过订阅alertService
的getMessage()
方法来返回一个Observable。
Angular 2/4 授权守卫
授权守卫用于防止未经身份验证的用户访问受限路由,在本示例中,它被用于在app.routing.ts
守卫home page 路由。有关angular 2 守卫的更多信息你可以查看thoughtram blog中的文章。
import {Injectable} from '@angular/core';
import {Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private router : Router) {}
canActivate(route : ActivatedRouteSnapshot, state : RouterStateSnapshot) {
if (localStorage.getItem('currentUser')) {
// 已登录所以返回true
return true;
}
// 未登录,所以跳转到登录页并且返回url
this
.router
.navigate(['/login'], {
queryParams: {
returnUrl: state.url
}
});
return false;
}
}
Angular 2/4 自定义 Http 服务
自定义http服务扩展了默认的http服务以添加以下功能:
- 它会自动将JWT令牌(如果登录)添加到所有请求的http授权头
- 使用appConfig文件中的api url预先提交请求URL
- 拦截来自api的401 unauthorized 响应并自动登出用户
import {Injectable} from "@angular/core";
import {
ConnectionBackend,
XHRBackend,
RequestOptions,
Request,
RequestOptionsArgs,
Response,
Http,
Headers
} from "@angular/http";
import {appConfig} from '../app.config';
import {Observable} from "rxjs/Observable";
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/throw';
@Injectable()
export class CustomHttp extends Http {
constructor(backend : ConnectionBackend, defaultOptions : RequestOptions) {
super(backend, defaultOptions);
}
get(url : string, options?: RequestOptionsArgs) : Observable < Response > {
return super
.get(appConfig.apiUrl + url, this.addJwt(options))
.catch(this.handleError);
}
post(url : string, body : string, options?: RequestOptionsArgs) : Observable < Response > {
return super
.post(appConfig.apiUrl + url, body, this.addJwt(options))
.catch(this.handleError);
}
put(url : string, body : string, options?: RequestOptionsArgs) : Observable < Response > {
return super
.put(appConfig.apiUrl + url, body, this.addJwt(options))
.catch(this.handleError);
}
delete(url : string, options?: RequestOptionsArgs) : Observable < Response > {
return super
.delete(appConfig.apiUrl + url, this.addJwt(options))
.catch(this.handleError);
}
// 私有的 helper 方法
private addJwt(options?: RequestOptionsArgs) : RequestOptionsArgs {
// 确保请求options和headers不为null
options = options || new RequestOptions();
options.headers = options.headers || new Headers();
// 添加jwt token的authorization header
let currentUser = JSON.parse(localStorage.getItem('currentUser'));
if (currentUser && currentUser.token) {
options
.headers
.append('Authorization', 'Bearer ' + currentUser.token);
}
return options;
}
private handleError(error : any) {
if (error.status === 401) {
// 返回 401 unauthorized 所以登出用户
window.location.href = '/login';
}
return Observable.throw(error._body);
}
}
export function customHttpFactory(xhrBackend : XHRBackend, requestOptions : RequestOptions) : Http {
return new CustomHttp(xhrBackend, requestOptions);
}
export let customHttpProvider = {
provide: Http,
useFactory: customHttpFactory,
deps: [XHRBackend, RequestOptions]
};
Angular 2/4 User Model
这是一个定义用户属性的Model
export class User {
_id: string;
username: string;
password: string;
firstName: string;
lastName: string;
}
Angular 2/4 Alert Service
Alert Service使应用程序中的任何组件能够通过Alert组件在页面顶部显示警报消息。
它具有显示成功和错误消息的方法,以及一个getMessage()
方法,返回一个Observable,该Alert组件订阅于每当需要显示消息的时候( the alert component to subscribe to notifications for whenever a message should be displayed.)。
import {Injectable} from '@angular/core';
import {Router, NavigationStart} from '@angular/router';
import {Observable} from 'rxjs';
import {Subject} from 'rxjs/Subject';
@Injectable()
export class AlertService {
private subject = new Subject < any > ();
private keepAfterNavigationChange = false;
constructor(private router : Router) {
//在路由变化时清除警告信息
router
.events
.subscribe(event => {
if (event instanceof NavigationStart) {
if (this.keepAfterNavigationChange) {
//只保持一个地址变化???
this.keepAfterNavigationChange = false;
} else {
//清除警告信息
this
.subject
.next();
}
}
})
}
success(message : string, keepAfterNavigationChange = false) {
this.keepAfterNavigationChange = keepAfterNavigationChange;
this
.subject
.next({type: 'success', text: message});
}
error(message : string, keepAfterNavigationChange = false) {
this.keepAfterNavigationChange = keepAfterNavigationChange;
this
.subject
.next({type: 'error', text: message});
}
getMessage() : Observable < any > {
return this
.subject
.asObservable();
}
}
Angular 2/4 Authentication Service
Authentication Service用于登录和注销应用程序,登陆后用户将凭据发送到api并查看JWT令牌的响应,如果有令牌则意味着认证成功,添加包含令牌的用户详细信息到local storage。
将用户详细信息储存到local storage,以便用户在刷新浏览器以及在浏览器会话之间保持登录状态,直到用户注销。如果您不希望用户在刷新或会话之间保存登录,则可将用户详细信息存储在session storage或者Authentication Service的属性之类的较不持久的位置来轻松更改此行为。
import {Injectable} from '@angular/core';
import {Http, Headers, Response} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
@Injectable()
export class AuthenticationService {
constructor(private http : Http) {}
login(username : string, password : string) {
return this
.http
.post('users/authenticate', {
username: username,
password: password
})
.map((response : Response) => {
// 如果返回jwt token则登录成功
let user = response.json();
if (user && user.token) {
//在local storage中存储用户详细信息和jwt token来在页面刷新之间保持用户登录
localStorage.setItem('currentUser', JSON.stringify(user));
}
return user;
})
}
logout() {
// 从local storage中删除user来登出
localStorage.removeItem('currentUser');
}
}
Angular 2/4 User Service
User Service包含一组通过api管理用户的CRUD方法。
import {Injectable} from '@angular/core';
import {Http, Headers, RequestOptions, Response} from '@angular/http';
import {User} from '../_models/index';
@Injectable()
export class UserService {
constructor(private http : Http) {}
getAll() {
return this
.http
.get('/users')
.map((response : Response) => response.json());
}
getById(_id : string) {
return this
.http
.get('/users/' + _id)
.map((response : Response) => response.json());
}
create(user : User) {
return this
.http
.post('/users/register', user);
}
update(user : User) {
return this
.http
.put('/users/' + user._id, user);
}
delete(_id : string) {
return this
.http
.delete('/users/' + _id);
}
}
Angular 2/4 Home Component 模板
Home Component 模板包含用于显示简单的欢迎消息,用户列表和注销链接的html和Angular 2模板语法。
<div class="col-md-6 col-md-offset-3">
<h1>Hi {{currentUser.firstName}}!</h1>
<p>You're logged in with the MEAN Stack & Angular 2!!</p>
<h3>All registered users:</h3>
<ul>
<li *ngFor="let user of users">
{{user.username}} ({{user.firstName}} {{user.lastName}})
- <a (click)="deleteUser(user._id)">Delete</a>
</li>
</ul>
<p><a [routerLink]="['/login']">Logout</a></p>
</div>
Angular 2/4 Home Component
Home Component从local storage 中获取当前用户,并从User Service中获取所有用户,填充到模板中。
import {Component, OnInit} from '@angular/core';
import {User} from '../_models/index';
import {UserService} from '../_services/index';
@Component({moduleId: module.id, templateUrl: 'home.component.html'})
export class HomeComponent implements OnInit {
currentUser : User;
users : User[];
constructor(private userService : UserService) {
this.currentUser = JSON.parse(localStorage.getItem('currentUser'));
}
ngOnInit() {
this.loadAllUsers();
}
deleteUser(_id : string) {
this
.userService
.delete(_id)
.subscribe(() => {
this.loadAllUsers()
});
}
private loadAllUsers() {
this
.userService
.getAll()
.subscribe(users => {
this.users = users
});
}
}
Angular 2/4 Login Component 模板
登录组件模板包含一个具有用户名和密码字段的登录表单,当单机提交按钮时,它会显示无效字段的验证信息。只要表单有效,
<div class="col-md-6 col-md-offset-3">
<h2>Login</h2>
<form name="form" (ngSubmit)="f.form.valid && login()" #f="ngForm" novalidate>
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
<label for="username">Username</label>
<input type="text" class="form-control" name="username" [(ngModel)]="model.username" #username="ngModel" required />
<div *ngIf="f.submitted && !username.valid" class="help-block">Username is required</div>
</div>
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !password.valid }">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" [(ngModel)]="model.password" #password="ngModel" required />
<div *ngIf="f.submitted && !password.valid" class="help-block">Password is required</div>
</div>
<div class="form-group">
<button [disabled]="loading" class="btn btn-primary">Login</button>
<img *ngIf="loading" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA=="
/>
<a [routerLink]="['/register']" class="btn btn-link">Register</a>
</div>
</form>
</div>
Angular 2/4 Login Component
登录组件使用身份验证服务来登录和注销应用程序,它在初始化(ngOnInit)时自动登出用户,因此登录页面也可用于注销。
import {Component, OnInit} from '@angular/core';
import {Router, ActivatedRoute} from '@angular/router';
import {AlertService, AuthenticationService} from '../_services/index';
@Component({moduleId: module.id, templateUrl: 'login.component.html'})
export class LoginComponent implements OnInit {
model : any = {};
loading = false;
returnUrl : string;
constructor(private route : ActivatedRoute, private router : Router, private authenticationService : AuthenticationService, private alertService : AlertService) {}
ngOnInit() {
//重置登录状态
this
.authenticationService
.logout();
//从路由参数中获取返回的url,或者默认为“/”
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
}
login() {
this.loading = true;
this
.authenticationService
.login(this.model.username, this.model.password)
.subscribe(data => {
this
.router
.navigate([this.returnUrl]);
}, error => {
this
.alertService
.error(error);
this.loading = false;
})
}
}
Angular 2/4 Register Component 模板
注册组件模板包含一个简单的注册表单,其中包含名字,姓氏,用户名和密码等字段。当单击提交按钮时,它会显示无效字段的验证消息。如果表单有限,则调用register()
方法来提交。
<div class="col-md-6 col-md-offset-3">
<h2>Register</h2>
<form name="form" (ngSubmit)="f.form.valid && register()" #f="ngForm" novalidate>
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
<label for="firstName">First Name</label>
<input type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName" #firstName="ngModel" required />
<div *ngIf="f.submitted && !firstName.valid" class="help-block">First Name is required</div>
</div>
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
<label for="lastName">Last Name</label>
<input type="text" class="form-control" name="lastName" [(ngModel)]="model.lastName" #lastName="ngModel" required />
<div *ngIf="f.submitted && !lastName.valid" class="help-block">Last Name is required</div>
</div>
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !username.valid }">
<label for="username">Username</label>
<input type="text" class="form-control" name="username" [(ngModel)]="model.username" #username="ngModel" required />
<div *ngIf="f.submitted && !username.valid" class="help-block">Username is required</div>
</div>
<div class="form-group" [ngClass]="{ 'has-error': f.submitted && !password.valid }">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" [(ngModel)]="model.password" #password="ngModel" required />
<div *ngIf="f.submitted && !password.valid" class="help-block">Password is required</div>
</div>
<div class="form-group">
<button [disabled]="loading" class="btn btn-primary">Register</button>
<img *ngIf="loading" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA=="
/>
<a [routerLink]="['/login']" class="btn btn-link">Cancel</a>
</div>
</form>
</div>
Angular 2/4 Register Component
Register Component只有一个register()
方法,当注册表单被用户提交后通过用户服务来创建新的用户。
import {Component} from '@angular/core';
import {Router} from '@angular/router';
import {AlertService, UserService} from '../_services/index';
@Component({moduleId: module.id, templateUrl: 'register.component.html'})
export class RegisterComponent {
model : any = {};
loading = false;
constructor(private router : Router, private userService : UserService, private alertService : AlertService) {}
register() {
this.loading = true;
this
.userService
.create(this.model)
.subscribe(data => {
this
.alertService
.success('Registration successful', true);
this
.router
.navigate(['/login']);
}, error => {
this
.alertService
.error(error);
this.loading = false;
});
}
}
Angular 2/4 App Component 模板
App Component模板是应用程序的根组件模板,它包括根据当前路由显示每个视图内容的路由插座指令,以及用于从系统中任何位置显示alert信息的alert指令。
<!-- main app container -->
<div class="jumbotron">
<div class="container">
<div class="col-sm-8 col-sm-offset-2">
<alert></alert>
<router-outlet></router-outlet>
</div>
</div>
</div>
<!-- credits -->
<div class="text-center">
<p>
<a href="http://jasonwatmore.com/post/2017/02/22/mean-with-angular-2-user-registration-and-login-example-tutorial" target="_top">MEAN Stack with Angular 2 - User Registration and Login Example & Tutorial</a>
</p>
<p>
<a href="http://jasonwatmore.com" target="_top">JasonWatmore.com</a>
</p>
</div>
Angular 2/4 App Component
App Component是应用程序的根组件,它定义了根标签<app></app>
和它的selector属性。
moduleId属性用于为templateUrl设置为允许相对路径。
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'app',
templateUrl: 'app.component.html'
})
export class AppComponent { }
Angular 2/4 App Config
App Config文件用于存储应用配置变量(如api url),把他们单独的放在一起可以使任何组件都可以轻易地引入它。在本例中,它被用于User Service 和Authentication Service。
export const appConfig = {
apiUrl: 'http://localhost:4000'
};
Angular 2/4 App Module
App Module定义了应用程序的根模块以及有关模块的元数据。有关Angular 2的更多信息,请查看官方文档。
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';
import {AppComponent} from './app.component';
import {routing} from './app.routing';
import {customHttpProvider} from './_helpers/index';
import {AlertComponent} from './_directives/index';
import {AuthGuard} from './_guards/index';
import {AlertService, AuthenticationService, UserService} from './_services/index';
import {HomeComponent} from './home/index';
import {LoginComponent} from './login/index';
import {RegisterComponent} from './register/index';
@NgModule({
imports: [
BrowserModule, FormsModule, HttpModule, routing
],
declarations: [
AppComponent, AlertComponent, HomeComponent, LoginComponent, RegisterComponent
],
providers: [
customHttpProvider, AuthGuard, AlertService, AuthenticationService, UserService
],
bootstrap: [AppComponent]
})
export class AppModule {}
Angular 2/4 App Routing
应用程序路由文件用来定义应用程序的路由,每个路由包含路径和关联组件,通过将AuthGuard传到路由的canActivate属性来保护主路由。
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/index';
import { LoginComponent } from './login/index';
import { RegisterComponent } from './register/index';
import { AuthGuard } from './_guards/index';
const appRoutes: Routes = [
{ path: '', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
// otherwise redirect to home
{ path: '**', redirectTo: '' }
];
export const routing = RouterModule.forRoot(appRoutes);
Angular 2/4 Main (Bootstrap) File
主文件是通过Angular启动和引导应用程序使用的入口点。
```js
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);