7.ng-zorro项目http拦截器和proxy代理

721 阅读5分钟

HttpInterceptor

http拦截器,统一处理请求和响应信息,附上ng-alain的优秀写法(偷偷修改了点)

default.interceptor.ts
import { Injectable, Injector } from '@angular/core';
import { Router, RouterStateSnapshot } from '@angular/router';
import {
    HttpInterceptor,
    HttpRequest,
    HttpHandler,
    HttpErrorResponse,
    HttpEvent,
    HttpResponseBase,
    HttpResponse,
    HttpHeaders
} from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { mergeMap, catchError } from 'rxjs/operators';
import { NzMessageService, NzNotificationService } from 'ng-zorro-antd';
import { environment } from '@env/environment';

const CODEMESSAGE = {
    200: '服务器成功返回请求的数据。',
    201: '新建或修改数据成功。',
    202: '一个请求已经进入后台排队(异步任务)。',
    204: '删除数据成功。',
    400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
    401: '用户没有权限(令牌、用户名、密码错误)。',
    403: '用户得到授权,但是访问是被禁止的。',
    404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
    406: '请求的格式不可得。',
    410: '请求的资源被永久删除,且不会再得到的。',
    422: '当创建一个对象时,发生一个验证错误。',
    500: '服务器发生错误,请检查服务器。',
    502: '网关错误。',
    503: '服务不可用,服务器暂时过载或维护。',
    504: '网关超时。',
};

/**
 * 默认HTTP拦截器,其注册细节见 `app.module.ts`
 */
@Injectable()
export class DefaultInterceptor implements HttpInterceptor {
    constructor(private injector: Injector) { }

    get msg(): NzMessageService {
        return this.injector.get(NzMessageService);
    }

    private goTo(url: string) {
        setTimeout(() => this.injector.get(Router).navigateByUrl(url));
    }

    private checkStatus(ev: HttpResponseBase) {
        if (ev.status >= 200 && ev.status < 300) { return; }
        const errortext = CODEMESSAGE[ev.status] || ev.statusText;
        this.injector.get(NzNotificationService).error(
            `请求错误 ${ev.status}: ${ev.url}`,
            errortext
        );
    }

    private handleData(ev: HttpResponseBase): Observable<any> {
        // 可能会因为 `throw` 导出无法执行 `_HttpClient` 的 `end()` 操作
        // if (ev.status > 0) {
        //   this.injector.get(_HttpClient).end();
        // }
        this.checkStatus(ev);
        // 业务处理:一些通用操作
        switch (ev.status) {
            case 200:
                // 业务层级错误处理,以下是假定restful有一套统一输出格式(指不管成功与否都有相应的数据格式)情况下进行处理
                // 例如响应内容:
                //  错误内容:{ status: 1, msg: '非法参数' }
                //  正确内容:{ status: 0, response: {  } }
                // 则以下代码片断可直接适用
                if (ev instanceof HttpResponse) {
                    const body: any = ev.body || {};
                    if (body.errno === 0) {
                        return of(new HttpResponse(Object.assign(ev, { body: body.data })));
                    } else {
                        this.msg.error(body.errmsg || '');
                        return of({})
                    }
                }
                break;
            case 401: // 未登录状态码
                // 请求错误 401: https://preview.pro.ant.design/api/401 用户没有权限(令牌、用户名、密码错误)。
                const url: string = this.injector.get(RouterStateSnapshot).url;
                // this.injector.get(AuthService).clear();
                break;
            case 403:
            case 404:
            case 500:
            default:
                if (ev instanceof HttpErrorResponse) {
                    console.warn('未可知错误,大部分是由于后端不支持CORS或无效配置引起', ev);
                    return throwError(ev);
                }
                break;
        }
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // 统一加上服务端前缀
        let url = req.url;
        if (!url.startsWith('https://') && !url.startsWith('http://')) {
            url = environment.SERVER_URL + url;
        }
        // 从本地获取用户详情
        const userInfo = JSON.parse(localStorage.getItem('userInfo')) || {};
        // 设置全局token
        const newHeaders = new HttpHeaders().set('Authorization', userInfo.token);

        const newReq = req.clone({ url, headers: newHeaders });
        return next.handle(newReq).pipe(
            mergeMap((event: any) => {
                // 允许统一对请求错误处理
                if (event instanceof HttpResponseBase) {
                    return this.handleData(event);
                }
                // 若一切都正常,则后续操作
                return of(event);
            }),
            catchError((err: HttpErrorResponse) => this.handleData(err)),
        );
    }
}
core.module.ts
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { DefaultInterceptor } from './net/default.interceptor';

