如何使用 React Router V4 从 axios 拦截器重定向?

Posted

技术标签:

【中文标题】如何使用 React Router V4 从 axios 拦截器重定向?【英文标题】:How to redirect from axios interceptor with react Router V4? 【发布时间】:2017-08-29 20:20:39 【问题描述】:

我想在收到 403 错误时在 axios 拦截器中进行重定向。但是如何访问 React 组件之外的历史记录?

在Navigating Programatically in React-Router v4 中,它在 React 组件的上下文中,但这里我在 axios 上下文中尝试

axios.interceptors.response.use(function (response) 
    // Do something with response data
    return response;
  , function (error) 
    // Do something with response error
    if(error.response.status === 403)  console.log("Redirection needed !"); 

    // Trow errr again (may be need for some other catch)
    return Promise.reject(error);
);

【问题讨论】:

试试这个代码:import browserHistory from 'react-router'; 然后browserHistory.push("/path"); import browserHistory from 'react-router-dom';browserHistory.push("/path") 尝试过,但它不起作用,这是V3 方式,不是吗? 是的,不幸的是,这似乎不适用于 V4 路由器... react-router-v6 ***.com/q/69953377/14665310问题的链接 【参考方案1】:

我通过从组件树外部访问我的 Redux Store 并从注销按钮发送相同的操作来解决这个问题,因为我的拦截器是在一个单独的文件中创建并在加载任何组件之前加载的。

所以,基本上,我做了以下事情:

index.js 文件:

//....lots of imports ommited for brevity
import  createStore, applyMiddleware  from 'redux';
import reduxThunk from 'redux-thunk';
import reducers from './reducers';
import  UNAUTH_USER  from './actions/types'; //this is just a constants file for action types.

const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
const store = createStoreWithMiddleware(reducers);

//Here is the guy where I set up the interceptors!
NetworkService.setupInterceptors(store);

//lots of code ommited again...
//Please pay attention to the "RequireAuth" below, we'll talk about it later

ReactDOM.render(
  <Provider store=store>
      <BrowserRouter>
          <div>
              <Header />
              <main className="plan-container">
                  <Switch>
                      <Route exact path="/" component=Landing />
                      <Route exact path="/login" component=Login />
                      <Route exact path="/signup" component=Signup />
                      <Route exact path="/calendar" component=RequireAuth(Calendar) />
                      <Route exact path="/profile" component=RequireAuth(Profile) />
                  </Switch>
              </main>
          </div>
      </BrowserRouter>
  </Provider>
  , document.querySelector('.main-container'));

network-service.js 文件中:

import axios        from 'axios';
import  UNAUTH_USER  from '../actions/types';

export default 
  setupInterceptors: (store) => 

    // Add a response interceptor
    axios.interceptors.response.use(function (response) 
        return response;
    , function (error) 
        //catches if the session ended!
        if ( error.response.data.token.KEY == 'ERR_EXPIRED_TOKEN') 
            console.log("EXPIRED TOKEN!");
            localStorage.clear();
            store.dispatch( type: UNAUTH_USER );
        
        return Promise.reject(error);
    );

  
;

最后但并非最不重要的一点是,我有一个 HOC(高阶组件),我将受保护的组件包装在其中,当会话结束时我会在其中执行实际重定向。这样,当我触发动作类型 UNAUTH_USER 时,它会将我的 session 减速器的 isLogged 属性设置为 false,因此该组件会随时收到通知并为我执行重定向。

require-auth.js 组件的文件:

import React,  Component  from 'react';
import  connect  from 'react-redux';

export default function(ComposedComponent) 

    class RequireAuth extends Component 

        componentWillMount() 
            if(!this.props.session.isLogged) 
                this.props.history.push('/login');
            
        ;

        componentWillUpdate(nextProps) 
            if(!nextProps.session.isLogged) 
                this.props.history.push('/login');
            
        ;

        render() 
            return <ComposedComponent ...this.props />
        
    

    function mapStateToProps(state) 
        return  session: state.session ;
    

    return connect(mapStateToProps)(RequireAuth);

