下面,我们来系统的梳理关于 React Router 路由守卫 的基本知识点:


一、路由守卫概述

1.1 什么是路由守卫

路由守卫是一种在用户导航到特定路由之前或离开特定路由时执行逻辑的机制。它允许开发者控制用户访问权限、验证条件或执行数据预加载等操作。

1.2 为什么需要路由守卫

  • 访问控制:限制未授权用户访问敏感页面
  • 数据预加载:在路由渲染前获取必要数据
  • 表单保护:防止用户意外离开包含未保存数据的页面
  • 权限验证:根据用户角色显示不同内容
  • SEO优化:确保页面渲染前满足SEO要求

二、核心守卫类型

2.1 进入守卫(Before Enter)

在路由渲染前执行的守卫逻辑:

// 使用自定义守卫组件
function AuthGuard({ children }) {
  const { isAuthenticated } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  
  useEffect(() => {
    if (!isAuthenticated) {
      navigate('/login', {
        state: { from: location },
        replace: true
      });
    }
  }, [isAuthenticated, navigate, location]);
  
  return isAuthenticated ? children : <LoadingSpinner />;
}

// 在路由配置中使用
<Route 
  path="/dashboard" 
  element={
    <AuthGuard>
      <Dashboard />
    </AuthGuard>
  }
/>

2.2 离开守卫(Before Leave)

在用户离开当前路由前执行的守卫逻辑:

function UnsavedChangesGuard() {
  const { isDirty } = useForm();
  const navigate = useNavigate();
  
  useEffect(() => {
    const handleBeforeUnload = (e) => {
      if (isDirty) {
        e.preventDefault();
        e.returnValue = '';
      }
    };
    
    window.addEventListener('beforeunload', handleBeforeUnload);
    
    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, [isDirty]);
  
  useBlocker((tx) => {
    if (isDirty && !window.confirm('您有未保存的更改,确定要离开吗?')) {
      tx.retry();
    }
  }, isDirty);
  
  return null;
}

// 在需要保护的组件中使用
function EditProfile() {
  return (
    <>
      <UnsavedChangesGuard />
      {/* 表单内容 */}
    </>
  );
}

2.3 数据加载守卫(Data Loading)

在路由渲染前加载必要数据:

function DataLoader({ children, loader }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const navigate = useNavigate();
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        const result = await loader();
        setData(result);
      } catch (err) {
        setError(err);
        navigate('/error', { state: { error: err.message } });
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [loader, navigate]);
  
  if (loading) return <LoadingSpinner />;
  if (error) return null; // 已重定向到错误页
  
  return children(data);
}

// 使用示例
<Route 
  path="/user/:id" 
  element={
    <DataLoader loader={() => fetchUser(userId)}>
      {(user) => <UserProfile user={user} />}
    </DataLoader>
  }
/>

三、实现路由守卫的四种模式

3.1 高阶组件模式(HOC)

function withGuard(Component, guard) {
  return function GuardedComponent(props) {
    const navigate = useNavigate();
    const location = useLocation();
    
    useEffect(() => {
      const result = guard({ location, navigate });
      
      if (result?.redirect) {
        navigate(result.redirect, {
          replace: true,
          state: { from: location }
        });
      }
    }, [navigate, location]);
    
    return <Component {...props} />;
  };
}

// 定义守卫函数
const authGuard = ({ location }) => {
  const { isAuthenticated } = useAuth();
  return !isAuthenticated 
    ? { redirect: `/login?from=${location.pathname}` } 
    : null;
};

// 应用守卫
const GuardedDashboard = withGuard(Dashboard, authGuard);

3.2 路由包装器模式

function RouteGuard({ children, conditions }) {
  const navigate = useNavigate();
  const location = useLocation();
  
  useEffect(() => {
    for (const condition of conditions) {
      const result = condition({ location, navigate });
      if (result?.redirect) {
        navigate(result.redirect, {
          replace: true,
          state: { from: location }
        });
        return;
      }
    }
  }, [conditions, navigate, location]);
  
  return children;
}

// 使用示例
<Route 
  path="/admin" 
  element={
    <RouteGuard conditions={[authGuard, adminGuard]}>
      <AdminPanel />
    </RouteGuard>
  }
/>

3.3 React Router v6.4+ Loader 模式

const router = createBrowserRouter([
  {
    path: '/dashboard',
    element: <Dashboard />,
    loader: async () => {
      const isAuthenticated = await checkAuth();
      
      if (!isAuthenticated) {
        throw redirect('/login');
      }
      
      const data = await fetchDashboardData();
      return data;
    },
    errorElement: <ErrorPage />
  }
]);

// 在组件中使用加载的数据
function Dashboard() {
  const data = useLoaderData();
  // 渲染数据...
}

3.4 路由配置元数据模式

const routes = [
  {
    path: '/',
    element: <Home />,
    public: true
  },
  {
    path: '/profile',
    element: <Profile />,
    guards: [authGuard]
  },
  {
    path: '/admin',
    element: <Admin />,
    guards: [authGuard, adminGuard]
  }
];

