当前位置: 首页 > news >正文

六、Angular 发送请求/ HttpClient 模块

一、应用 HttpClient 模块

  • @angular/common/http 中的 HttpClient 类基于浏览器提供的 XMLHttpRequest 接口。
  • 要想使用 HtpClient 模块,就要先导入 Anqular 的 HttpClientModule。大多数 Web 应用程序都会在根模块 AppModule 中导入它。
  1. 编辑 src/app/app.module.ts,导入 HttpClientModule 模块,导入顺序要在 BrowserModule 之后

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    //导入 httpClient
    import { HttpClientModule } from '@angular/common/http';import { AppComponent } from './app.component';@NgModule({declarations: [AppComponent],imports: [BrowserModule,HttpClientModule //导入 httpClient,注意要放在 BrowserModule 之后],providers: [],bootstrap: [AppComponent]
    })
    export class AppModule {}
    
  2. 通过构造函数将实例注册到类中

    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';@Injectable()
    export class DemoService {constructor(private http: HttpClient) {}
    }
    

二、模拟后端接口(创建RESTful API 服务)

[1]. 使用 json-server 创建 RESTful API 服务

  • json-server 是一个 Node.js 模块,底层运行在 Express 服务器上,用户可以指定一个JSON 文件作为 RESTful API 服务的数据源。 使用json-server 在本地搭建一个JSON 服务器对外提供 RESTful API 服务。前端开发工程师在无后端的情况下,可以用它作为后端 RESTfulAPI 服务器。
  1. 全局安装 json-server
    node 版本超过 14 直接安装即可

    npm install -g json-server
    

    我的node版本为 12.11.0,所以选择固定 json-server 版本

    npm install -g json-server@0.17.4
    
  2. 在任意位置创建 data 目录,并创建 db.json 文件,内容如下

    {"data": [{"username": "张三","age": 15}]
    }
    
  3. 在 data 目录下打开命令行窗口, 输入如下指令启动 json-server

    json-server db.json
    

    控制台将会有如下信息

  4. 在浏览器中输入 http://localhost:3000/data

[2]. 使用 Angular 内存数据库模拟服务器

  • Angular 内存数据库基于in-memory-web-api库,该库用于 Angular 演示和测试时调用内存中的网络 API,可模仿RESTful API 服务上的 CRUD 增、、改、查操作。它拦截了 Angular的 HTTP 请求和HttpClient 请求,这些请求原本会发送到远程服务器,然后将它们重定向到定义的内存数据库中。
  • in-memory-web-api 库集成在 Angular 中,该库会替换 HttpClient 模块中的 HttpBackend服务,新的服务会模拟 RESTful 风格的后端的行为。
  • in-memory-web-api 库仅拦截了 Angular 中的 HTTP 请求,它实际上没有运行 Web服务器。因此我们不能通过浏览器或者其他 Angular 环境外的工具访问它的 RESTful API 资源。
  • in-memory-web-api 库所虚拟的 API位于内存中,这也就意味着当刷新浏览器后,所有的数据都会消失。
  • 使用 Angular 内存数据库的优势显而易见: 无须单独构建和启动测试服务器
  1. 在angualr 项目中安装 in-memory-web-api 库
    我的node版本为 12.11.0,所以选择固定 angular-in-memory-web-api 版本

    npm i angular-in-memory-web-api@0.10.0 --save
    
  2. src/app 目录下新建 In-mem-hero-service.ts 文件,内容如下

    import { InMemoryDbService } from 'angular-in-memory-web-api';export class InMemHeroService implements InMemoryDbService {// 创建模拟数据createDb() {// 变量名 heroes 将被视作 URL 的一部分let heroes = [{ id: 1, name: '张三' },{ id: 2, name: '李四' },{ id: 3, name: '王五' },{ id: 4, name: '赵六' },{ id: 5, name: '孙琦' }];return { heroes };}
    }
    
  3. src/app/app.module.ts 文件中导入 InMemHeroService 类

    • HttpClientInMemoryWebApiModule 的 forRoot 方法的可以提供第二个参数
      HttpclientInMemoryWebApiModule.forRoot(InMemHeroService, { delay: 0} ) // 无延迟
      HttpclientInMemoryWebApiModule.forRoot(InMemHeroService, { delay: 500 }) //延迟500ms
      
    • 默认情况下,HttpClientlnMemoryWebApiModule 模块会拦截所有的 HttpClient 请求在实际工作中,我们可能需要同时使用 HttpClient 模块和 HttpClientinMemoryWebApiModule模块,意思是同时访问外部和内存的RESTful API资源。这时,我们可以通过配置选项passThruUnknownUrl 来实现,具体代码如下。
      HttpClientInMemoryWebApiModule.forRoot(InMemHeroService,{passThruUnknownUrl: true})
      
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';//导入 httpClient
    import { HttpClientModule } from '@angular/common/http';
    // 导入 HttpClientInMemoryWebApiModule 注册数据存储服务
    import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';import { AppRoutingModule } from './app-routing.module';
    import { AppComponent } from './app.component';// 导入自己创建的 InMemHeroService 类
    import { InMemHeroService } from './in-mem-hero-service';@NgModule({declarations: [AppComponent],imports: [BrowserModule,AppRoutingModule,HttpClientModule, //导入 HttpClientModule BrowserModule 之后HttpClientInMemoryWebApiModule.forRoot(InMemHeroService) //该模块必须在 HttpClientModule 之后],providers: [],bootstrap: [AppComponent]
    })
    export class AppModule {}
    
  4. 修改 src/app/app.components.ts 文件

    import { Component, OnInit } from '@angular/core';
    import { HttpClient } from '@angular/common/http';@Component({selector: 'app-root',template: ``,styles: []
    })
    export class AppComponent implements OnInit {constructor(private http: HttpClient) {}ngOnInit(): void {// 获取所有的数据this.http.get('api/heroes').subscribe((data) => {console.log(data);});// 获取id为1的数据this.http.get('api/heroes/1').subscribe((data) => {console.log(data); // {id:1, name: "张三"}});// 获取name以李开头的数据this.http.get('api/heroes?name=^李').subscribe((data) => {console.log(data); // [{id:2, name: "李四"}]});}
    }
    

三、从服务器获取数据

  • HttpClient 模块允许我们在调用 HTTP 请求时使用泛型,通过泛型告诉 Angular 我们期望从HTTP 请求获得的响应类型。响应的类型可以是 any 变量类型(如 string )、类或接口等。如下面的代码执行 HttpClient 模块的 GET 请求,将预期的响应类型指定为 Hero 对象的数组。
    export class Hero {constructor(public id = 1, public name = '') {}
    }this.http.get<hero[]>(this.hreoesUrl)
    
  • 指定响应类型是给 TypeScript 看的声明,并不能保证服务器会实际使用此类型的对象进行响应。服务器 API 返回的实际响应类型是由服务器来保证的。换句话说,用户可以对Hero 类中的属性随意定义。因此,服务器实际返回的对象与类的定义并没有直接关系。
  1. 新建一个项目 demo-http

    ng new demo-http -t -s --minimal
    
  2. 安装 Angular 内存数据库

    npm i angular-in-memory-web-api@0.10.0 -S
    
  3. 新建一个 hero 接口(位置:src/app/hero.ts)

    ng g interface hero
    
  4. 修改 hero 接口文件 (位置:src/app/hero.ts)

    export interface Hero {id: number;name: string;
    }
    
  5. 新建 inMemHero 服务(位置:src/app/in-mem-hero.service.ts)

    ng g s inMemHero
    
  6. 修改 inMemHero 服务文件(位置:src/app/in-mem-hero.service.ts)

    import { Injectable } from '@angular/core';
    import { InMemoryDbService, RequestInfo } from 'angular-in-memory-web-api';
    import { Observable } from 'rxjs';@Injectable()
    export class InMemHeroService implements InMemoryDbService {createDb(reqInfo?: RequestInfo): {} | Observable<{}> | Promise<{}> {// 变量名 heroes 将被视作 URL 的一部分let heroes = [{ id: 1, name: '张三' },{ id: 2, name: '李四' },{ id: 3, name: '王五' },{ id: 4, name: '赵六' },{ id: 5, name: '孙琦' }];return { heroes };}
    }
    
  7. 编辑 src/app/app.module.ts 文件

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';//导入 httpClient
    import { HttpClientModule } from '@angular/common/http';
    // 导入 HttpClientInMemoryWebApiModule 注册数据存储服务
    import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';import { AppComponent } from './app.component';// 导入自己创建的 InMemHeroService 类
    import { InMemHeroService } from './in-mem-hero.service';@NgModule({declarations: [AppComponent],imports: [BrowserModule,HttpClientModule, //导入 HttpClientModule BrowserModule 之后HttpClientInMemoryWebApiModule.forRoot(InMemHeroService) //该模块必须在 HttpClientModule 之后],providers: [],bootstrap: [AppComponent]
    })
    export class AppModule {}
    
  8. 编辑 src/app/app.component.ts 组件

    import { HttpClient } from '@angular/common/http';
    import { Component, OnInit } from '@angular/core';
    import { Observable } from 'rxjs';
    import { Hero } from './hero';@Component({selector: 'app-root',template: `<div style="text-align: center;"><p *ngFor="let hero of heroes">{{ hero.id }}-{{ hero.name }}</p></div>`,styles: []
    })
    export class AppComponent implements OnInit {private heroesUrl = 'api/heroes';heroes: Hero[];constructor(private http: HttpClient) {}ngOnInit(): void {this.getHeroes().subscribe((data) => {console.log(data);this.heroes = data;});}getHeroes(): Observable<Hero[]> {return this.http.get<Hero[]>(this.heroesUrl);}
    }
    

四、HttpClient 模块的请求头配置

[1]. 添加请求头

HttpClient 方法的最后一个参数可以指定一个可选的配置对象,通过它可以对请求头进行配置。常见的配置有需要 Content-Type 标识来显式声明 HTTP 请求正文的 MIME 类型、权限认证中的Authorization 令牌以及 HTTP 请求中的参数传递等。

import { HttpClient } from '@angular/common/http';
import { HttpHeaders } from '@angular/common/http';export class DemoService {constructor(private http: HttpClient) {}getData() {const httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})};return this.http.get('api/heroes', httpOptions);}
}

[2]. 获取完整的响应信息

有时访问服务器,需要读取它返回的一个特殊的响应头或响应状态码,因此可能需要完整的响应信息,而不是只有响应体。在 HttpClient 模块的 get0)方法中,observe 选项可用来告诉 HttpClient 模块,希望服务器返回完整的响应信息,代码如下。

import { HttpClient } from '@angular/common/http';export class DemoService {constructor(private http: HttpClient) {}getData() {return this.http.get('api/heroes', { observe: 'response' });}
}

[3]. 配置请求参数

设置单个参数

import { HttpClient, HttpParams } from '@angular/common/http';export class DemoService {constructor(private http: HttpClient) {}searchHero(key: string) {const options = { params: new HttpParams().set('name', key) };return this.http.get('api/heroes', options);}
}

设置多个参数

new HttpParams().append('id', '1').append('name', '张三')

使用 fromString 变量通过 URL 查询字符串构建请求参数

new HttpParams({ fromString: 'id=1&name=张三'});

[4]. 请求非 JSON 数据

不是所有的 API 都会返回 JSON 数据。有时候它们会从服务器读取文本文件,并把文本文件的内容记录下来,然后把这些内容使用 Observable 的形式返回给调用者。我们可以通过在HttpClient 模块提供的 get() 方法中配置 responseType 选项来指定获取响应内容的类型。

this.http.get (filename, {responseType: 'text'})
.pipe(tap( data => console.log(filename, data))
);

五、HttpClient 模块与RxJS配合

[1]. 错误处理

  • 处理单个接口的错误:调用 HttpClient() 方法,返回可观察对象,可以在可观测对象的订阅方法中添加错误处理的逻辑代码:这种方式仅针对某个组件的接口,无法处理某一类错误

    // getHeroes(): Observable<Hero[]> {
    //   const httpOptions = {
    //     headers: new HttpHeaders({
    //       'Content-Type': 'application/json'
    //     })
    //   };//   return this.http.get<Hero[]>(this.heroesUrl);
    // }this.getHeroes().subscribe((data) => {console.log(data);this.heroes = data;},(error) => {console.log(error);}
    );
    
  • 在接口处理错误:由 HttpClient 方法返回的可观察对象通过管道传给错误处理器

    import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
    import { Observable, throwError } from 'rxjs';
    import { catchError, retry } from 'rxjs/operators';
    import { Hero } from './hero';export class DemoService {private heroesUrl = 'api/heroes';constructor(private http: HttpClient) {}getHeroes(): Observable<Hero[]> {return this.http.get<Hero[]>(this.heroesUrl).pipe(catchError(this.handleError) // 错误处理);}private handleError(error: HttpErrorResponse) {if (error.error instanceof ErrorEvent) {// 代码运行错误或网络错误console.error('代码运行错误或网络错误:', error.error.message);} else {// 服务器发生错误,返回一个不成功的响应代码console.error(`错误码是:${error.status}, 错误信息:${error.error}`);}return throwError('系统发生错误,请稍后重试');}
    }
    

[2]. 重试

RxJS 提供了几个 retry 操作符,它们可以对失败的可观察对象自动重新订阅几次,其中最简单的是 retry0)操作符。对调用 HttpClient 方法返回的结果进行重新订阅会导致重新发起 HTTP 请求。

import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { Hero } from './hero';export class DemoService {private heroesUrl = 'api/heroes';constructor(private http: HttpClient) {}getHeroes(): Observable<Hero[]> {return this.http.get<Hero[]>(this.heroesUrl).pipe(retry(3), // 重试失败的请求,最多可重试3次catchError(this.handleError) // 错误处理);}private handleError(error: HttpErrorResponse) {if (error.error instanceof ErrorEvent) {// 代码运行错误或网络错误console.error('代码运行错误或网络错误:', error.error.message);} else {// 服务器发生错误,返回一个不成功的响应代码console.error(`错误码是:${error.status}, 错误信息:${error.error}`);}return throwError('系统发生错误,请稍后重试');}
}

六、把数据发送到服务器

[1]. 发送 POST 请求

handleError 可以看 第五章的错误处理

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Hero } from './hero';export class DemoService {private heroesUrl = 'api/heroes';constructor(private http: HttpClient) {}addHero(hero: Hero): Observable<Hero> {const httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})};return this.http.post<Hero>(this.heroesUrl, hero, httpOptions);}
}

