AngularJS的HTTP拦截器类(ES6)失去了对'this'的绑定。

16

我正在使用ES6类构建一个AngularJS应用程序,并使用traceur将其转换为AMD格式的ES5。

在我的模块中,我导入拦截器类并将其注册为服务,然后在module.config中将此服务注册到$httpProvider.interceptors中:

var commonModule = angular.module(moduleName, [constants.name]);

import authenticationInterceptor from './authentication/authentication.interceptor';

commonModule.service('authenticationInterceptor', authenticationInterceptor);

commonModule.config( $httpProvider =>  {
    $httpProvider.interceptors.push('authenticationInterceptor');
});

我的拦截器类注入了 $q$window 服务,并将它们保存在构造函数中以备后用。我跟踪了这部分代码并且注入是正确进行的:

'use strict';
/*jshint esnext: true */

var authenticationInterceptor = class AuthenticationInterceptor {

    /* ngInject */
    constructor($q, $window) {
        this.$q = $q;
        this.$window = $window;
    }

    responseError(rejection) {
        var authToken = rejection.config.headers.Authorization;
        if (rejection.status === 401 && !authToken) {
            let authentication_url = rejection.data.errors[0].data.authenticationUrl;
            this.$window.location.replace(authentication_url);
            return this.$q.defer(rejection);
        }
        return this.$q.reject(rejections);
    }
}

authenticationInterceptor.$inject = ['$q', '$window'];

export default authenticationInterceptor;
当我发出一个响应 401 的请求时,拦截器会适当地触发,但在“responseError”方法中,“this”变量指向的是窗口对象而不是我的拦截器,因此我无法访问this.$qthis.$window。我不知道为什么?有任何想法吗?
9个回答

27

由于Angular框架仅保留处理程序函数本身的引用并直接调用它们,因此上下文(this)会丢失,正如alexpods所指出的那样。

我最近写了一篇关于使用TypeScript编写$http拦截器的博客文章,这也适用于ES6类: AngularJS 1.x Interceptors Using TypeScript

总结一下我在这篇文章中讨论的内容,为了不在处理程序中丢失this,您必须将方法定义为箭头函数,有效地将这些函数直接放置在编译后的ES5代码的类constructor函数内部。

class AuthenticationInterceptor {

    /* ngInject */
    constructor($q, $window) {
        this.$q = $q;
        this.$window = $window;
    }

    responseError = (rejection) => {
        var authToken = rejection.config.headers.Authorization;
        if (rejection.status === 401 && !authToken) {
            let authentication_url = rejection.data.errors[0].data.authenticationUrl;
            this.$window.location.replace(authentication_url);
            return this.$q.defer(rejection);
        }
        return this.$q.reject(rejections);
    }
}
如果您坚持将拦截器编写为完全基于原型的类,您可以为拦截器定义一个基类并进行扩展。基类将使用实例方法替换原型拦截器函数,因此我们可以像这样编写拦截器:

class HttpInterceptor {
  constructor() {
    ['request', 'requestError', 'response', 'responseError']
        .forEach((method) => {
          if(this[method]) {
            this[method] = this[method].bind(this);
          }
        });
  }
}

class AuthenticationInterceptor extends HttpInterceptor {

    /* ngInject */
    constructor($q, $window) {
        super();
        this.$q = $q;
        this.$window = $window;
    }

    responseError(rejection) {
        var authToken = rejection.config.headers.Authorization;
        if (rejection.status === 401 && !authToken) {
            let authentication_url = rejection.data.errors[0].data.authenticationUrl;
            this.$window.location.replace(authentication_url);
            return this.$q.defer(rejection);
        }
        return this.$q.reject(rejections);
    }
}

对我有用。谢谢。 - chr.solr

4

看这些源代码的行数