function GuardedRoutes() {
  return (
    <Routes>
      {routes.map((route) => (
        <Route
          key={route.path}
          path={route.path}
          element={
            <GuardProvider guards={route.guards || []}>
              {route.element}
            </GuardProvider>
          }
        />
      ))}
    </Routes>
  );
}

function GuardProvider({ children, guards }) {
  const navigate = useNavigate();
  const location = useLocation();
  
  useEffect(() => {
    for (const guard of guards) {
      const result = guard({ location, navigate });
      if (result?.redirect) {
        navigate(result.redirect, {
          replace: true,
          state: { from: location }
        });
        return;
      }
    }
  }, [guards, navigate, location]);
  
  return children;
}

四、常见守卫场景实现

4.1 认证守卫

function useAuthGuard(options = {}) {
  const { loginPath = '/login' } = options;
  const { isAuthenticated, isLoading } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  
  useEffect(() => {
    if (!isLoading && !isAuthenticated) {
      navigate(loginPath, {
        replace: true,
        state: { from: location }
      });
    }
  }, [isAuthenticated, isLoading, navigate, location, loginPath]);
  
  return { isAuthenticated, isLoading };
}

// 使用示例
function AuthGuard({ children }) {
  const { isLoading } = useAuthGuard();
  
  if (isLoading) return <LoadingSpinner />;
  
  return children;
}

4.2 角色权限守卫

function RoleGuard({ children, requiredRoles }) {
  const { user } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  
  useEffect(() => {
    if (user && !hasRequiredRoles(user.roles, requiredRoles)) {
      navigate('/forbidden', {
        replace: true,
        state: { from: location }
      });
    }
  }, [user, requiredRoles, navigate, location]);
  
  if (!user) return <LoadingSpinner />;
  
  return hasRequiredRoles(user.roles, requiredRoles) ? children : null;
}

// 辅助函数
function hasRequiredRoles(userRoles, requiredRoles) {
  return requiredRoles.some(role => userRoles.includes(role));
}

// 使用示例
<Route 
  path="/admin" 
  element={
    <RoleGuard requiredRoles={['admin', 'superadmin']}>
      <AdminPanel />
    </RoleGuard>
  }
/>

4.3 订阅状态守卫

function SubscriptionGuard({ children }) {
  const { subscription } = useUser();
  const navigate = useNavigate();
  
  useEffect(() => {
    if (subscription?.status === 'expired') {
      navigate('/renew-subscription', { replace: true });
    } else if (!subscription?.isActive) {
      navigate('/pricing', { replace: true });
    }
  }, [subscription, navigate]);
  
  return subscription?.isActive ? children : <LoadingSpinner />;
}

4.4 功能开关守卫

function FeatureGuard({ children, feature }) {
  const { isFeatureEnabled } = useFeatureFlags();
  const navigate = useNavigate();
  
  useEffect(() => {
    if (!isFeatureEnabled(feature)) {
      navigate('/feature-disabled', { replace: true });
    }
  }, [feature, isFeatureEnabled, navigate]);
  
  return isFeatureEnabled(feature) ? children : null;
}

五、高级守卫模式

5.1 组合守卫

function composeGuards(...guards) {
  return function CombinedGuard({ location, navigate }) {
    for (const guard of guards) {
      const result = guard({ location, navigate });
      if (result) return result;
    }
    return null;
  };
}

// 创建组合守卫
const adminAreaGuard = composeGuards(
  authGuard,
  adminGuard,
  subscriptionGuard
);

// 使用组合守卫
<Route 
  path="/admin" 
  element={
    <RouteGuard guard={adminAreaGuard}>
      <AdminPanel />
    </RouteGuard>
  }
/>

5.2 异步守卫

function AsyncGuard({ children, guard }) {
  const [isAllowed, setIsAllowed] = useState(null);
  const navigate = useNavigate();
  const location = useLocation();
  
  useEffect(() => {
    let isMounted = true;
    
    const checkGuard = async () => {
      try {
        const result = await guard({ location, navigate });
        if (isMounted) {
          if (result?.redirect) {
            navigate(result.redirect, {
              replace: true,
              state: { from: location }
            });
          } else {
            setIsAllowed(true);
          }
        }
      } catch (error) {
        if (isMounted) {
          navigate('/error', {
            state: { error: error.message },
            replace: true
          });
        }
      }
    };
    
    checkGuard();
    
    return () => {
      isMounted = false;
    };
  }, [guard, navigate, location]);
  
  if (isAllowed === null) return <LoadingSpinner />;
  return isAllowed ? children : null;
}

// 使用示例
const asyncAuthGuard = async () => {
  const isAuth = await checkAuthToken();
  return isAuth ? null : { redirect: '/login' };
};

<Route 
  path="/dashboard" 
  element={
    <AsyncGuard guard={asyncAuthGuard}>
      <Dashboard />
    </AsyncGuard>
  }
