如何集中处理注销+重定向
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重定向到登录页面:
- 来自组件
AppHeader
- 来自每个 http 请求的“401 拦截器”
虽然所有必需的“操作”都已在 AppHeader
内部发生,但我不知道在 http.service.ts
.
中由谁来完成所有这些工作
问题
- 如何在 401 拦截时从
http.service.ts
中调度 logout
操作?
换句话说:如何在无法访问 useDispatch
挂钩时从“原始 TS 服务”(或其他“非反应组件文件”)调度操作?
我已经尝试使用 import { store } from '.../store'
+ store.dispatch(logout);
直接使用商店的调度方法,但这让我很难理解运行时错误。
- 我应该如何从 401 拦截器中执行路由器重定向?
这基本上归结为与上述相同的问题:How to perform the redirect when not having access to the useHistory
hook?有什么方法可以“直接”访问路由器历史记录吗?
- 除上述之外:我是否应该直接从注销
AsyncThunk
中执行路由器重定向,而不是从调用 reducer 的任何地方执行它(ATM AppHeader
和 401 拦截器) ?
如果建议从 AsyncThunk
执行重定向:如何使用路由器历史记录(同上问题...)?
- 以上都是“废话”吗?如果是这样,这里的实际最佳做法是什么?
仅供参考
我对整个 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 服务将无法正常工作...
虽然这基本上可行,但我仍然非常愿意接受改进建议!
我正在 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重定向到登录页面:
- 来自组件
AppHeader
- 来自每个 http 请求的“401 拦截器”
虽然所有必需的“操作”都已在 AppHeader
内部发生,但我不知道在 http.service.ts
.
问题
- 如何在 401 拦截时从
http.service.ts
中调度logout
操作?
换句话说:如何在无法访问useDispatch
挂钩时从“原始 TS 服务”(或其他“非反应组件文件”)调度操作?
我已经尝试使用import { store } from '.../store'
+store.dispatch(logout);
直接使用商店的调度方法,但这让我很难理解运行时错误。 - 我应该如何从 401 拦截器中执行路由器重定向?
这基本上归结为与上述相同的问题:How to perform the redirect when not having access to theuseHistory
hook?有什么方法可以“直接”访问路由器历史记录吗? - 除上述之外:我是否应该直接从注销
AsyncThunk
中执行路由器重定向,而不是从调用 reducer 的任何地方执行它(ATMAppHeader
和 401 拦截器) ?
如果建议从AsyncThunk
执行重定向:如何使用路由器历史记录(同上问题...)? - 以上都是“废话”吗?如果是这样,这里的实际最佳做法是什么?
仅供参考
我对整个 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 服务将无法正常工作...
虽然这基本上可行,但我仍然非常愿意接受改进建议!