news 2026/4/19 18:11:02

Angular后端联动06,Angular 实战:基于 HttpClient 实现登录与数据列表查询

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Angular后端联动06,Angular 实战:基于 HttpClient 实现登录与数据列表查询

在 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/api

2.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)) ); } }

核心说明

  1. 封装了通用的请求头处理逻辑,自动为请求添加 Token(鉴权必备);
  2. 统一的错误处理,针对 401 状态码(Token 失效)做了自动登出处理;
  3. 使用 TypeScript 接口定义参数和返回值类型,提升代码可读性和类型安全;
  4. 通过tap操作符处理登录成功后的 Token 存储,catchError捕获请求异常。

三、实现登录组件

3.1 创建登录组件

ng generate component components/login

3.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-list

4.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快速搭建模拟接口:

  1. 安装 json-server:
npm install -g json-server
  1. 创建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 } } }
  1. 启动模拟服务:
json-server --watch db.json --port 3000 --routes routes.json

routes.json用于映射接口路径,可根据需要配置)

七、功能测试与注意事项

  1. 登录测试:输入用户名密码,点击登录,成功后跳转列表页,LocalStorage 中可看到 Token;
  2. 列表查询:输入关键词查询、切换页码,验证数据展示是否正常;
  3. Token 失效:手动修改 LocalStorage 中的 Token,刷新列表页,会自动跳转到登录页;
  4. 异常处理:关闭模拟服务,触发网络异常,验证错误提示是否正常显示。

总结

本文通过 Angular 的 HttpClient 实现了完整的登录与数据列表查询功能,核心要点包括:

  1. 模块化封装:将 HTTP 请求封装为独立服务,统一处理请求头、Token 和错误,提升代码复用性;
  2. 鉴权机制:登录成功后存储 Token,后续请求自动携带 Token,Token 失效时自动登出;
  3. 用户体验:添加加载状态、错误提示、表单验证,提升交互体验;
  4. 路由守卫:简单的路由守卫确保未登录用户无法访问受保护的列表页。

这套实现方案符合 Angular 最佳实践,可直接应用于实际项目,也可在此基础上扩展更多功能(如请求拦截器、响应拦截器、更多表单验证规则等)。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 5:55:33

揭秘Docker Compose滚动更新:如何实现服务无感升级与故障规避

第一章&#xff1a;揭秘Docker Compose滚动更新&#xff1a;实现无感升级的核心机制 在现代微服务架构中&#xff0c;应用的持续交付与零停机部署已成为基本需求。Docker Compose 通过声明式配置和容器编排能力&#xff0c;支持服务的滚动更新策略&#xff0c;确保系统在升级过…

作者头像 李华
网站建设 2026/4/18 8:44:02

I2S协议差分变体(如ISPL):概念扩展硬件对比

差分I2S接口崛起&#xff1a;从ISPL到LVDS&#xff0c;如何突破音频传输的物理极限&#xff1f;你有没有遇到过这样的问题&#xff1f;在车载音响系统中&#xff0c;明明用了高端DAC芯片&#xff0c;但播放高解析音频时底噪却始终压不下去&#xff1b;或者在工业级录音设备里&a…

作者头像 李华
网站建设 2026/4/18 6:26:30

伟创SD600方案伺服EtherCAT电路图说明书代码解读

伟创SD600方案伺服EtherCAT电路图说明书代码。最近在研究伺服控制系统&#xff0c;接触到了伟创SD600方案中EtherCAT相关部分&#xff0c;感觉挺有意思&#xff0c;今天就来和大家分享一下其中电路图说明书代码的一些要点。 EtherCAT简介 在深入代码之前&#xff0c;先简单说一…

作者头像 李华
网站建设 2026/4/18 8:31:34

Dify响应异常全解析(90%开发者忽略的容错陷阱)

第一章&#xff1a;Dify响应异常全解析&#xff08;90%开发者忽略的容错陷阱&#xff09;在集成 Dify 框架进行 AI 应用开发时&#xff0c;多数开发者关注功能实现&#xff0c;却忽视了其异步响应机制中潜藏的容错陷阱。当模型推理超时、网络抖动或上下文长度溢出时&#xff0c…

作者头像 李华
网站建设 2026/4/19 13:22:59

你真的会设Dify文档路径吗?5个关键点决定数据可访问性

第一章&#xff1a;Dify文档保存路径的核心概念 Dify 是一个开源的低代码 AI 应用开发平台&#xff0c;其文档保存路径机制是系统设计中的关键组成部分。理解文档的存储结构有助于开发者高效管理应用数据、进行备份恢复以及实现自定义集成。 存储架构概述 Dify 默认采用分层目…

作者头像 李华
网站建设 2026/4/18 8:36:26

OpenBMC在ASPEED平台上的架构设计深度剖析

OpenBMC 在 ASPEED 平台上的架构设计&#xff1a;从驱动适配到服务协同的深度拆解在现代数据中心&#xff0c;服务器不再是“插电即用”的黑盒设备。它们需要被远程监控、自动诊断、动态调优——而这一切的背后&#xff0c;都离不开一个沉默却关键的角色&#xff1a;基板管理控…

作者头像 李华