[2]. 发送 DELETE 请求

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Hero } from './hero';export class DemoService {private heroesUrl = 'api/heroes';constructor(private http: HttpClient) {}deleteHero(hero: Hero | number): Observable<Hero> {const id = typeof hero === 'number' ? hero : hero.id;const url = this.heroesUrl + '/' + id;return this.http.delete<Hero>(url);}
}

[3]. 发送 PUT 请求

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Hero } from './hero';export class DemoService {private heroesUrl = 'api/heroes';constructor(private http: HttpClient) {}updateHero(hero: Hero): Observable<Hero> {return this.http.put<Hero>(this.heroesUrl, hero);}
}

[4]. 用例

  1. 新建一个项目

    ng new demo-http -s -t --minimal
    
  2. 安装 Angular 内存数据库模拟服务器

    npm i angular-in-memory-web-api@0.10.0 --save
    
  3. 使用命令新建一个 hero 接口文件(位置src/app/hero.ts)

    ng g interface hero
    
  4. 修改 hero 接口文件 (位置src/app/hero.ts)

    export interface Hero {id: number;name: string;
    }
    
  5. 使用命令新建服务文件,用作请求的数据 (位置src/app/in-mem-hero.service.ts)

    ng g s inMemHero
    
  6. 修改服务文件 (位置src/app/in-mem-hero.service.ts)

    import { Injectable } from '@angular/core';
    import { InMemoryDbService, RequestInfo } from 'angular-in-memory-web-api';
    import { Observable } from 'rxjs';@Injectable()
    export class InMemHeroService implements InMemoryDbService {createDb(reqInfo?: RequestInfo): {} | Observable<{}> | Promise<{}> {// 变量名 heroes 将被视作 URL 的一部分let heroes = [{ id: 1, name: '张三' },{ id: 2, name: '李四' },{ id: 3, name: '王五' },{ id: 4, name: '赵六' },{ id: 5, name: '孙琦' }];return { heroes };}
    }
    
  7. 编辑 src/app/app.modulee.ts 模块文件

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';import { AppComponent } from './app.component';
    import { ReactiveFormsModule } from '@angular/forms';
    import { HttpClientModule } from '@angular/common/http';
    import { InMemHeroService } from './in-mem-hero.service';
    import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';@NgModule({declarations: [AppComponent],imports: [BrowserModule,ReactiveFormsModule,HttpClientModule, // 须在 BrowserModule 后面HttpClientInMemoryWebApiModule.forRoot(InMemHeroService) // 须在 HttpClientModule 后面],providers: [],bootstrap: [AppComponent]
    })
    export class AppModule {}
    
  8. 使用命令新建服务,用于发送 http 请求(位置:src/app/hero.service.ts)

    ng g s hero
    
  9. 修改src/app/hero.service.ts 文件

    import { HttpClient, HttpHeaders } from '@angular/common/http';
    import { Injectable } from '@angular/core';
    import { Observable, of } from 'rxjs';
    import { tap, catchError } from 'rxjs/operators';
    import { Hero } from './hero';@Injectable({providedIn: 'root'
    })
    export class HeroService {// 内存数据库的 REST API 地址private herosUrl = 'api/heroes';// 请求头httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})};constructor(private http: HttpClient) {}// 用作处理请求产生的错误private handleError<T>(operation = 'operation', result?: T) {return (error: any): Observable<T> => {console.log(`${operation} 失败:${error.message}`);return of(result as T); // 返回可观察对象};}getHeroes(): Observable<Hero[]> {return this.http.get<Hero[]>(this.herosUrl).pipe(tap((_) => console.log('获取所有数据')),catchError(this.handleError<any>('getHeroes')));}addHero(hero: Hero): Observable<Hero> {return this.http.post<Hero>(this.herosUrl, hero, this.httpOptions).pipe(tap((newHero: Hero) => console.log(`添加的 hero 的id=${newHero.id}`)),catchError(this.handleError<Hero>('addHero')));}deleteHero(hero: Hero | number): Observable<Hero> {const id = typeof hero === 'number' ? hero : hero.id;const url = `${this.herosUrl}/${id}`;return this.http.delete<Hero>(url, this.httpOptions).pipe(tap((_) => console.log(`删除的 hero 的id=${id}`)),catchError(this.handleError<any>('deleteHero', hero)));}updateHero(hero: Hero): Observable<Hero> {hero.name = hero.name + (hero.id + 1);return this.http.put<Hero>(this.herosUrl, hero, this.httpOptions).pipe(tap((_) => console.log(`更新 hero id=${hero.id}`), catchError(this.handleError('updateHero', hero))));}
    }
    
  10. 编辑 src/app/app.component.ts 文件

    import { Component, OnInit } from '@angular/core';
    import { Hero } from './hero';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    import { HeroService } from './hero.service';@Component({selector: 'app-root',template: `<div><table><tr><th>ID</th><th>姓名</th><th>操作</th></tr><tr *ngFor="let hero of heroes"><td>{{ hero.id }}</td><td>{{ hero.name }}</td><td><button (click)="deleteHero(hero.id)">删除</button><button (click)="updateHero(hero)">更新</button></td></tr></table><br /><form [formGroup]="formGroup" (ngSubmit)="onSubmit()"><div class="block"><label>Id:</label><input type="text" formControlName="id" /></div><div class="block"><label>姓名:</label><input type="text" formControlName="name" /></div><input type="submit" value="添加" [disabled]="!formGroup.valid" /><br /><br />表单是否有效:{{ formGroup.valid }}<br />表单完整数据:{{ formGroup.valid | json }}<br /></form></div>`,styles: ['form { border: 1px solid red; } ', '.block label { display: inline-block; width: 50px; text-align: right; }']
    })
    export class AppComponent implements OnInit {heroes: Hero[];formGroup: FormGroup;constructor(private heroService: HeroService, private fb: FormBuilder) {}ngOnInit(): void {this.getHeroes();// 初始化表单this.formGroup = this.fb.group({id: this.fb.control('', Validators.required),name: this.fb.control('', Validators.required)});}getHeroes() {this.heroService.getHeroes().subscribe((data) => (this.heroes = data));}updateHero(hero: Hero) {this.heroService.updateHero(hero).subscribe((data) => {console.log('修改数据:', data);this.getHeroes();});}deleteHero(id: number) {this.heroService.deleteHero(id).subscribe((data) => {console.log('删除数据', data);this.getHeroes();});}onSubmit() {const hero = this.formGroup.value;hero.id = Number(hero.id);this.heroService.addHero(hero).subscribe((hero) => {if (hero) {this.getHeroes();} else {alert('发送错误');}this.formGroup.reset();});}
    }
  11. 页面如下

七、Angular 拦截器

Angular 中的拦截器(Httplnterceptor 接口)提供了一种拦截 HTTP 请求和 HTTP 响应的方法,可以用来监视与转换 HTTP 请求和 HTTP 响应。 拦截器使用一种常规的、标准的方法对每一次 HTTP 的请求和响应任务执行如认证、添加请求参数和记录日志等很多种隐式任务。 如果没有拦截器,那么开发者将不得不对 HttpClient 模块的每次调用显式地执行这些任务。

[1]. 创建拦截器

  • 要创建拦截器,就要创建一个实现了 Httplnterceptor 接口的类,并实现该接口中的 intercept()方法。用户可以使用如下的 Anqular CLI命令创建拦截器。
    ng generate interceptor <name>
    
  1. 新建一个拦截器 my,将生成 src/app/my.interceptor.ts 文件

    ng g interceptor my
    
  2. src/app/my.interceptor.ts 文件内容如下

    import { Injectable } from '@angular/core';
    import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
    import { Observable } from 'rxjs';@Injectable()
    export class MyInterceptor implements HttpInterceptor {constructor() {}/**** request: 请求对象实例* next: 拦截器链表中的下一个拦截器*/intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {// 输入请求信息,只有这一行是添加的,别的都是生成的console.log(JSON.stringify(request));// 继续向下走return next.handle(request);}
    }
    

[2]. 配置拦截器提供商

  • 在Angular 中配置提供商后,应用程序就可以使用提供商来配置注入器了。注入器负责提供依赖注入服务,进而 Web 应用程序就能使用依赖注入服务了。因此,在创建了拦截器后,我们还需要进一步配置拦截器提供商。

  • 由于拦截器是 HttpClient 服务的(可选)依赖,因此必须在提供 HttpClient 服务的同一个(或其各级父注入器)注入器中提供这些拦截器。我们在根模块 AppModule 中导入了HttoClientModule 模块,导致 Web 应用程序在其根注入器中提供了 HtpClient 服务,所以也要在根模块 AppModule 中提供这些拦截器。配置拦截器提供商的注册语句格式如下。

    @NgModule({providers: [{ provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true }],
    })
    
  • 在上述代码中,我们在@NgModule() 装饰器的元数据的 providers 选项里配置拦截器提供商。其中 provide 选项值HTTP_INTERCEPTORS 常量来自 @angular/common/http 包; useClass选项值是我们创建的拦截器;multi 选项值为 true,表示当前注入的是一个数组的值,而不是单一的值,multi 选项值默认为 true。如果在 Web 应用程序中仅配置一个拦截器提供商,那么程序代码也可以直接写成如下形式。

    @NgModule({providers: [MyInterceptor],
    })
    

[3]. 用例

配置日志和错误信息的拦截器

  1. 新建一个项目

    ng new demo-http4 -s -t --minimal --routing=false --style=css
    
  2. 新建一个日志拦截器文件(位置:src/app/log.interceptor.ts)

    ng g interceptor log
    
  3. 修改日志拦截器文件(位置:src/app/log.interceptor.ts)

    import { Injectable } from '@angular/core';
    import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpResponse } from '@angular/common/http';
    import { Observable } from 'rxjs';
    import { tap, finalize } from 'rxjs/operators';@Injectable()
    export class LogInterceptor implements HttpInterceptor {constructor() {}intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {const started = Date.now();let ok: string;return next.handle(request).pipe(tap(// 正常是返回 HttpResponse 类型对象(event) => {console.log('进入Log 拦截器');ok = event instanceof HttpResponse ? 'succeeded' : '';},// 错误时返回 HttpErrorResponse 类型对象(error) => (ok = 'failed')),// 当 HTTP 请求调用完成或者有错误发生时执行下面的逻辑finalize(() => {const elapsed = Date.now() - started;const msg = `${request.method} "${request.urlWithParams}" ${ok} in ${elapsed} ms.`;console.log('Log拦截器' + msg); //输入请求信息}));}
    }
    
  4. 新建一个错误拦截器文件(位置:src/app/error.interceptor.ts)

    ng g interceptor error
    
  5. 修改错误拦截器文件(位置:src/app/error.interceptor.ts)

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';@Injectable()
export class ErrorInterceptor implements HttpInterceptor {constructor() {}intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {return next.handle(request).pipe(tap((data) => console.log('进入 error 拦截器,没有发生错误', data),catchError((err) => {if (err.status === 401) {console.log('进入 error 拦截器,发生了 401 错误');}const error = err.error.message || err.statusText;return throwError(error);})));}
}
  1. 编辑 src/app/app.module.ts 文件

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';import { HTTP_INTERCEPTORS } from '@angular/common/http';
    import { HttpClientModule } from '@angular/common/http';import { AppComponent } from './app.component';
    import { LogInterceptor } from './log.interceptor';
    import { ErrorInterceptor } from './error.interceptor';@NgModule({declarations: [AppComponent],imports: [BrowserModule, HttpClientModule],providers: [ // angular 会按照顺序依次进行拦截{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },{ provide: HTTP_INTERCEPTORS, useClass: LogInterceptor, multi: true }],bootstrap: [AppComponent]
    })
    export class AppModule {}
    
  2. 新建 user 接口文件(位置:src/app/user.ts)

    ng g interface user
    
  3. 修改 user 接口文件(位置:src/app/user.ts)

    export interface User {login: string;url: string;
    }
    
  4. 新建一个 github 服务类文件(位置:src/app/github.service.ts)

    ng g s github
    
  5. 修改 github 服务类文件(位置:src/app/github.service.ts)

    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs';
    import { User } from './user';@Injectable({providedIn: 'root'
    })
    export class GithubService {// github 的接口private usersUrl = 'https://api.github.com/users?since=1';constructor(private http: HttpClient) {}getUsers(): Observable<User[]> {return this.http.get<User[]>(this.usersUrl);}
    }
    
  6. 修改 src/app/app.component.ts 文件

    import { Component, OnInit } from '@angular/core';
    import { Observable } from 'rxjs';
    import { GithubService } from './github.service';
    import { User } from './user';@Component({selector: 'app-root',template: ` <div><h3>从github 上获取 users</h3><div *ngFor="let user of users$ | async"><strong>User Name: </strong>{{ user.login }} <strong>GitHub URL: </strong>{{ user.url }}</div></div>`,styles: []
    })
    export class AppComponent implements OnInit {users$: Observable<Array<User>>;constructor(private githubService: GithubService) {}ngOnInit(): void {this.users$ = this.githubService.getUsers();}
    }
    
  7. 页面展示,控制台将输出拦截的信息,两个拦截器分别进入了两次,请求时一次,响应时一次

相关文章:

六、Angular 发送请求/ HttpClient 模块

一、应用 HttpClient 模块 angular/common/http 中的 HttpClient 类基于浏览器提供的 XMLHttpRequest 接口。要想使用 HtpClient 模块&#xff0c;就要先导入 Anqular 的 HttpClientModule。大多数 Web 应用程序都会在根模块 AppModule 中导入它。 编辑 src/app/app.module.ts…...

11_Redis数据类型-Geo地理位置

1.Geo地理位置介绍 1.1 基本概念 Redis中的GEO(Geographic)是一种专门用于处理地理位置信息的功能模块,自Redis 3.2版本引入。它特别适合用于LBS(基于位置的服务)应用,如查找附近的餐馆、用户或兴趣点等。Redis GEO的实现依赖于有序集合(sorted set),并且利用了Geoh…...

电脑每次开机卡到windows图标界面进不去

我遇到的现象是&#xff1a; 可以正常开机&#xff0c;也可以重装系统&#xff0c;主板电池换过&#xff0c;硬盘&#xff0c;内存也没问题&#xff0c;但每次开机都会卡到这个界面不动&#xff0c;也不崩溃&#xff0c;也进不去系统。最后的解决方法&#xff1a;换CPU&#x…...

如何将 DotNetFramework 项目打包成 NuGet 包并发布

如何将 DotNetFramework 项目打包成 NuGet 包并发布 在软件开发过程中&#xff0c;将项目打包成 NuGet 包并发布到 NuGet 库&#xff0c;可以让其他开发者方便地引用和使用你的项目成果。以下是将 WixWPFWizardBA 项目打包成 NuGet 包并发布的详细步骤&#xff1a; 1. 创建 .n…...

7 分布式定时任务调度框架

先简单介绍下分布式定时任务调度框架的使用场景和功能和架构&#xff0c;然后再介绍世面上常见的产品 我们在大型的复杂的系统下&#xff0c;会有大量的跑批&#xff0c;定时任务的功能&#xff0c;如果在独立的子项目中单独去处理这些任务&#xff0c;随着业务的复杂度的提高…...

鸿蒙UI开发——日历选择器

1、概 述 在项目开发中&#xff0c;我们时常会用到日历选择器&#xff0c;效果如下&#xff1a; ArkUI已经为我们提供了组件&#xff0c;我们可以直接使用&#xff0c;下面针对日历组件做简单介绍。 2、CalendarPickerDialog 接口定义如下&#xff1a; // 定义日历选择器弹…...

【python基础——异常BUG】

什么是异常(BUG) 检测到错误,py编译器无法继续执行,反而出现错误提示 如果遇到错误能继续执行,那么就捕获(try) 1.得到异常:try的执行,try内只可以捕获一个异常 2.预案执行:except后面的语句 3.传入异常:except … as uestcprint(uestc) 4.没有异常:else… 5.鉴定完毕,收尾的语…...

【论文复现】量子海洋捕食者算法用于多级图像分割问题

目录 1.摘要2.海洋捕食者算法MPA原理3.改进策略4.结果展示5.参考文献6.代码获取 1.摘要 本文提出了一种基于量子理论的改进海洋捕食者算法&#xff08;QMPA&#xff09;&#xff0c;专门用于解决多级图像分割问题。QMPA算法利用薛定谔波函数中的概率函数来确定任何时刻粒子的位…...

【python自写包模块的标准化方法】

目标: 自写一个包,提供关于字符串和文件的模块 要求对异常可以检测 str_tools.py: def str_reverse(s):""":param s: 传入的字符串:return: 反转后的字符串"""# i -1# j 0# s2 ""# while i > (-len(s)):# s2 s[i]# …...

STL——二叉搜索树

目录 二叉搜索树的概念 ⼆叉搜索树的性能分析 ⼆叉搜索树的插⼊ ⼆叉搜索树的查找 ⼆叉搜索树的删除 中序遍历结果为升序序列 二叉搜索树的概念 ⼆叉搜索树⼜称⼆叉排序树&#xff0c;它或者是⼀棵空树&#xff0c;或者是具有以下性质的⼆叉树 • 若它的左⼦树不为空&#…...

2025年XR行业展望:超越虚拟,融合现实

随着科技的飞速发展,扩展现实(XR)技术正逐渐从科幻走进日常生活。2025年,除了备受瞩目的AI百镜大战,XR行业同样充满期待,一系列创新产品和技术进步有望重塑我们对数字世界的体验。 Meta Quest 3S:VR行业的风向标 作为Meta旗下的拳头产品,Quest系列一直是VR市场的领军…...

python异常机制

异常是什么&#xff1f; 软件程序在运行过程中&#xff0c;非常可能遇到刚刚提到的这些问题&#xff0c;我们称之为异常&#xff0c;英文是Exception&#xff0c;意思是例外。遇到这些例外情况&#xff0c;或者交异常&#xff0c;我们怎么让写的程序做出合理的处理&#xff0c…...

JVM与Java体系结构

一、前言: Java语言和JVM简介: Java是目前最为广泛的软件开发平台之一。 JVM:跨语言的平台 随着Java7的正式发布&#xff0c;Java虚拟机的设计者们通过JSR-292规范基本实现在Java虚拟机平台上运行非Java语言编写的程序。 Java虚拟机根本不关心运行在其内部的程序到底是使用何…...

【Python】Python与C的区别

文章目录 语句结束符代码块表示变量声明函数定义注释格式Python的标识符数据输入input()函数数据输出print()函数 语句结束符 C 语言 C 语言中每条语句必须以分号;结束。例如&#xff0c;int a 10;、printf("Hello, World!");。分号是语句的一部分&#xff0c;用于…...

怎么抓取ios 移动app的https请求?

怎么抓取IOS应用程序里面的https&#xff1f; 这个涉及到2个问题 1.电脑怎么抓到IOS手机流量&#xff1f; 2.HTTPS怎么解密&#xff1f; 部分app可以使用代理抓包的方式&#xff0c;但是正式点的app用代理抓包是抓不到的&#xff0c;例如pin检测&#xff0c;证书双向校验等…...

中学综合素质笔记3

第一章职业理念 第三节 教师观 考情提示&#xff1a; 单选题材料分析题 学习要求&#xff1a; 理解、 识记、 运用 &#xff08;一&#xff09;教师职业角色的转变&#xff08;单选材料分析&#xff09; 从教师与学生的关系看——对学生 新课程要求教师应该是学生学习的引…...

U盘加密软件哪个好用?免安装、安全、防复制

U盘作为一种便携式存储设备&#xff0c;因其携带方便、使用灵活而广受欢迎。然而&#xff0c;U盘的易失性也使其成为数据泄露的高风险载体。为了确保U盘中数据的安全性&#xff0c;使用专业的U盘加密软件是必不可少的。 免安装 更方便 《U盘超级加密3000》这款软件下载后直接…...

C++异常

1.C语言的错误处理方式 1.1直接终止程序 利用assert和exit都是直接终止程序。 1.2返回错误码 例如C语言程序的很对接口函数都会将错误信息存储在errno中表示错误。当我们自己设计函数时&#xff0c;函数返回值和返回错误码容易混淆&#xff0c;且如果函数调用栈较深时&…...

银河麒麟v10 arm版 鲲鹏 U盘安装 +解决 安装源 设置基础软件仓库时出错

人人为我&#xff0c;我为人人&#xff0c;分享出来&#xff0c;避免他人踩坑 U盘刻录 出现问题 进入系统安装界面&#xff0c;这里可以看到在“软件”-“安装源”选项上报错了&#xff0c;提示“设置基础软件仓库时出错” 2种解决办法 1. 有网的情况&#xff08;注意自己查地址…...

[network]回顾:集线器(Hub)

集线器&#xff08;Hub&#xff09;的发明是计算机网络发展史上的一个重要里程碑。它最初的设计目的是为了解决局域网&#xff08;LAN&#xff09;中多台计算机共享网络资源的需求。 #mermaid-svg-OAmOmKYGAXoglS5z {font-family:"trebuchet ms",verdana,arial,sans-…...

【Vue.js 组件化】高效组件管理与自动化实践指南

文章目录 摘要引言组件命名规范与组织结构命名规范目录组织 依赖管理工具自动化组件文档生成构建自动引入和文档生成的组件化体系代码结构自动引入组件配置使用 Storybook 展示组件文档自动生成 代码详解QA 环节总结参考资料 摘要 在现代前端开发中&#xff0c;组件化管理是 V…...

打桩机:灾害救援中的 “应急尖兵”,稳固支撑的保障|鼎跃安全

在自然灾害或突发事故中&#xff0c;如地震、泥石流、洪涝灾害、山体滑坡等&#xff0c;地质条件的不稳定可能导致建筑物倒塌、道路损毁、堤坝决口等情况&#xff0c;严重威胁人员和财产安全。 打桩机是一种用于将桩打入地基的重型机械设备&#xff0c;其主要功能是提供支撑力&…...

java1-相对路径与绝对路径

注意注意~开始新部分啦! 开始正式分享java前,先为大家分享一下一个常用的概念---文件的相对路径与绝对路径. 开篇明义: 相对路径是指一个文件或目录相对于当前工作目录的路径。相对路径不包含根目录&#xff0c;而是从当前目录开始计算。 绝对路径是指一个文件或目录从根目录…...

工厂管理中 BOM(物料清单)

工厂管理中 BOM&#xff08;物料清单&#xff09;的一些优点&#xff1a; 1. 提高生产计划准确性 - 准确反映产品所需的物料及数量&#xff0c;为生产计划提供可靠依据&#xff0c;减少因物料估算错误导致的生产延误。 2. 优化成本控制 - 有助于精确计算产品成本&…...

allure报告修改默认语言为中文

1、项目根目录创建.py文件&#xff0c;把代码复制进去 import os from pathlib import Pathdef create_settings_js_file(directory"../pytest_mytt/reports/allures/", filenamesettings.js):# 创建或确认目录存在Path(directory).mkdir(parentsTrue, exist_okTrue…...

浅谈弱电系统RVVP和RVSP电缆的区别(

1、RVVP 1.1RVVP电缆定义&#xff1f; RVVP电缆抗干扰软电缆、屏蔽电缆、信号电缆、控制电缆&#xff08;名字很多&#xff09;&#xff0c;学名&#xff1a;铜芯-聚氯乙烯绝缘-屏蔽聚氯乙烯护套-软电缆。 1.2RVVP执行标准 主要执行标准为JB/T8734.5-2016&#xff0c;部…...

MySQL 入门大全:常用函数

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…...

RV1126+FFMPEG推流项目(1)总体框架讲解

音视频推流项目的讲解 项目介绍 本项目通过 RV1126 采集摄像头和麦克风数据&#xff0c;采用 H.264/H.265 视频编码技术和 AAC 音频编码技术进行压缩和合成复合流&#xff0c;然后推送到流媒体服务器。 项目框图 下图展示了整个项目的总体流程图&#xff0c;核心部分包括&a…...

量子计算遇上人工智能:突破算力瓶颈的关键?

引言&#xff1a;量子计算遇上人工智能——突破算力瓶颈的关键&#xff1f; 在数字化时代的浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;正以前所未有的速度改变着我们的生活&#xff0c;从语音助手到自动驾驶&#xff0c;从医学诊断到金融分析&#xff0c;无不彰显其…...

Kafka消费者如何优雅下线

一、背景 我们在Kafka消费程序中&#xff0c;可能会调用dubbo接口&#xff0c;也可能会使用线程池&#xff0c;连接池等&#xff0c;但是在服务下线的时候&#xff0c;kafka的消费总是会报错。比如dubbo接口就会抛出异常RpcException: The channel is closed. 这说明kafka还在…...

Go语言的网络编程

Go语言的网络编程 Go语言&#xff08;又称Golang&#xff09;是一种由Google开发的开源编程语言&#xff0c;以简洁、高效和并发为主要特点。网络编程是Go语言的一个重要应用领域&#xff0c;其优秀的并发模型和丰富的标准库使得Go成为开发高性能网络应用的理想选择。本篇文章…...

网络安全 | 网络安全法规:GDPR、CCPA与中国网络安全法

网络安全 | 网络安全法规&#xff1a;GDPR、CCPA与中国网络安全法 一、前言二、欧盟《通用数据保护条例》&#xff08;GDPR&#xff09;2.1 背景2.2 主要内容2.3 特点2.4 实施效果与影响 三、美国《加利福尼亚州消费者隐私法案》&#xff08;CCPA&#xff09;3.1 背景3.2 主要内…...

外部获取nVisual所在层级方法

Iframe嵌入nVisual&#xff0c;在iframe渲染完成之后&#xff0c;以后通过增加window.addEventListener()方法监听message事件&#xff0c;来获取nvisual当前的所在层级以及所选中的节点列表以及线缆列表。 nVisualPatrolDiagramIdList 变量是获取nVisual当前所在的层级的ID值…...

ARIMA模型 (AutoRegressive Integrated Moving Average) 算法详解与PyTorch实现

ARIMA模型 (AutoRegressive Integrated Moving Average) 算法详解与PyTorch实现 目录 ARIMA模型 (AutoRegressive Integrated Moving Average) 算法详解与PyTorch实现1. ARIMA模型概述1.1 时间序列预测1.2 ARIMA的优势2. ARIMA的核心技术2.1 自回归 (AR)2.2 差分 (I)2.3 移动平…...

解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域

解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务&#xff08;Spring MVC Springboot&#xff09;同时允许跨域 Tomcat 配置允许跨域Web 项目配置允许跨域Tomcat 同时允许静态文件和 Web 服务跨域 偶尔遇到一个 Tomcat 部署项目跨域问题&#xff0c;因为已经处理…...

【深度学习量化交易13】继续优化改造基于miniQMT的量化交易软件,增加补充数据功能,优化免费下载数据模块体验!

我是Mr.看海&#xff0c;我在尝试用信号处理的知识积累和思考方式做量化交易&#xff0c;应用深度学习和AI实现股票自动交易&#xff0c;目的是实现财务自由~ 目前我正在开发基于miniQMT的量化交易系统——看海量化交易系统。 MiniQMT是一种轻量级的量化交易解决方案&#xff0…...

【并发篇】CompletableFuture学习

CompletableFuture 异步编程 前言 我们异步执行一个任务时&#xff0c;一般是用线程池 Executor 去创建。 如果不需要有返回值&#xff0c;任务实现 Runnable 接口&#xff1b;如果需要有返回值&#xff0c;任务实现 Callable 接口&#xff0c;调用 Executor 的 submit 方法…...

【动手学电机驱动】STM32-MBD(3)Simulink 状态机模型的部署

STM32-MBD&#xff08;1&#xff09;安装 Simulink STM32 硬件支持包 STM32-MBD&#xff08;2&#xff09;Simulink 模型部署入门 STM32-MBD&#xff08;3&#xff09;Simulink 状态机模型的部署 [STM32-MBD&#xff08;4&#xff09;Simulink 状态机实现按键控制] (https://bl…...

springCloudGateWay使用总结

1、什么是网关 功能: ①身份认证、权限验证 ②服务器路由、负载均衡 ③请求限流 2、gateway搭建 2.1、创建一个空项目 2.2、引入依赖 2.3、加配置 3、断言工厂 4、过滤工厂 5、全局过滤器 6、跨域问题...

04、Redis深入数据结构

一、简单动态字符串SDS 无论是Redis中的key还是value&#xff0c;其基础数据类型都是字符串。如&#xff0c;Hash型value的field与value的类型&#xff0c;List型&#xff0c;Set型&#xff0c;ZSet型value的元素的类型等都是字符串。redis没有使用传统C中的字符串而是自定义了…...

zephyr移植到STM32

Zephy如何移植到单片机 1. Window下搭建开发环境1.1 安装Choncolatey1.2 安装相关依赖1.3创建虚拟python环境1.4 安装west1.4.1 使用 pip 安装 west1.4.2 检查 west 安装路径1.4.3 将 Scripts路径添加到环境变量1.4.4 验证安装 1.5 获取zephyr源码和[安装python](https://so.cs…...

Windows使用AutoHotKey解决鼠标键连击现象(解决鼠标连击、单击变双击的故障)

注&#xff1a;罗技鼠标&#xff0c;使用久了之后会出现连击现象&#xff0c;如果刚好过保了&#xff0c;可以考虑使用软件方案解决连击现象&#xff1a; 以下是示例AutoHotKey脚本&#xff0c;实现了调用XButton1用于关闭窗口&#xff08;以及WinW&#xff0c;XButton2也导向…...

案例研究:UML用例图中的结账系统

在软件工程和系统分析中&#xff0c;统一建模语言&#xff08;UML&#xff09;用例图是一种强有力的工具&#xff0c;用于描述系统与其用户之间的交互。本文将通过一个具体的案例研究&#xff0c;详细解释UML用例图的关键概念&#xff0c;并说明其在设计结账系统中的应用。 用…...

将光源视角的深度贴图应用于摄像机视角的渲染

将光源视角的深度贴图应用于摄像机视角的渲染是阴影映射&#xff08;Shadow Mapping&#xff09;技术的核心步骤之一。这个过程涉及到将摄像机视角下的片段坐标转换到光源视角下&#xff0c;并使用深度贴图来判断这些片段是否处于阴影中。 1. 生成光源视角的深度贴图 首先&…...

安卓漏洞学习(十八):Android加固基本原理

APP加固技术发展历程 APK加固整体思路 加固整体思路&#xff1a;先解压apk文件&#xff0c;取出dex文件&#xff0c;对dex文件进行加密&#xff0c;然后组合壳中的dex文件&#xff08;Android类加载机制&#xff09;&#xff0c;结合之前的apk资源&#xff08;解压apk除dex以外…...

前端数据模拟器 mockjs 和 fakerjs

功能&#xff1a;帮助前端生成随机数据&#xff0c;独立于后端单独开发 一、mockjs 安装&#xff1a;npm install mockjs 优点&#xff1a;官网是中文。 缺点&#xff1a;目前该库已经无人维护&#xff0c;也没人解决github上的bug。 官网 github地址 二、fakerjs 安装&#xf…...

Ruby语言的软件开发工具

Ruby语言的软件开发工具概述 引言 Ruby是一种简单且功能强大的编程语言&#xff0c;它以优雅的语法和灵活性而闻名。自1995年首次发布以来&#xff0c;Ruby已经被广泛应用于各种开发领域&#xff0c;特别是Web开发。随着Ruby语言的普及&#xff0c;相关的开发工具也日益丰富。…...

P8772 [蓝桥杯 2022 省 A] 求和

题目描述 给定 &#x1d45b; 个整数 &#x1d44e;1,&#x1d44e;2,⋯ ,&#x1d44e;&#x1d45b; 求它们两两相乘再相加的和&#xff0c;即 &#x1d446;&#x1d44e;1⋅&#x1d44e;2&#x1d44e;1⋅&#x1d44e;3⋯&#x1d44e;1⋅&#x1d44e;&#x1d45b;&…...

(七)Linux库的串口开发

文章目录 基于官方提供的串口测试代码部分解析代码部分1. usage 函数2. opt_parsing_err_handle 函数3. sig_handle 函数4. init_serial 函数5. serial_write 函数6. serial_read 函数7. run_read_mode 函数8. run_write_mode 函数9. run_loopback_test 函数 进行测试第一步编译…...

【git】在服务器使用docker设置了一个gogs服务器,访问和现实都不理想

以下问题应该都可以通过设置custom/conf/app.ini来解决 配置文档参考地址:https://www.bookstack.cn/read/gogs_zh/advanced-configuration_cheat_sheet.md domain显示的事localhost&#xff0c;实际上应该是一个IP地址。 关键字&#xff1a; DOMAIN ROOT_URL 因为是docker…...