/>

5.3 条件重定向守卫

function ConditionalRedirect({ children, condition, to }) {
  const navigate = useNavigate();
  const location = useLocation();
  
  useEffect(() => {
    if (condition()) {
      navigate(to, {
        replace: true,
        state: { from: location }
      });
    }
  }, [condition, to, navigate, location]);
  
  return condition() ? null : children;
}

// 使用示例
<Route 
  path="/profile" 
  element={
    <ConditionalRedirect 
      condition={() => isMobile()} 
      to="/mobile/profile"
    >
      <DesktopProfile />
    </ConditionalRedirect>
  }
/>

六、实践与常见问题

6.1 路由守卫最佳实践

  1. 守卫顺序:先认证后权限,先通用后特殊
  2. 加载状态:异步检查时提供加载指示器
  3. 错误处理:妥善处理守卫中的错误
  4. 避免循环:确保守卫逻辑不会导致无限重定向
  5. 测试覆盖:为守卫编写单元测试和集成测试

6.2 常见问题解决方案

问题:无限重定向循环

// 登录页面守卫
function LoginPage() {
  const { isAuthenticated } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  
  useEffect(() => {
    if (isAuthenticated) {
      // 检查来源是否已经是登录页
      const from = location.state?.from?.pathname || '/';
      if (from !== '/login') {
        navigate(from, { replace: true });
      } else {
        navigate('/', { replace: true });
      }
    }
  }, [isAuthenticated, navigate, location]);
  
  // 渲染登录表单...
}

问题:守卫多次触发

function StableGuard({ children, guard }) {
  const navigate = useNavigate();
  const location = useLocation();
  const initialCheck = useRef(false);
  
  useEffect(() => {
    if (!initialCheck.current) {
      initialCheck.current = true;
      const result = guard({ location, navigate });
      if (result?.redirect) {
        navigate(result.redirect, {
          replace: true,
          state: { from: location }
        });
      }
    }
  }, [guard, navigate, location]);
  
  return children;
}

问题:组件卸载时导航

function SafeEffectGuard({ children, guard }) {
  const navigate = useNavigate();
  const location = useLocation();
  const isMounted = useRef(true);
  
  useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);
  
  useEffect(() => {
    const result = guard({ location, navigate });
    
    if (result?.redirect && isMounted.current) {
      navigate(result.redirect, {
        replace: true,
        state: { from: location }
      });
    }
  }, [guard, navigate, location]);
  
  return children;
}

七、案例:电商平台路由守卫

// 路由配置
const router = createBrowserRouter([
  {
    path: '/',
    element: <MainLayout />,
    children: [
      {
        index: true,
        element: <HomePage />
      },
      {
        path: 'product/:id',
        element: <ProductDetailPage />,
        loader: async ({ params }) => {
          const product = await fetchProduct(params.id);
          if (!product) throw new Error('Product not found');
          return product;
        },
        errorElement: <ProductErrorPage />
      },
      {
        path: 'cart',
        element: <CartPage />,
        guards: [authGuard]
      },
      {
        path: 'checkout',
        element: <CheckoutPage />,
        guards: [authGuard, cartNotEmptyGuard],
        loader: async () => {
          const [cart, addresses] = await Promise.all([
            fetchCart(),
            fetchAddresses()
          ]);
          return { cart, addresses };
        }
      },
      {
        path: 'admin',
        element: <AdminLayout />,
        guards: [authGuard, adminGuard],
        children: [
          {
            index: true,
            element: <AdminDashboard />
          },
          {
            path: 'products',
            element: <ProductManagement />
          }
        ]
      }
    ]
  },
  {
    path: '/login',
    element: <LoginPage />
  },
  {
    path: '*',
    element: <NotFoundPage />
  }
]);

// 购物车非空守卫
const cartNotEmptyGuard = ({ navigate }) => {
  const { cartItems } = useCart();
  
  if (cartItems.length === 0) {
    navigate('/cart', {
      state: { message: '请先添加商品到购物车' },
      replace: true
    });
    return { block: true };
  }
  
  return null;
};

// 管理员守卫
const adminGuard = ({ navigate }) => {
  const { user } = useAuth();
  
  if (!user?.roles.includes('admin')) {
    navigate('/forbidden', { replace: true });
    return { block: true };
  }
  
  return null;
};

八、总结

8.1 路由守卫关键点

  1. 守卫类型:进入守卫、离开守卫、数据守卫
  2. 实现模式:高阶组件、路由包装器、Loader API、元数据配置
  3. 常见场景:认证、权限、订阅状态、功能开关
  4. 高级模式:守卫组合、异步守卫、条件重定向

8.2 性能优化建议

  • 守卫复用:创建可复用的守卫组件
  • 按需加载:使用React.lazy进行代码分割
  • 缓存策略:缓存守卫检查结果
  • 取消请求:组件卸载时取消异步守卫操作
  • 并行加载:使用Promise.all并行执行多个守卫
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