如何集中处理注销+重定向

How to handle logout + redirect in a centralized manner

我正在 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 +状态更新and重定向到登录页面:

  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 拦截器中执行路由器重定向?
    这基本上归结为与上述相同的问题:How to perform the redirect when not having access to the useHistory hook?有什么方法可以“直接”访问路由器历史记录吗?
  3. 除上述之外:我是否应该直接从注销 AsyncThunk 中执行路由器重定向,而不是从调用 reducer 的任何地方执行它(ATM AppHeader 和 401 拦截器) ?
    如果建议从 AsyncThunk 执行重定向:如何使用路由器历史记录(同上问题...)?
  4. 以上都是“废话”吗?如果是这样,这里的实际最佳做法是什么?

仅供参考

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

我通过结合在其他 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

// ...

受保护-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 服务将无法正常工作...

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