// apply interceptors
forEach(reversedInterceptors, function(interceptor) {
    if (interceptor.request || interceptor.requestError) {
        chain.unshift(interceptor.request, interceptor.requestError);
    }
    if (interceptor.response || interceptor.responseError) {
        chain.push(interceptor.response, interceptor.responseError);
    }
});

interceptor.responseError 方法被推入链中时,它失去了上下文(只是函数被推入,没有任何上下文);
稍后,它将作为拒绝回调添加到 promise 中:这里
while (chain.length) {
    var thenFn = chain.shift();
    var rejectFn = chain.shift();

    promise = promise.then(thenFn, rejectFn);
}

如果 Promise 被拒绝,rejectFn (你的 responseError 函数) 将作为普通函数执行。在这种情况下,如果脚本在非严格模式下执行,则 this 引用 window,否则等于 null

我认为 Angular 1 是考虑 ES5 编写的,因此我认为使用 ES6 不是一个好主意。


1
感谢您的深入回答。我从来不太喜欢深入挖掘Angular源代码,因为它非常复杂。至于ES6,它是ES5的超集,因此我现在可以根据需要混合使用样式,这就是我所做的。我将拦截器类更改为返回对象的函数,其他所有内容都保持不变,它也起作用了。我正在推动ES6以走在前沿,但有时候当你领先时,会有一些小问题。 - Kendrick Burson

4
为了增加讨论,您可以从构造函数中返回一个包含显式绑定类方法的对象。
export default class HttpInterceptor {

   constructor($q, $injector) {
       this.$q = $q;
       this.$injector = $injector;

       return {
           request: this.request.bind(this),
           requestError: this.requestError.bind(this),
           response: this.response.bind(this),
           responseError: this.responseError.bind(this)
       }
   }

   request(req) {
       this.otherMethod();
       // ...
   }

   requestError(err) {
       // ...
   }

   response(res) {
       // ...
   }

   responseError(err) {
       // ...
   }

   otherMethod() {
       // ...
   }

}

我发现这是最简单/最优雅的解决方案。我将工厂对象的创建放入了一个create()方法中,而不是构造函数中。你的错别字我盲目地复制粘贴,真的让我感到困惑!response: this.responseError.bind(this) - hackel

4

我正经历着同样的问题,但是我通过像解决ES5范围问题一样将“this”设置为self变量来找到了一个解决方法,它很好用:

let self;

class AuthInterceptor{

   constructor(session){
       self = this;
       this.session = session;
   }

   request(config){
       if(self.session) {
           config.headers = self.session.getSessionParams().headers; 
       }
       return config;
   }

   responseError(rejection){
       if(rejection.status == 401){

       }

       return rejection;
   }

}

export default AuthInterceptor;

1
我对这个解决方案的唯一问题是你的“self”变量在全局范围内。我想,如果你将其包装在一个amd定义中,它会被限制在闭包中,但它仍然感觉像一个全局变量。 - Kendrick Burson
我尝试在类内部定义它,但是它不起作用,我不知道为什么在ES6类中定义局部变量是被禁止的,无论如何,Traceur编译器会将其包装在System.registerModule定义中。 - Avie

1
请注意,在类属性中使用箭头函数是ES7的实验性功能。但是,大多数转译器都没有问题。

If you want to stick to the official ES6 implementation you can create instance methods instead of prototype methods by defining your methods in the constructor.

class AuthenticationInterceptor {
  /* ngInject */
  constructor($q, $window) {
    
    this.responseError = (rejection) => {
      const authToken = rejection.config.headers.Authorization;
      if (rejection.status === 401 && !authToken) {
        const authentication_url = rejection.data.errors[0].data.authenticationUrl;
        $window.location.replace(authentication_url);
        return $q.defer(rejection);
      }
      return $q.reject(rejection);
    };
    
  }
}

我喜欢这个解决方案,因为它减少了样板代码的数量;
  • 您不再需要将所有依赖项放在this中。因此,您可以仅使用$q而不是this.$q
  • 无需从构造函数显式返回绑定的类方法