希望有帮助!

【讨论】:

首先,这是 HOC 的绝佳解决方案!但是,您不想检查componentDidUpdate 中的下一个更新的会话道具,而不是componentWillUpdate @w3bshark 当我在componentWillUpdatecomponentWillMount 中检查它时,我实际上是在避免在会话丢失时重新渲染组件。如果我在componentDidUpdate 中这样做,它会首先被渲染,然后检查会话是否应该存在,在我看来,这是对安全性的破坏和错误的设计。 我无法理解您的解决方案。拦截器不会添加到您要导出的任何 axios 实例中,那么如何发出 api 请求以使用所述拦截器?我想你没有暴露window.axios。看来您需要以某种方式导出用拦截器装饰的 axios 实例,但我看不出那会在哪里 嗨@MaciejGurban。据我所知,axios 在导入时就像一个单例,所以只要你在某个时候调用拦截器,它就会在库的同一个实例中应用于整个应用程序。 我没有使用 redux,而是使用 React Context 来管理应用程序状态。我不确定是否有办法从 React 外部调用用户上下文。任何指示/建议?【参考方案2】:

我通过从 history (https://github.com/ReactTraining/history) 包创建浏览器历史记录并将其传递给拦截器函数,然后从中调用 .push() 方法来解决此任务。

主文件代码(部分):

// app.js
import  createBrowserHistory  from 'history';
import httpService from './api_client/interceptors';

...

const history = createBrowserHistory();
httpService.setupInterceptors(store, history);

拦截器配置:

import axios from 'axios';

export default 
  setupInterceptors: (store, history) => 

      axios.interceptors.response.use(response => 
        return response;
      , error => 

      if (error.response.status === 401) 
        store.dispatch(logoutUser());
      

      if (error.response.status === 404) 
         history.push('/not-found');
      

      return Promise.reject(error);
    );
  ,
;

此外,您应该使用来自react-router (https://github.com/ReactTraining/react-router) 的Router,并传递与history 参数相同的历史对象。

// app.js
...
ReactDOM.render(
  <Provider store=store>
     <Router history=history>
        ...
     </Router>
  </Provider>
, document.getElementById('#root'))

【讨论】:

在这种情况下,您用来进行 API 调用的代码是什么样的,即您导入什么来发出请求? @MaciejGurban 我简单地导入了 axios 库并照常使用getpost 我明白了,那么您将如何测试这些拦截器? 谢谢你救了我【参考方案3】:

这似乎对我有用

 function (error) 
            var accessDenied = error.toString().indexOf("401");
            if (accessDenied !== -1) 
              console.log('ACCESS DENIED')
              return window.location.href = '/accessdenied'
            
          );

【讨论】:

这将完全重新加载选项卡并从一开始就开始反应,而不是操纵window.history【参考方案4】:

这很好用。

window.location.href = `$process.env.REACT_APP_BASE_HREF/login`;

【讨论】:

【参考方案5】:

我发现的最佳解决方案是在我的主要 React 组件中定义 axios.interceptors 并使用 that 来处理错误: (以及来自路由器 V4 的 withRouter

import withRouter from 'react-router-dom';

class Homepage extends Component 
  static propTypes = 
    history: PropTypes.object.isRequired
  

  constructor(props)
    super(props);

    let that = this;
    axios.interceptors.response.use(function (response) 
        // Do something with response data
        return response;
      , function (error) 
        // Do something with response error
        if(error.response.status === 403)  that.handle403() 

        // Trow errr again (may be need for some other catch)
        return Promise.reject(error);
    );

  

  handle403()
    this.props.history.push('/login');
  

【讨论】:

这个依赖于组件初始化顺序,如果在安装拦截器之前有组件发出axios请求,将不起作用。【参考方案6】:

刚刚意识到问题是针对反应路由器 v4 的,我已经写了我在 v5 中使用的答案。

我通过将 useHistory()&lt;Router&gt; 内部传递给 axios 拦截器解决了这个问题。

App.js:

// app.js

function App() 
  return (
    <Router>
      <InjectAxiosInterceptors />

      <Route ... />
      <Route ... />
    </Router>
  )

InjectAxiosInterceptors.js:

import  useEffect  from "react"
import  useHistory  from "react-router-dom"
import  setupInterceptors  from "./plugins/http"

function InjectAxiosInterceptors () 
  const history = useHistory()

  useEffect(() => 
    console.log('this effect is called once')
    setupInterceptors(history)
  , [history])

  // not rendering anything
  return null

插件/http.js:

import axios from "axios";

const http = axios.create(
  baseURL: 'https://url'
)

/**
 * @param import('history').History history - from useHistory() hook
 */
export const setupInterceptors = history => 
  http.interceptors.response.use(res => 
    // success
    return res
  , err => 
    const  status  = err.response
  
    if (status === 401) 
      // here we have access of the useHistory() from current Router
      history.push('/login')
    
  
    return Promise.reject(err)
  )


export default http

【讨论】:

如何在组件内部使用拦截器。【参考方案7】:

这是对我有用的已接受答案的修改版本。

使用 BrowserRouter 将 App 组件包装在 index.js 中,否则 useHistory() 挂钩将不起作用。

import React from 'react';
...
import  BrowserRouter  from "react-router-dom";

ReactDOM.render(
  <Provider store=store>
    <BrowserRouter><App /></BrowserRouter>
  </Provider>,
  document.getElementById('root')
);

创建一个单独的文件来实例化自定义 axios 实例

import axios from 'axios';

let headers = ;
const baseURL = "http://localhost:8080"
const jwtToken = localStorage.getItem("Authorization");

if (jwtToken) 
    headers.Authorization = 'Bearer ' + jwtToken;


const axiosInstance = axios.create(
    baseURL: baseURL,
    headers,
);

export default axiosInstance;

使用之前创建的自定义 axios 实例的拦截器方法创建另一个文件。

import axiosInstance from "./ServerAxios";
import  useHistory  from "react-router-dom";

const baseURL = "http://localhost:8080"

const SetupInterceptors = () => 
    let history = useHistory();
    axiosInstance.interceptors.response.use(function (response) 
        return response;
    , function (error) 
        var status = error.response.status;
        var resBaseURL = error.response.config.baseURL;
        if (resBaseURL === baseURL && status === 403) 
            localStorage.removeItem("Authorization");
            history.push("/login");
        
        return Promise.reject(error);
    );


export default SetupInterceptors;

然后导入并调用App.js文件中的setup方法

...
import  createBrowserHistory  from 'history';
import SetupInterceptors from './middleware/NetworkService';
const App = () => 
  const history = createBrowserHistory();
  SetupInterceptors(history);
...

然后每当你需要使用自定义的 axios 实例时,导入实例化的文件并使用它。

import ServerAxios from "../middleware/ServerAxios";
ServerAxios.post(......);

【讨论】:

【参考方案8】:

接受的答案并不能解决我的问题。在 axios 和拦截器周围的票没有触发之后,我发现 axios 不支持像上面描述的那样全局装饰拦截器。对于未来的读者,请记住,axios 已将此global interceptor 标记为功能。所以也许我们将来会得到它。参考:https://github.com/axios/axios/issues/993。

我确实有一个用于所有 api 调用的 axios 实例,所以我解决了在其中定义拦截器的问题。

【讨论】:

【参考方案9】:

我正在使用 react-router-dom,它具有“历史”道具,可用于过渡到新路线

 history.push('/newRoute')

【讨论】:

以上是关于如何使用 React Router V4 从 axios 拦截器重定向?的主要内容,如果未能解决你的问题,请参考以下文章

从 v3 迁移到 v4 react-router 时出现错误

react-router 从 v3 版本升到 v4 版本,升级小记

React Router v4 - 如何检测后退按钮导航与 url 刷新?

如何在React Router v4中获取所有参数

如何使用 react-router v4 检测路由变化?

React Hooks with React Router v4 - 我如何重定向到另一个路由?