ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Angular GET请求实战:参数传递、响应处理与错误捕获完全指南

Angular GET请求实战:参数传递、响应处理与错误捕获完全指南 Angular后端联动系列写到第二篇了。上一篇我们聊了项目初始化和环境搭建今天专门把GET请求掰开揉碎讲清楚。为什么单拎GET出来因为我发现很多人在Angular里做后端交互时GET请求看着简单但真正落地时会遇到一堆零碎问题参数拼了半天后端不认、响应格式对不上、接口报错页面直接白屏。这篇就把参数传递、响应处理、错误捕获这三块彻底讲透配合完整可跑的代码示例新手照着做就能通老手也可以查漏补缺。1. 准备工作与整体思路拆解1.1 为什么需要单独深入GET请求先说个现实情况。Angular里发GET请求最直观的写法就是http.get(url)但实际项目里几乎不可能这么简单。你需要拼查询参数、处理分页、传递排序字段、应对后端不同的数据封装格式还得考虑loading状态、错误提示、超时重试。这些需求叠加起来简单的http.get就远远不够了。还有一个容易忽略的问题GET请求的语义是“获取数据”它在整个应用里往往是高频操作。列表页、详情页、下拉选项、用户信息全都靠GET撑着。如果GET这层地基没打好后续无论是加拦截器还是做统一错误处理都会非常痛苦。所以我建议把GET请求单独抽象成一套规范无论是直接写在组件里还是封装成Service都要有统一的处理模式。下面我们默认使用Angular 16版本RxJS 7TypeScript 4.9。不同版本的API差异不大但这些基础版本往上都支持本文的写法。如果你还在用Angular 12以下的旧版本建议先升级很多响应处理和错误捕获的最佳实践在旧版本上表现不一样。1.2 环境准备与HttpClientModule引入用Angular CLI创建项目后第一件事就是在AppModule或独立组件里引入HttpClientModule。这里我遇到过一个很典型的问题很多人刚创建项目就急着写请求结果控制台报NullInjectorError: No provider for HttpClient原因就是忘了注册模块。// app.module.ts import { NgModule } from angular/core; import { BrowserModule } from angular/platform-browser; import { HttpClientModule } from angular/common/http; NgModule({ imports: [ BrowserModule, HttpClientModule // 必须引入 ] }) export class AppModule { }如果你用的是Angular 15的独立组件模式就在app.config.ts里加上provideHttpClient()// app.config.ts import { ApplicationConfig } from angular/core; import { provideHttpClient } from angular/common/http; export const appConfig: ApplicationConfig { providers: [provideHttpClient()] };注意这里的provideHttpClient()是Angular 15引入的新方式配合独立组件使用会清爽很多。如果你的项目还在用NgModule模式继续用HttpClientModule也没问题两者不要混用。注册好之后在组件或服务里注入HttpClient就可以开始请求了。但我不建议直接在组件里散落大量HTTP调用最好先建一个Service层做统一管理后面讲实操示例的时候会给完整代码。2. GET请求参数传递从基础到进阶2.1 URL路径参数与Query参数的区别刚接触HTTP的时候很容易把“路径参数”和“查询参数”搞混。其实很好区分路径参数是URL路径的一部分比如/api/user/42其中42是用户ID。RESTful风格里常用来定位资源。查询参数是?后面的键值对比如/api/user?page1size20常用来做筛选、分页、排序。Angular里处理这两种参数的姿势完全不一样。路径参数通常用字符串拼接或者模板字符串const userId 42; this.http.get(/api/user/${userId});查询参数则推荐用HttpParams对象来构建而不是手动拼字符串。为什么因为HttpParams会自动帮你处理编码问题。比如参数值里包含中文、空格、特殊符号手动拼接很容易出问题而HttpParams会统一编码后端收到的就是规范格式。2.2 HttpParams的不可变性与构建技巧HttpParams有一个很重要的特性不可变。这意味着你每调用一次set或append返回的都是一个新的HttpParams实例原对象不受影响。我见过有人写这样的代码// 错误示范 let params new HttpParams(); params.set(page, 1); // 返回值被丢弃了 params.set(size, 20); // 同样没保存 this.http.get(/api/user, { params });这个请求发出去后端收到的params是空的。正确做法是链式调用或者重新赋值// 正确方式1链式调用 const params new HttpParams() .set(page, 1) .set(size, 20) .set(keyword, 张三); // 正确方式2重新赋值 let params new HttpParams(); params params.set(page, 1); params params.append(tag, 前端); params params.append(tag, Angular); // append允许重复键这里多说一句set和append的区别。set是“设置”如果键已存在就替换旧值append是“追加”允许同一个键有多个值。比如筛选标签时一个文章可能有多个tag用append就对了后端拿到的query string是这样的?tag前端tagAngular。有时候参数是动态构建的比如从表单里收集条件再传给后端我习惯写一个专门的方法来处理buildParams(filter: UserFilter): HttpParams { let params new HttpParams(); Object.entries(filter).forEach(([key, value]) { if (value ! null value ! undefined value ! ) { params params.set(key, String(value)); } }); return params; }这个方法会跳过空值保证URL干净同时避免把undefined拼进去。2.3 参数传递的常见坑第一个坑是数组参数。Angular的HttpParams默认把数组转成keyvalue1keyvalue2这种格式。但如果后端期望的是keyvalue1,value2逗号分隔你就需要手动处理const tags [前端, Angular]; const params new HttpParams().set(tags, tags.join(,));第二个坑是日期格式。直接set(startDate, date)会把Date对象转成浏览器默认格式后端解析容易乱。建议统一格式化成字符串比如YYYY-MM-DD HH:mm:ss这个可以在工具函数里统一处理。第三个坑跟HttpClient的params选项相关HttpParams会被自动编码但你传字符串时不会。也就是说你可以直接写params: { page: 1 }Angular会内部转成HttpParams。但如果值是数字记得转成字符串。我在项目中见过不少Cannot read properties of undefined的报错最后查下来就是参数类型不对导致请求URL异常。3. 响应处理从JSON到类型安全3.1 泛型与类型映射GET请求发出去返回的是一个Observable默认情况下Angular会尝试把响应体解析成JSON。但解析出来的结果默认是Object类型你用起来没有类型提示还容易在编译期漏掉错误。正确做法是使用泛型interface User { id: number; name: string; email: string; } this.http.getUser(/api/user/42) .subscribe(user { console.log(user.name); // 有类型提示了 });但实际项目中很多后端不会直接返回数据本身而是包一层统一响应结构。比如这样的格式{ code: 0, message: success, data: { id: 42, name: 张三 } }这种情况我建议定义泛型接口来处理interface ApiResponseT { code: number; message: string; data: T; } this.http.getApiResponseUser(/api/user/42) .subscribe(res { if (res.code 0) { console.log(res.data.name); } else { // 业务错误 } });把ApiResponseT定义成一个可复用泛型项目里所有接口都可以统一使用代码整洁度会提升很多。3.2 完整响应对象与响应头处理有些场景你不仅需要响应体还需要响应头、状态码、Cookies等信息。Angular的get方法提供了observe选项this.http.get(/api/user/42, { observe: response }) .subscribe(response { console.log(response.status); // 200 console.log(response.headers); // HttpHeaders对象 console.log(response.body); // 响应体 });这个在文件下载场景特别有用。比如后端在响应头里返回文件名你需要从中取出来this.http.get(/api/file/download, { observe: response, responseType: blob }) .subscribe(response { const contentDisposition response.headers.get(Content-Disposition); // 解析文件名 const filename contentDisposition?.split(filename)[1] ?? download.txt; // 触发浏览器下载 const url window.URL.createObjectURL(response.body as Blob); const a document.createElement(a); a.href url; a.download filename; a.click(); window.URL.revokeObjectURL(url); });这里有个细节默认情况下responseType是json下载文件必须显式设置成blob。另外读取响应头时要注意跨域环境下某些自定义响应头是读不到的除非后端在CORS响应头里显式暴露Access-Control-Expose-Headers。3.3 响应转换与流式处理如果响应体是嵌套结构或者你需要把字段名转换成前端方便使用的驼峰格式可以在请求管道里加map操作符import { map } from rxjs/operators; this.http.getApiResponseUser(/api/user/42) .pipe( map(res res.data), map(user ({ ...user, displayName: ${user.name} (${user.id}) })) ) .subscribe(user { console.log(user.displayName); });再提一个进阶用法如果你想在GET请求返回之前做数据防抖或联级请求RxJS的switchMap就非常顺手。比如先获取用户ID再请求用户详情this.http.getnumber(/api/current-user-id) .pipe( switchMap(id this.http.getUser(/api/user/${id})) ) .subscribe(user { console.log(user); });这种写法本质上是把两个请求串起来比在subscribe回调里嵌套发起第二个请求要清晰得多而且取消第一个请求时第二个也不会发出去。4. 错误捕获网络异常与业务错误双管齐下4.1 HttpErrorResponse结构解析Angular的HTTP错误统一封装成HttpErrorResponse对象这里要区分两类错误一类是HTTP状态码非2xx比如404、500、502另一类是网络层错误比如断网、跨域被拦截、请求超时。区分方式有技巧看error.status是否存在import { HttpErrorResponse } from angular/common/http; this.http.getUser(/api/user/42) .subscribe({ next: user console.log(user), error: (err: HttpErrorResponse) { if (err.status 0) { // 网络错误或CORS拦截或请求被取消 console.log(网络异常请检查连接); } else { // HTTP状态码错误 console.log(后端返回错误${err.status} ${err.message}); } } });为什么网络错误的status是0这是惯例表示无法与服务器建立有效连接或请求未完成。常见原因包括后端服务挂了、客户端断网、请求被CORS策略拦截、HTTPS证书不匹配等。关于错误对象里的message我遇到过很多次误导性的情况——Angular会把完整的请求URL塞进去导致用户看到一大长串信息。所以我更推荐友好提示用户时只显示自己定义的错误信息不要直接引用err.message。4.2 在管道中用catchError统一捕获在subscribe里写错误处理每写一个请求都要重复一遍。更优雅的方式是配合管道操作符把错误转换成一种可控的类型流import { catchError, of } from rxjs; interface SafeResultT { data?: T; error?: string; } this.http.getUser(/api/user/42) .pipe( catchError((err: HttpErrorResponse) { // 这里可以做日志上报 console.error(GET /api/user/42 failed:, err); return of({ error: 加载用户失败 }); }) ) .subscribe(res { if (error in res) { // 显示错误提示 } else { // 正常渲染数据 } });这样把错误消化在管道里组件侧的subscribe就不需要每次处理error回调了。不过某些场景下你可能希望上游错误继续往外抛给全局拦截器可以用throwError重新抛出catchError((err: HttpErrorResponse) { if (err.status 401) { // 特殊处理但不终止流 return throwError(() err); } return of({ error: 普通请求失败 }); })4.3 请求重试与超时控制GET请求适合重试因为它是幂等的重复执行不会产生副作用。但重试也要讲究策略不能无脑重试。RxJS提供了retry操作符this.http.getUser(/api/user/42) .pipe( retry(2), // 失败后重试2次最多总计3次 catchError(err of({ error: 多次尝试后仍然失败 })) ) .subscribe();retry(2)是瞬时重试如果后端正在重启恢复这个节奏可能太快。想控制重试延迟可以用retryWhen或RxJS 7的retry配置项import { retry, timer } from rxjs; import { mergeMap } from rxjs/operators; this.http.getUser(/api/user/42) .pipe( retry({ count: 3, delay: (error, retryCount) timer(retryCount * 1000), // 1s, 2s, 3s递增 }) ) .subscribe();这里delay返回一个Observabletimer(retryCount * 1000)表示每次重试前等待的毫秒数。我第一次写这种逻辑时用的是retryWhen后来发现新版RxJS的retry配置式写法更容易读也更灵活。超时控制同样重要。后端接口如果迟迟不返回用户会以为页面卡死了。RxJS的timeout操作符可以直接设置超时时间超过就抛错误import { timeout, catchError } from rxjs/operators; this.http.getApiResponseUser(/api/user/42) .pipe( timeout(5000), // 5秒超时 catchError(err { if (err.name TimeoutError) { return of({ error: 请求超时请稍后重试 }); } return of({ error: 请求失败 }); }) ) .subscribe();注意这里的TimeoutError是RxJS自己抛的错误不是HttpErrorResponse所以判断方式不一样。4.4 全局拦截器统一处理错误项目大了之后每个请求都手动处理401、403、500会很累也容易漏。这时候就要上HttpInterceptor了。拦截器可以在请求发出去之前和响应返回之后统一做处理import { Injectable } from angular/core; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from angular/common/http; import { Observable, throwError } from rxjs; import { catchError } from rxjs/operators; Injectable() export class ErrorInterceptor implements HttpInterceptor { intercept(req: HttpRequestany, next: HttpHandler): ObservableHttpEventany { return next.handle(req).pipe( catchError((err: HttpErrorResponse) { switch (err.status) { case 401: // 跳转登录页或刷新token break; case 403: console.error(没有权限访问该资源); break; case 500: console.error(服务器内部错误); break; case 502: console.error(网关错误可能后端服务挂了); break; } return throwError(() err); }) ); } }注册拦截器也不复杂NgModule模式下在APP_INITIALIZER或模块providers里声明{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }独立组件模式下则在app.config.tsexport const appConfig: ApplicationConfig { providers: [ provideHttpClient( withInterceptors([errorInterceptor]) // 函数式拦截器 ) ] };值得说明的是Angular 15开始推荐函数式拦截器写法比类拦截器更简洁。但如果你维护的是老项目类拦截器依然完全可用。拦截器里可以做统一错误提示、token注入、日志上报是后端联动的核心枢纽后面的系列文章我会单独展开讲token注入和刷新机制。5. 完整实操示例用户列表查询理论知识讲了不少现在我把整套东西串起来做一个带搜索筛选、分页、loading状态、错误处理的用户列表查询。这个例子覆盖了前面讲的所有核心点。5.1 Service层代码实现先写Service层统一管理API地址和参数构建import { Injectable } from angular/core; import { HttpClient, HttpParams } from angular/common/http; import { Observable } from rxjs; import { map, catchError, timeout } from rxjs/operators; export interface User { id: number; name: string; email: string; role: string; } export interface PagedResultT { total: number; items: T[]; } export interface UserQuery { page: number; size: number; keyword?: string; role?: string; } Injectable({ providedIn: root }) export class UserService { private apiBase /api; constructor(private http: HttpClient) {} getUsers(query: UserQuery): ObservablePagedResultUser { let params new HttpParams() .set(page, String(query.page)) .set(size, String(query.size)); if (query.keyword) { params params.set(keyword, query.keyword); } if (query.role) { params params.set(role, query.role); } return this.http.getApiResponsePagedResultUser(${this.apiBase}/users, { params }) .pipe( timeout(8000), map(res res.data), catchError(err { console.error(获取用户列表失败, err); throw err; // 抛给调用方处理或全局拦截器 }) ); } }注意两点第一这里把page和size转换成字符串是因为HttpParams的set方法接收的值类型是字符串第二如果ApiResponse是全局通用接口我会把它放到shared/models目录下而不是在每个Service里重复定义。5.2 组件层实现与取消订阅组件里调用Service处理loading状态和错误提示import { Component, OnDestroy, OnInit } from angular/core; import { Subject } from rxjs; import { takeUntil } from rxjs/operators; import { UserService, User } from ./user.service; Component({ selector: app-user-list, templateUrl: ./user-list.component.html }) export class UserListComponent implements OnInit, OnDestroy { users: User[] []; total 0; page 1; size 10; keyword ; loading false; errorMsg ; private destroy$ new Subjectvoid(); constructor(private userService: UserService) {} ngOnInit() { this.loadUsers(); } loadUsers() { this.loading true; this.errorMsg ; this.userService.getUsers({ page: this.page, size: this.size, keyword: this.keyword }) .pipe(takeUntil(this.destroy$)) .subscribe({ next: res { this.users res.items; this.total res.total; }, error: err { this.loading false; this.errorMsg 加载用户列表失败请稍后重试; } }); } onSearch(keyword: string) { this.keyword keyword; this.page 1; this.loadUsers(); } onPageChange(page: number) { this.page page; this.loadUsers(); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } }takeUntil(this.destroy$)是用来防止组件销毁后订阅还在执行避免内存泄漏。这个习惯从Angular 2时代一直延续到现在我每次都提醒自己写上已经是肌肉记忆了。5.3 模板配合loading与错误状态模板这边可以结合Angular的控制流语法div classfilter-bar input (keyup.enter)onSearch(input.value) placeholder搜索用户 #input / button (click)onSearch(input.value)查询/button /div if (loading) { div classloading加载中.../div } else if (errorMsg) { div classerror{{ errorMsg }}/div } else { table thead trthID/thth姓名/thth邮箱/thth角色/th/tr /thead tbody for (user of users; track user.id) { tr td{{ user.id }}/td td{{ user.name }}/td td{{ user.email }}/td td{{ user.role }}/td /tr } empty { trtd colspan4暂无数据/td/tr } /tbody /table } div classpagination button (click)onPageChange(page - 1) [disabled]page 1上一页/button span第 {{ page }} 页 / 共 {{ total | number }} 条/span button (click)onPageChange(page 1) [disabled]page * size total下一页/button /div这里用了Angular 17的if、for新控制流语法比*ngIf、*ngFor更符合直觉性能也更好。如果你还停留在旧语法也完全不影响其他逻辑换回去就行。6. 常见问题速查与排查技巧实录6.1 高频错误场景汇总我把这些年做Angular开发踩过、带人时看到的高频GET请求问题整理成一张速查表可以贴在电脑边随时看现象可能原因排查方向与解决方式请求发出但后端收不到参数使用了params.set()但没接收返回值改成链式调用或重新赋值后端返回中文乱码URL编码不一致用HttpParams自动编码不要手拼URLstatus 0请求失败网络断开、CORS被拦截、请求被取消打开Network标签看具体失败原因检查后端CORS配置接口报404URL路径错误或后端路由未匹配对比接口文档检查apiBase拼接是否正确接口报502 Bad Gateway网关/代理层异常后端服务可能挂了先确认后端是不是崩了再用curl等工具直连测试接口响应体拿到了但字段全部是undefined泛型与真实结构不匹配打印原始响应JSON重新定义接口类型组件销毁后依然收到数据订阅未取消使用takeUntil或AsyncPipe请求一直在pending直到超时后端处理慢或网络问题加timeout操作符设置合理超时时间下载文件时文件名乱码响应头编码解析问题尝试decodeURIComponent处理filename*字段自定义响应头读不到CORS未暴露该头后端在响应头加Access-Control-Expose-Headers这表我每次做技术分享都会拿出来当提纲特别适合新人在排查问题时对照。6.2 调试工具与经验心得最后分享几个调试GET请求时的实用技巧。先说工具。Angular应用调试HTTP我一直用Chrome DevTools的Network面板但有个细节很多人容易忽略——在Fetch/XHR筛选标签下可以按请求状态码直接筛选比如只查看502的记录排查后端异常时效率提升不少。如果遇到跨域CORS问题Console面板会把被拦截的请求和原因一并打出来仔细读报错里的Access-Control-Allow-Origin相关提示能快速定位是后端没配还是前端多带了请求头。再说代码调试。如果你在组件里订阅后没有反应我建议先把observe: response加上在subscribe回调里打印完整response看看status、headers、body哪个环节不正常。这样能快速判断问题出在网络层、服务端还是代码解析逻辑。关于错误捕获我个人的习惯是分三层来应对。第一层Service层做接口级错误转换把业务错误码转成友好提示第二层拦截器做全局兜底处理401、403、500这类通用状态第三层组件里只处理与当前页面强相关的错误场景比如列表加载失败显示重试按钮。这个分层刚开始搭建时麻烦一点但后面加新接口时越用越顺手。另外建议给每个GET请求设置超时时间。我踩过很深的坑是后端某个接口偶尔卡住不返回前端用户等了几分钟直接关页面。加上timeout(8000)后虽然偶尔请求会失败但至少用户能立刻看到报错而不是干等。失败之后配合重试策略通常能覆盖后端短暂抖动的情况。这个系列下一期我计划写POST和PUT请求的表单提交与数据更新期间会讲到HttpClient处理非JSON数据、带文件上传、拦截器统一加认证头这些内容如果你在联调中遇到过相关问题可以提前把报错信息整理好到时候对照排查。今天这篇GET请求的内容从参数传递到响应处理再到错误捕获覆盖了前后端联调中最常见的坑。实际操作中如果还有别的问题欢迎在评论区贴出你的报错信息我看到会尽量回复。
返回列表