这套代码实现了一个 Angular 单页应用内的用户登录 / 注册功能模块,核心包含「前端交互层(组件 + 模板)」和「状态管理层(Signals 服务)」。整体基于 Angular 原生能力实现,无第三方状态管理库,用 Signals 替代传统 RxJS 实现响应式,依赖 Bootstrap 快速实现基础样式;
Angular 安装和项目初始化
安装angular
npm install -g @angular/cli
初始化项目
ng new ui --routing=true --style=css
开始调试服务
cd ui
ng serve
添加 Bootstrap
添加 jquery 和 bootstrap
npm add jquery
npm add bootstrap@4.5.3
修改 angular.json 加入样式和脚本引用
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css"
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
],
添加 angular component
mkdir components
cd components
ng generate component navbar
定义 app-navbar
package\ui\src\app\components\navbar\navbar.ts
定义组件和传参
子组件接参:
- @Input() 定义要传入的属性。
- !确定为非空,不需要初始化
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-navbar',
imports: [],
templateUrl: './navbar.html',
styleUrl: './navbar.css',
})
export class Navbar {
@Input()
title!: string
}
定义组件html模板
package\ui\src\app\components\navbar\navbar.html
<div class="sitenav d-flex justify-content-center align-items-center">
<a class="btn btn-primary btn-home" to="/"></a>
<h5 class="my-2 brand">{{title}}</h5>
<a class="btn btn-primary btn-manage" to="/manage"></a>
</div>
添加引用
app.ts 添加引用
package\ui\src\app\app.ts
- 引用 Navbar
- imports Navbar
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Navbar } from './components/navbar/navbar';
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
Navbar,
],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected readonly title = signal('ui');
}
app.html 添加引用
package\ui\src\app\app.html
与之前定义的selector相匹配 - 父组件传参通过 title 赋值
<div class="page-welcome">
<app-navbar title="Book hotel" />
<router-outlet></router-outlet>
</div>
添加新页面
创建页面组件
ng generate component pages/book
页面结构
src/app/pages/home/
├── home.component.ts # 组件逻辑
├── home.component.html # 页面模板(HTML)
├── home.component.css # 页面样式
└── home.component.spec.ts # 测试文件(新手可暂时忽略)
修改页面模板
定义参数
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-book',
imports: [],
templateUrl: './book.html',
styleUrl: './book.css',
})
export class Book implements OnInit {
id = ""
name = ""
telephone = ""
size = 0
constructor(private route: ActivatedRoute) {
}
ngOnInit(): void {
const idInRoute = this.route.snapshot.paramMap.get('id')
if (idInRoute !== null) {
this.id = idInRoute
}
//订阅参数的变化
this.route.queryParamMap.subscribe(params => {
const idInQuery = this.route.snapshot.queryParamMap.get('id')
if (idInQuery !== null) {
this.id = idInQuery
}
})
}
}
修改页面模板
<div class="reservation">
<div class="card">
<div class="card-img-top">Book ID: {{id}}</div>
<div class="card-body">
<h5 class="card-title">My Reservation</h5>
<p class="card-text"></p>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">Name: {{name}}</li>
<li class="list-group-item">Telephone: {{telephone}}</li>
<li class="list-group-item">Table Size: {{size}}</li>
</ul>
<div class="card-body card-btns">
<a onClick={onUpdate} class="btn btn-primary card-link">Update Reservation</a>
<a onClick={onCancel} class="btn btn-default card-link">Cancel</a>
</div>
</div>
</div>
添加页面路由
package\ui\src\app\app.routes.ts
- 默认进入 Sign 登录页面
- book/:id 为带参路由
import { Routes } from '@angular/router';
import { Book } from './pages/book/book';
import { Sign } from './pages/sign/sign';
export const routes: Routes = [
{
path: '',
component: Sign
},
{
path: 'book/:id',
component: Book
},
];
Angular Signals 用户状态管理实例
Angular 16+ 推出的 Signals 是官方原生响应式工具,无需额外安装库,可直接作为轻量状态管理方案,具体原生、轻量、性能优的特点。
ng generate service state/user.state.service
这段代码是一个 Angular 全局状态服务(UserStateService),基于 Signals 管理用户相关状态,核心实现了用户注册(signUp)、登录(signIn)、根据用户名查询用户(findUser) 三个功能,状态全部存储在内存中(无持久化)。
- Injectable:Angular 服务的核心装饰器,标记该类可被依赖注入;
- signal:Angular Signals 核心 API,用于创建响应式状态;
- User 类型:定义用户数据结构,包含可选的 username(用户名)和 password(密码)字段(? 表示可选)。
import { Injectable, signal } from '@angular/core';
export type User = {
username?: string
password?: string
}
@Injectable({
providedIn: 'root',
})
export class UserStateService {
// current login user
user = signal<User>({})
// all users
list = signal<User[]>([])
async findUser(username?: string) {
if (username === undefined) {
return
}
return this.list().find(
(user) => user.username === username
)
}
async signUp(user: User) {
if (await this.findUser(user.username)) {
throw new Error("User exist")
} else {
this.list().push(user)
this.user.set(user)
}
}
async signIn(loginUser: User) {
const user = await this.findUser(loginUser.username)
if (user === undefined) {
throw new Error("User not exist")
}
if (user.password !== loginUser.password) {
throw new Error("Password not right")
}
this.user.set(user)
}
}
这是一个 Angular 登录 / 注册组件(Sign 组件),核心实现:
- 从页面输入框获取用户名和密码;
- 调用 UserStateService 完成注册(onSingUp)、登录(onSignIn);
- 登录成功后跳转到 /book/[用户名] 路径;
- 注册 / 登录失败时,在页面展示错误提示信息。
import { Component } from '@angular/core';
import { User, UserStateService } from '../../state/user.state.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-sign',
imports: [ ],
templateUrl: './sign.html',
styleUrl: './sign.css',
})
export class Sign {
message = ""
constructor(
private router: Router,
private userStateService: UserStateService,
) {
}
getUser(): User {
return {
username: document.querySelector<HTMLInputElement>("#username")?.value,
password: document.querySelector<HTMLInputElement>("#password")?.value
}
}
onLogin() {
this.router.navigate(['/book', this.userStateService.user().username])
}
async onSingUp() {
try {
await this.userStateService.signUp(this.getUser())
this.message = "User signed up"
} catch(e: any) {
this.message = e.toString()
}
}
async onSignIn() {
try {
await this.userStateService.signIn(this.getUser())
this.onLogin()
} catch(e: any) {
this.message = e.toString()
}
}
}
这是一个登录 / 注册的表单页面模板,包含用户名、密码输入框和注册 / 登录按钮,点击按钮会触发组件对应的方法,页面还会根据组件的 message 属性展示提示信息
<div class="datalist">
<form>
<h3>{{ message || "Please Login" }}</h3>
<div class="form-group">
<label htmlFor="username">Username</label>
<input type="text" class="form-control" id="username" value="admin" />
</div>
<div class="form-group">
<label htmlFor="password">Password</label>
<input type="password" class="form-control" id="password" value="123456" />
</div>
<div class="submit-wrap">
<button (click)="onSingUp()" class="btn btn-primary btn-submit btn-lg" type="button">Sign Up</button>
<button (click)="onSignIn()" class="btn btn-primary btn-submit btn-lg ml-3" type="button">Sign In</button>
</div>
</form>
</div>