额外的缩进层次是一个缺点。此外,对于经常实例化的类,此方法可能不适用,因为在这种情况下会消耗更多的内存。例如; 对于在同一页上可能被多次使用的组件控制器,使用直接类属性(转译为原型方法)更有效。不必担心服务、提供程序和工厂,因为它们都是单例,并且只会被实例化一次。


0
为了补充有关箭头函数的其他好答案,我认为在拦截器中使用静态工厂方法会更加简洁:
export default class AuthenticationInterceptor {
 static $inject = ['$q', '$injector', '$rootRouter'];
 constructor ($q, $injector, $rootRouter) {
  this.$q = $q;
  this.$injector = $injector;
  this.$rootRouter = $rootRouter;
 }

 static create($q, $injector, $rootRouter) {
  return new AuthenticationInterceptor($q, $injector, $rootRouter);
 }

 responseError = (rejection) => {
  const HANDLE_CODES = [401, 403];

  if (HANDLE_CODES.includes(rejection.status)) {
   // lazy inject in order to avoid circular dependency for $http
   this.$injector.get('authenticationService').clearPrincipal();
   this.$rootRouter.navigate(['Login']);
  }
  return this.$q.reject(rejection);
 }
}

使用方法:

.config(['$provide', '$httpProvider', function ($provide, $httpProvider) {
$provide.factory('reauthenticationInterceptor', AuthenticationInterceptor.create);
$httpProvider.interceptors.push('reauthenticationInterceptor');
}]);

0

使用箭头函数的工作解决方案:

var AuthInterceptor = ($q, $injector, $log) => {
    'ngInject';

    var requestErrorCallback = request => {
        if (request.status === 500) {
          $log.debug('Something went wrong.');
        }
        return $q.reject(request);
    };

    var requestCallback = config => {
        const token = localStorage.getItem('jwt');

        if (token) {
            config.headers.Authorization = 'Bearer ' + token;
        }
        return config;
    };

    var responseErrorCallback = response => {
         // handle the case where the user is not authenticated
        if (response.status === 401 || response.status === 403) {
            // $rootScope.$broadcast('unauthenticated', response);
            $injector.get('$state').go('login');
       }
       return $q.reject(response);
    }

  return {
    'request':       requestCallback,
    'response':      config => config,
    'requestError':  requestErrorCallback,
    'responseError': responseErrorCallback,
  };
};

/***/
var config = function($httpProvider) {
    $httpProvider.interceptors.push('authInterceptor');
};

/***/    
export
default angular.module('services.auth', [])
    .service('authInterceptor', AuthInterceptor)
    .config(config)
    .name;

OP想把它做成一个类。你的解决方案是将其作为函数或以Angular 1中经典的方式完成,这也不错。但这并没有回答问题。 - Andrei R

0

我的工作解决方案,不使用ngInject

myInterceptor.js

export default ($q) => {
let response = (res) => {
    return res || $q.when(res);
}

let responseError = (rejection) => {
    //do your stuff HERE!!
    return $q.reject(rejection);
}

return {
    response: response,
    responseError: responseError
}

}

myAngularApp.js

// angular services
import myInterceptor from 'myInterceptor';

// declare app
const application = angular.module('myApp', [])
        .factory('$myInterceptor', myInterceptor)
        .config(['$httpProvider', function($httpProvider) {  
           $httpProvider.interceptors.push('$myInterceptor');
        }]);

-1
export default class AuthInterceptor{


    /*@ngInject;*/
    constructor(SomeService,$q){

        this.$q=$q;
        this.someSrv = SomeService;



        this.request = (config) =>{
            ...
            this.someSrv.doit();
            return config;

        }

        this.response = (response)=>{
            ...
            this.someSrv.doit();
            return response;
        }

        this.responseError = (response) => {
           ...
           return this.$q.reject(response);
        }



    }



}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接