Angularjs http-перехватчик класса (ES6) теряет привязку к 'this'
Я создаю приложение 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 или this. $window.
Я не могу понять, почему? Любые идеи?
Ответы
Ответ 1
Контекст (this
) теряется, поскольку структура Angular сохраняет ссылки только на функции обработчика и вызывает их непосредственно без какого-либо контекста, поскольку alexpods указал.
Недавно я написал сообщение в блоге о написании перехватчиков $http
с использованием TypeScript, что также относится к классам ES6: AngularJS 1.x Interceptors Использование 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);
}
}
Ответ 2
Посмотрите эти строки исходного кода:
// 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
, если script выполняется в нестрочном режиме или равно null
в противном случае.
IMHO Angular 1 был написан с учетом ES5, поэтому я думаю, что использование его с ES6 не является хорошей идеей.
Ответ 3
Это точно та же проблема, которую я испытываю, однако,
Я нашел обходной путь, установив 'this' в переменную self, точно так же, как решение проблемы определения области на 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;
Ответ 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() {
// ...
}
}
Ответ 5
Обратите внимание, что использование функций стрелок в свойствах класса является экспериментальной функцией для 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);
};
}
}
Ответ 6
Рабочее решение со стрелочными функциями:
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;
Ответ 7
Чтобы оценить другие тонкие ответы на функции стрелок, я думаю, что это немного чище, используя статический метод factory в 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');
}]);
Ответ 8
Мое рабочее решение без использования 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');
}]);
Ответ 9
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);
}
}
}