【问题标题】:How to handle logout + redirect in a centralized manner如何集中处理注销+重定向
【发布时间】:2021-10-17 08:20:35
【问题描述】:

我正在一个 reducer (+ AsyncThunk) 中执行注销所需的所有操作(异步 http 请求、本地存储 + 状态更新)。我唯一缺少的就是在这个“集中式上下文”中执行所需的重定向到登录页面。

以下是涉及的主要代码:

http.service.ts

// ...
const httpClient: AxiosInstance = axios.create({ /* ... */ });

httpClient.interceptors.response.use(undefined, async function (error) {
  if (401 === error.response.status) {
    // TODO: How to logout incl. http request, localstorage + state update & redirect?
  }
  return Promise.reject(error);
});
// ...

auth.service.ts

// ...
class AuthService {
  // ...
  async logout(): Promise<void> {
    localStorage.removeItem("User");
    await httpClient.post(ApiRoutesService.postLogout());
  }
}
// ...

auth.slice.ts

// ...
export const logout = createAsyncThunk("auth/logout", async () => {
  await AuthService.logout();
});
// ...
const authSlice = createSlice({
  // ...
  extraReducers: (builder) => {
    builder
      // ...
      .addCase(logout.fulfilled, (state, _action) => {
        state.isLoggedIn = false;
        state.id = emptyUser.id;
        state.email = emptyUser.email;
        state.fullName = emptyUser.fullName;
      });
  },
});
// ...

app-header.tsx

// ...
export default function AppHeader(): JSX.Element {
  // ...
  const onLogout = useCallback(() => {
    dispatch(logout());
  }, [dispatch]);

  return (
    <nav>
      <ul>
        {/* ... */}
        <li>
          <Link to={RouterService.paths.login} onClick={onLogout}>
            Logout
          </Link>
        </li>
      </ul>
      {/* ... */}
    </nav>
  );
}

问题

ATM 有 2 个地方我想执行整个注销,包括。 http 请求,localStorage + 状态更新重定向到登录页面:

  1. 从组件内部AppHeader
  2. 来自每个 http 请求的“401 拦截器”

虽然AppHeader 内部已经发生了所有必需的“操作”,但我不知道在http.service.ts 内部应该由谁来做这一切。

问题

  1. 如何在 401 拦截时从 http.service.ts 内调度 logout 操作?
    换句话说:当无法访问 useDispatch 挂钩时,如何从“原始 TS 服务”(或其他“非反应组件文件”)调度操作?
    我已经尝试使用 import { store } from '.../store' + store.dispatch(logout); 直接使用商店的调度方法,但这让我很难理解运行时错误。
  2. 我应该如何从 401 拦截器中执行路由器重定向?
    这基本上归结为与上述相同的问题:当无法访问 useHistory 挂钩时如何执行重定向?有没有办法“直接”访问路由器历史记录?
  3. 除上述之外:我是否应该直接从注销AsyncThunk 中执行路由器重定向,而不是从调用reducer 的任何地方(ATM AppHeader 和401 拦截器)执行路由器重定向?
    如果建议从AsyncThunk 执行重定向:如何使用路由器历史记录(与上述相同的问题...)?
  4. 以上都是“废话”吗?如果是,这里有哪些实际的最佳做法?

仅供参考

我对整个 React 生态系统还很陌生,并且正在学习所涉及的所有概念。显示的代码几乎是从各种在线资源中复制粘贴和调整的,虽然我确实了解所涉及的基础知识,但我对有关 redux 工具包、钩子等的所有细节只有一点浅薄的了解。

【问题讨论】:

    标签: reactjs react-redux react-hooks react-router redux-toolkit


    【解决方案1】:

    我通过结合我在其他 SO 问题和互联网资源(博客等)中找到的一些建议解决了这个问题:

    概述

    • 将创建后的 redux 存储“注入”到 http 服务中,以允许它调度操作(logout 此处)
    • 创建一个显式的ProtectedRoute 组件,而不是路由器默认的Route 用于需要用户进行身份验证的路由。
      如果有人尝试访问受保护的路由而未经过身份验证,或者经过身份验证的用户在运行时注销(例如,由于某些过期的 cookie 等),此路由“监听”存储中的更改并执行重定向。李>

    实现细节

    http.service.ts

    const httpClient: AxiosInstance = axios.create({ /* ... */ });
    
    // Creation of the interceptor is now wrapped inside a function which allows
    // injection of the store
    export function setupHttpInterceptor(store: AppStore): void {
      http.interceptors.response.use(undefined, async function (error) {
        if (401 === error.response.status) {
          // Store action can now be dispatched from within the interceptor
          store.dispatch(logout());
        }
        return Promise.reject(error);
      });
    }
    

    store.ts

    // ...
    import { setupHttpInterceptor } from '../services/http.service';
    // ...
    
    export const store = configureStore({ /* ... */ });
    
    setupHttpInterceptor(store); // Inject the store into the http service
    
    // ...
    

    protected-route.tsx

    // ...
    
    type ProtectedRouteProps = { path?: string } & RouteProps;
    
    export function ProtectedRoute(routeProps: ProtectedRouteProps): JSX.Element {
      const { isLoggedIn } = useAppSelector((state) => state.auth);
      const loginCmpState: LoginLocationState = { referrer: routeProps.path };
    
      // This handles the redirect and also reacts to changes to `auth` state "during runtime"
      return isLoggedIn ? (
        <Route {...routeProps} />
      ) : (
        <Redirect to={{ pathname: RouterService.paths.login, state: loginCmpState }} />
      );
    }
    

    app-body.tsx(无论在哪里定义路由)

    // ...
    export default function AppBody(): JSX.Element {
      return (
        <Switch>
          <Route path='/login'>
            <Login />
          </Route>
          <ProtectedRoute path='/user-home'>
            <UserHome />
          </ProtectedRoute>
        </Switch>
      );
    }
    // ...
    

    注意事项

    我觉得商店需要将自己注入到 http 服务中有点奇怪,因为这会在这两者之间产生一种耦合,恕我直言,这不是很干净。如果商店以某种方式“忘记”进行拦截,http 服务将无法正常工作......

    虽然这基本上可行,但我仍然非常愿意提出改进建议!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-07-21
      • 1970-01-01
      • 2017-06-01
      • 2012-11-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多