@NgModule({
  declarations: [],
  imports: [],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true }
  ]
})
export class CoreModule { }

如果http拦截器直接依赖在app.module.ts里,项目中有惰性加载的模块,因为惰性模块没有注入到app.module.ts中,会无法使用拦截器,所以依赖在core模块,需要使用的模块将core引入即可。

1.拦截器中使用service

this.injector.get(NzMessageService)

private nzMessageService:NzMessageService

两种写法功能上基本一致,使用injector.get是动态加载,不用在初始化时就把所有服务全部获取。

2.业务逻辑处理

接口返回状态码方式:

200:接口成功,再处理逻辑

401:一般都是返回登陆处理

其他:接口失败

处理完成后根据业务逻辑返回相应状态:

return of(data); 返回成功数据

return throwError(ev); 返回错误原因

login.conponent.ts
this.passportService.login({
  username: this.validateForm.value.userName,
  password: this.validateForm.value.password
}).subscribe(data => {
  // 处理接口 return of(data)
  console.warn(data)
}, error => {
  // 处理拦截器中 return throwError(ev);
  console.warn(error)
})

3.请求头拼接token

用户登陆成功后,后面每个接口都需要携带token请求,为了防止页面刷新,登陆成功后的token存储到local里

// 从本地获取用户详情
const userInfo = JSON.parse(localStorage.getItem('userInfo')) || {};
// 设置全局token
const newHeaders = new HttpHeaders().set('Authorization', userInfo.token);

4.修改请求路径

此处加的是环境变量,后面代理中会用到

// 统一加上服务端前缀
let url = req.url;
if (!url.startsWith('https://') && !url.startsWith('http://')) {
    url = environment.SERVER_URL + url;
}

proxy代理

项目通过代理的方式来达到前后端分离,通过配置代理直接服务器进行代码开发。

在根目录新建proxy.config.json文件,在angular.json中配置即可。

proxy.config.json
{
  "/api": {
    "target": "http://127.0.0.1:8360"
  }
}
angular.json
{
  ...
  "projects": {
    "ng-admin": {
      ...
      "architect": {
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "ng-admin:build",
            "proxyConfig": "proxy.config.json"
          },
          ...
}

此时接口路径中存在/api就会发起代理请求

http://localhost:4200/api/user/login

实际发出的请求是:

http://127.0.0.1:8360/api/user/login
特殊情况

在新项目开发时,可能多个开发同时开发,并没有统一规定接口路径,导致找不到代理的路径

a的接口:/api/user/login

b的接口:/rest/auth/add

当然也可以配置多个

proxy.config.json
{
  "/api": {
    "target": "http://127.0.0.1:8360"
  },
  "/rest": {
    "target": "http://127.0.0.1:8360"
  },
}

如果更多的话处理起来不友好。

可以换种解决方法,使用环境变量,在本地环境变量中添加SERVER_URL用于代理使用,生产环境不配置

// 统一加上服务端前缀
let url = req.url;
if (!url.startsWith('https://') && !url.startsWith('http://')) {
    url = environment.SERVER_URL + url;
}
environment.ts
export const environment = {
  production: false,
  SERVER_URL: '/ng-api'
};
environment.prod.ts
export const environment = {
  production: false,
  SERVER_URL: ''
};

接口发出的请求在本地展示为:

http://localhost:4200/ng-api/api/user/login

此时修改代理文件,将环境变量里面的值作为代理的key,执行完成后key被替换为空,不再考虑后端是否统一。

proxy.config.json
{
  "/ng-api": {
    "target": "http://127.0.0.1:8360",
    "pathRewrite": { "^/ng-api": "" }
  }
}

接口发出的请求在本地展示为:

http://localhost:4200/api/user/login
更多proxy.config.json配置

参考webpack官网的proxy

webpack.js.org/configurati…