http-класс перехватчика angularjs (ES6) теряет связь с этим

Я создаю и приложение AngularJS, использующее классы ES6 с трейсером, транслирующимся в ES5 в формате AMD.

в моем модуле я импортирую класс перехватчика и регистрирую его как сервис, а затем регистрирую этот сервис с помощью $httpProvider.interceptors в module.config:

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 указывает на объект окна, а не на мой перехватчик, поэтому у меня нет доступа к этому.$ Q или это.$ окно.

Я не могу понять, почему? Есть идеи?

9 ответов

Контекст (this) теряется, потому что среда Angular хранит ссылки только на сами функции-обработчики и вызывает их напрямую без какого-либо контекста, как указывал alexpods.

Я недавно написал сообщение в блоге о написании $http перехватчики, использующие TypeScript, который также применяется к классам ES6: AngularJS 1.x Перехватчики, использующие TypeScript.

Подводя итог, что я обсуждал в этом посте, чтобы не потерять this в ваших обработчиках вы должны будете определять методы как функции стрелок, эффективно помещая функции непосредственно в класс constructor функция в скомпилированном коде ES5.

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

Чтобы добавить в диалог, вы можете вернуть объект из конструктора, который содержит явно связанные методы класса.

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() {
       // ...
   }

}

Посмотрите на эти строки исходного кода:

// 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 метод помещается в цепочку, он теряет свой контекст (просто функция помещается без какого-либо контекста);

Позже здесь будет добавлено обещание как отклонить обратный вызов:

while (chain.length) {
    var thenFn = chain.shift();
    var rejectFn = chain.shift();

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

Так что, если обещание будет отклонено, rejectFn(ваш responseError функция) будет выполняться как обычная функция. В этом случае this ссылки на window если скрипт выполняется в нестрогом режиме или равен null иначе.

IMHO Angular 1 был написан с учетом ES5, поэтому я думаю, что использовать его с ES6 не очень хорошая идея.

Это точно та же проблема, с которой я сталкиваюсь, однако я нашел обходной путь, установив 'this' в собственную переменную, точно так же, как решение проблемы с областью видимости на es5, и она отлично работает:

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;

Обратите внимание, что использование функций стрелок в свойствах класса является экспериментальной функцией для ES7. Однако у большинства транспортеров с этим нет проблем.

Если вы хотите придерживаться официальной реализации ES6, вы можете создавать методы экземпляра вместо методов-прототипов, определяя ваши методы в конструкторе.

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, Так что вместо использования this.$q Вы можете просто использовать $q,
  • Нет необходимости возвращать явно связанные методы класса из конструктора

Наличие дополнительного уровня отступов является недостатком. Кроме того, этот метод может не подойти для классов, которые создаются много, поскольку в этом случае он потребляет больше памяти. Например; Использование прямых свойств класса (перенесенных в методы-прототипы) более эффективно для контроллеров компонентов, которые могут использоваться несколько раз на одной странице. Не беспокойтесь об услугах, провайдерах и фабриках, так как все они единичны и будут созданы только один раз.

Рабочий раствор с функциями стрелок:

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;

Мое рабочее решение без использования 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');
        }]);

Чтобы дополнить другие прекрасные ответы относительно функций стрелок, я думаю, что это немного чище, используя статический фабричный метод в Interceptor:

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');
}]);
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);
        }



    }



}
Другие вопросы по тегам