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