在 Angular 项目开发中,HttpClient 是处理 HTTP 网络请求的核心工具,登录认证与数据列表查询则是前端开发中最基础也最常用的功能组合。本文将从零开始,手把手教你基于 Angular 的 HttpClient 实现用户登录、Token 鉴权以及数据列表查询的完整流程,帮助你掌握 Angular 中网络请求的核心用法。
一、环境准备与基础配置
1.1 核心依赖
Angular 的 HttpClient 功能封装在@angular/common/http包中,Angular 4.3+ 版本已内置该模块,无需额外安装依赖,只需在模块中导入即可使用。
1.2 导入 HttpClientModule
首先需要在项目的根模块(通常是app.module.ts)中导入HttpClientModule,这是使用 HttpClient 的前提:
// app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { HttpClientModule } from '@angular/common/http'; // 导入HttpClient模块 import { FormsModule } from '@angular/forms'; // 用于表单双向绑定 import { AppComponent } from './app.component'; import { LoginComponent } from './components/login/login.component'; import { DataListComponent } from './components/data-list/data-list.component'; @NgModule({ declarations: [ AppComponent, LoginComponent, DataListComponent ], imports: [ BrowserModule, HttpClientModule, // 注册HttpClient模块 FormsModule // 表单模块 ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }二、封装 HTTP 请求服务
为了提高代码复用性和可维护性,最佳实践是将 HTTP 请求封装为独立的服务,而非直接在组件中编写请求逻辑。我们先创建一个api.service.ts服务,统一处理登录和数据查询请求。
2.1 创建 API 服务
使用 Angular CLI 快速创建服务:
ng generate service services/api2.2 实现请求封装(含 Token 拦截)
// src/app/services/api.service.ts import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; // 定义接口返回数据格式 interface ApiResponse<T = any> { code: number; msg: string; data: T; } // 用户登录参数类型 export interface LoginParams { username: string; password: string; } // 列表查询参数类型 export interface ListQueryParams { pageNum: number; pageSize: number; keyword?: string; } @Injectable({ providedIn: 'root' }) export class ApiService { // 基础接口地址 private baseUrl = 'http://localhost:3000/api'; // 存储Token的key private tokenKey = 'user_token'; constructor(private http: HttpClient) { } /** * 创建请求头(自动携带Token) */ private getHeaders(): HttpHeaders { const token = localStorage.getItem(this.tokenKey); let headers = new HttpHeaders({ 'Content-Type': 'application/json' }); // 如果有Token,添加到请求头 if (token) { headers = headers.set('Authorization', `Bearer ${token}`); } return headers; } /** * 错误处理 */ private handleError(error: HttpErrorResponse): Observable<never> { let errorMsg = '未知错误'; if (error.error instanceof ErrorEvent) { // 客户端错误 errorMsg = `客户端错误: ${error.error.message}`; } else { // 服务端错误 errorMsg = `服务端错误: ${error.status} - ${error.message}`; // 登录失效(401),清除Token并跳转登录页 if (error.status === 401) { this.clearToken(); window.location.href = '/login'; } } console.error('请求异常:', errorMsg); return throwError(() => new Error(errorMsg)); } /** * 保存Token */ setToken(token: string): void { localStorage.setItem(this.tokenKey, token); } /** * 清除Token */ clearToken(): void { localStorage.removeItem(this.tokenKey); } /** * 用户登录 */ login(params: LoginParams): Observable<ApiResponse<{ token: string }>> { return this.http.post<ApiResponse<{ token: string }>>( `${this.baseUrl}/login`, params, { headers: this.getHeaders() } ).pipe( tap(res => { // 登录成功,保存Token if (res.code === 200 && res.data?.token) { this.setToken(res.data.token); } }), catchError(this.handleError.bind(this)) ); } /** * 查询数据列表 */ getDataList(params: ListQueryParams): Observable<ApiResponse<{ list: any[], total: number }>> { return this.http.get<ApiResponse<{ list: any[], total: number }>>( `${this.baseUrl}/data-list`, { headers: this.getHeaders(), params: params // 自动拼接为URL参数 } ).pipe( catchError(this.handleError.bind(this)) ); } }核心说明:
- 封装了通用的请求头处理逻辑,自动为请求添加 Token(鉴权必备);
- 统一的错误处理,针对 401 状态码(Token 失效)做了自动登出处理;
- 使用 TypeScript 接口定义参数和返回值类型,提升代码可读性和类型安全;
- 通过
tap操作符处理登录成功后的 Token 存储,catchError捕获请求异常。
三、实现登录组件
3.1 创建登录组件
ng generate component components/login3.2 登录组件模板(login.component.html)
<!-- src/app/components/login/login.component.html --> <div class="login-container"> <h2>用户登录</h2> <form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)"> <div class="form-item"> <label>用户名:</label> <input type="text" name="username" ngModel required placeholder="请输入用户名" #username="ngModel"> <div *ngIf="username.invalid && username.touched" class="error"> 用户名不能为空 </div> </div> <div class="form-item"> <label>密码:</label> <input type="password" name="password" ngModel required placeholder="请输入密码" #password="ngModel"> <div *ngIf="password.invalid && password.touched" class="error"> 密码不能为空 </div> </div> <button type="submit" [disabled]="loginForm.invalid || isLoading"> <span *ngIf="!isLoading">登录</span> <span *ngIf="isLoading">登录中...</span> </button> <div *ngIf="errorMsg" class="error">{{ errorMsg }}</div> </form> </div> <style scoped> .login-container { width: 400px; margin: 100px auto; padding: 20px; border: 1px solid #eee; border-radius: 8px; } .form-item { margin-bottom: 15px; } .form-item label { display: inline-block; width: 80px; } .form-item input { width: 280px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } button { width: 100%; padding: 10px; background: #1677ff; color: white; border: none; border-radius: 4px; cursor: pointer; } button:disabled { background: #8cc5ff; cursor: not-allowed; } .error { color: #f5222d; font-size: 12px; margin-top: 5px; } </style>3.3 登录组件逻辑(login.component.ts)
// src/app/components/login/login.component.ts import { Component } from '@angular/core'; import { NgForm } from '@angular/forms'; import { Router } from '@angular/router'; import { ApiService, LoginParams } from '../../services/api.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent { isLoading = false; // 登录加载状态 errorMsg = ''; // 错误提示 constructor( private apiService: ApiService, private router: Router ) { } /** * 提交登录表单 */ onSubmit(form: NgForm): void { // 表单验证不通过,直接返回 if (form.invalid) { return; } this.isLoading = true; this.errorMsg = ''; const loginParams: LoginParams = form.value; // 调用登录接口 this.apiService.login(loginParams).subscribe({ next: (res) => { this.isLoading = false; if (res.code === 200) { // 登录成功,跳转到数据列表页 this.router.navigate(['/data-list']); } else { this.errorMsg = res.msg || '登录失败,请检查账号密码'; } }, error: (err) => { this.isLoading = false; this.errorMsg = err.message || '网络异常,登录失败'; } }); } }四、实现数据列表查询组件
4.1 创建列表组件
ng generate component components/data-list4.2 列表组件模板(data-list.component.html)
<!-- src/app/components/data-list/data-list.component.html --> <div class="data-list-container"> <div class="header"> <h2>数据列表</h2> <button (click)="apiService.clearToken(); router.navigate(['/login'])">退出登录</button> </div> <!-- 查询条件 --> <div class="search-form"> <input type="text" [(ngModel)]="keyword" placeholder="请输入关键词查询" (keyup.enter)="getList()"> <button (click)="getList()">查询</button> <button (click)="resetSearch()">重置</button> </div> <!-- 加载状态 --> <div *ngIf="isLoading" class="loading">加载中...</div> <!-- 错误提示 --> <div *ngIf="errorMsg" class="error">{{ errorMsg }}</div> <!-- 数据列表 --> <table *ngIf="!isLoading && !errorMsg" class="data-table"> <thead> <tr> <th>ID</th> <th>名称</th> <th>描述</th> <th>创建时间</th> </tr> </thead> <tbody> <tr *ngFor="let item of list"> <td>{{ item.id }}</td> <td>{{ item.name }}</td> <td>{{ item.desc }}</td> <td>{{ item.createTime }}</td> </tr> <tr *ngIf="list.length === 0"> <td colspan="4" class="empty">暂无数据</td> </tr> </tbody> </table> <!-- 分页 --> <div class="pagination" *ngIf="!isLoading && !errorMsg"> <button (click)="changePage(pageNum - 1)" [disabled]="pageNum === 1"> 上一页 </button> <span>第 {{ pageNum }} 页 / 共 {{ totalPages }} 页</span> <button (click)="changePage(pageNum + 1)" [disabled]="pageNum >= totalPages"> 下一页 </button> </div> </div> <style scoped> .data-list-container { width: 800px; margin: 20px auto; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .search-form { margin-bottom: 20px; } .search-form input { padding: 8px; width: 300px; margin-right: 10px; } .loading { text-align: center; padding: 20px; color: #666; } .error { color: #f5222d; padding: 10px; background: #fff1f0; border-radius: 4px; margin-bottom: 20px; } .data-table { width: 100%; border-collapse: collapse; } .data-table th, .data-table td { border: 1px solid #eee; padding: 10px; text-align: left; } .data-table th { background: #f5f5f5; } .empty { text-align: center; color: #999; } .pagination { margin-top: 20px; display: flex; align-items: center; gap: 10px; } .pagination button { padding: 5px 10px; cursor: pointer; } .pagination button:disabled { cursor: not-allowed; opacity: 0.5; } </style>4.3 列表组件逻辑(data-list.component.ts)
// src/app/components/data-list/data-list.component.ts import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { ApiService, ListQueryParams } from '../../services/api.service'; @Component({ selector: 'app-data-list', templateUrl: './data-list.component.html', styleUrls: ['./data-list.component.css'] }) export class DataListComponent implements OnInit { // 查询参数 pageNum = 1; // 当前页码 pageSize = 10; // 每页条数 keyword = ''; // 关键词 // 数据展示 list: any[] = []; // 列表数据 total = 0; // 总条数 totalPages = 0; // 总页数 // 状态控制 isLoading = false; errorMsg = ''; constructor( public apiService: ApiService, // 公开以便模板使用 private router: Router ) { } ngOnInit(): void { // 组件初始化时加载列表 this.getList(); } /** * 获取数据列表 */ getList(): void { this.isLoading = true; this.errorMsg = ''; const params: ListQueryParams = { pageNum: this.pageNum, pageSize: this.pageSize, keyword: this.keyword.trim() || undefined }; this.apiService.getDataList(params).subscribe({ next: (res) => { this.isLoading = false; if (res.code === 200) { this.list = res.data.list; this.total = res.data.total; // 计算总页数 this.totalPages = Math.ceil(this.total / this.pageSize); } else { this.errorMsg = res.msg || '获取列表失败'; } }, error: (err) => { this.isLoading = false; this.errorMsg = err.message || '网络异常,获取列表失败'; } }); } /** * 切换页码 */ changePage(num: number): void { if (num < 1 || num > this.totalPages) { return; } this.pageNum = num; this.getList(); } /** * 重置查询条件 */ resetSearch(): void { this.keyword = ''; this.pageNum = 1; this.getList(); } }五、配置路由
为了实现登录页和列表页的跳转,需要配置 Angular 路由:
// app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LoginComponent } from './components/login/login.component'; import { DataListComponent } from './components/data-list/data-list.component'; // 简单的路由守卫:未登录禁止访问列表页 const authGuard = () => { const token = localStorage.getItem('user_token'); if (token) { return true; } else { return { redirectTo: '/login' }; } }; const routes: Routes = [ { path: '', redirectTo: '/login', pathMatch: 'full' }, { path: 'login', component: LoginComponent }, { path: 'data-list', component: DataListComponent, canActivate: [authGuard] } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }别忘了在app.component.html中添加路由出口:
<!-- app.component.html --> <router-outlet></router-outlet>六、模拟后端接口(可选)
为了让代码能直接运行,你可以使用json-server快速搭建模拟接口:
- 安装 json-server:
npm install -g json-server- 创建
db.json文件:
{ "login": { "code": 200, "msg": "登录成功", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzM1NjM4NDAwfQ.8Z7s9X8s7d6f5g4h3j2k1l0poiuytrewq" } }, "data-list": { "code": 200, "msg": "查询成功", "data": { "list": [ { "id": 1, "name": "测试数据1", "desc": "这是第一条测试数据", "createTime": "2026-01-01" }, { "id": 2, "name": "测试数据2", "desc": "这是第二条测试数据", "createTime": "2026-01-02" } ], "total": 2 } } }- 启动模拟服务:
json-server --watch db.json --port 3000 --routes routes.json(routes.json用于映射接口路径,可根据需要配置)
七、功能测试与注意事项
- 登录测试:输入用户名密码,点击登录,成功后跳转列表页,LocalStorage 中可看到 Token;
- 列表查询:输入关键词查询、切换页码,验证数据展示是否正常;
- Token 失效:手动修改 LocalStorage 中的 Token,刷新列表页,会自动跳转到登录页;
- 异常处理:关闭模拟服务,触发网络异常,验证错误提示是否正常显示。
总结
本文通过 Angular 的 HttpClient 实现了完整的登录与数据列表查询功能,核心要点包括:
- 模块化封装:将 HTTP 请求封装为独立服务,统一处理请求头、Token 和错误,提升代码复用性;
- 鉴权机制:登录成功后存储 Token,后续请求自动携带 Token,Token 失效时自动登出;
- 用户体验:添加加载状态、错误提示、表单验证,提升交互体验;
- 路由守卫:简单的路由守卫确保未登录用户无法访问受保护的列表页。
这套实现方案符合 Angular 最佳实践,可直接应用于实际项目,也可在此基础上扩展更多功能(如请求拦截器、响应拦截器、更多表单验证规则等)。