React 路由守卫:前端安全与控制的魔法钥匙 🗝️

React 路由守卫初体验 🔍

想象一下:你写了个光鲜亮丽的后台管理系统,刚上线就被产品经理一顿吐槽 ------"游客都能直接访问管理员页面?这要是被黑客盯上,我可要背锅了!" 😱

这时候你才猛然想起:对啊!路由跳转的时候压根没做任何限制。用户只要知道 URL,就能随意访问任何页面,这可不太行。

路由守卫就是解决这个问题的 "保安大叔"------ 在用户访问某个页面之前,先拦下来问问:"你有通行证吗?"" 你有这个区域的权限吗?" 确认没问题了,才放你进去。

在 React 项目里,路由守卫几乎是必备功能。无论是防止未登录用户访问个人中心,还是阻止普通用户进入管理员后台,都离不开它。今天咱们就用最接地气的方式,把 React 路由守卫彻底讲明白!

什么是路由守卫 🤔

路由守卫的概念

路由守卫(Route Guards)本质上就是路由跳转的 "安检门" 。当用户从一个页面跳转到另一个页面时,它会先跳出来执行一些检查逻辑,再决定是放行(允许跳转)还是拦截(重定向到其他页面)。

你可以把它理解成小区门口的保安:

  • 外卖小哥想进小区?先登记(验证身份)
  • 装修工人要上楼?得有业主开具的通行证(权限检查)
  • 陌生人硬闯?不好意思请回(拦截并重定向)

核心功能大揭秘

路由守卫能做的事情可不少,主要有这几类:

  • 身份验证:最常用的功能!比如检测用户是否登录,没登录就踢回登录页。就像电影院检票 ------ 没票?先去售票窗口!
  • 权限控制:就算登录了,也不是什么页面都能进。普通用户不能进管理员页面,这就需要权限控制。
  • 数据预加载:有些页面需要先加载数据才能正常显示(比如详情页需要先获取 ID 对应的信息),可以用守卫在跳转前加载好数据。
  • 路由重定向 :比如把访问/的用户自动转到/home,或者把没有权限的用户转到/unauthorized页面。
  • 页面访问控制:比如某些页面只允许特定角色访问,或者在维护期间禁止所有人访问。

有了这些功能,你的应用才能既安全又好用 ------ 不会让游客看到付费内容,也不会让用户在等待数据加载时看到空白页面。

React Router 中的实现方式 🛠️

React Router 本身并没有内置 "路由守卫" 这个 API,但我们可以通过它提供的工具自己实现。常用的有三种方式,咱们一个个来看:

1. 高阶组件(HOC)方式

高阶组件(HOC)就像一个 "包装机器",把组件塞进去,加上守卫逻辑,再吐出来一个新的、带守卫功能的组件。

jsx 复制代码
// PrivateRoute.jsx
import { Navigate, useLocation } from "react-router-dom";

// 这个组件就是我们的"守卫包装机"
const PrivateRoute = ({ children, isAuthenticated }) => {
  // 获取当前访问的位置(路径)
  const location = useLocation();

  // 如果没登录,就重定向到登录页
  if (!isAuthenticated) {
    // 这里的state:{from: location}很重要------记录用户原本想去哪
    // 等登录成功后,就能直接跳回这个页面,体验超棒!
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  // 登录了就放行,显示原本要访问的内容
  return children;
};

// 使用方式:把需要保护的组件包起来
<Route
  path="/dashboard"
  element={
    <PrivateRoute isAuthenticated={isLoggedIn}>
      <Dashboard />
    </PrivateRoute>
  }
/>;

这种方式的优点是复用性强 ------ 写一次PrivateRoute,所有需要登录才能访问的页面都能用上。就像买了一个安检门,所有重要房间都能装。

2. 组件内守卫

有时候我们需要在组件内部做守卫检查(比如检查该用户是否有操作这个组件的权限),这时候就可以用组件内守卫。

jsx 复制代码
// Dashboard.jsx
import { useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom";

const Dashboard = () => {
  const navigate = useNavigate(); // 用于跳转路由
  const location = useLocation(); // 获取当前位置

  // 组件一加载就执行检查(依赖变化时也会执行)
  useEffect(() => {
    // 检查用户是否有权限访问dashboard
    if (!hasPermission("dashboard")) {
      navigate("/unauthorized", { replace: true });
      return; // 没权限就跳走,后面代码不执行
    }

    // 检查用户账号是否激活(比如是否验证邮箱)
    if (!isUserActive()) {
      navigate("/login", {
        state: { from: location }, // 同样记录原本想去的地方
        replace: true,
      });
    }
  }, [navigate, location]); // 依赖变化时重新检查

  // 所有检查通过,才显示内容
  return <div>Dashboard Content</div>;
};

这种方式的特点是针对性强------ 守卫逻辑和组件深度绑定,适合组件专属的检查逻辑。比如 Dashboard 组件需要检查 "仪表盘访问权限",Profile 组件需要检查 "个人信息修改权限",各自的守卫逻辑互不干扰。

3. 路由配置中的守卫

如果你的路由是集中配置的(比如用createBrowserRouter定义所有路由),可以直接在配置里写守卫逻辑。

jsx 复制代码
// router.jsx
import { createBrowserRouter, Navigate } from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />, // 布局组件(比如包含导航栏)
    children: [
      {
        path: "dashboard",
        // 直接在这里判断:登录了就显示Dashboard,否则跳登录页
        element: isAuthenticated ? <Dashboard /> : <Navigate to="/login" replace />
      },
      {
        path: "admin",
        // 检查是否有管理员角色:有就显示AdminPanel,否则跳无权限页
        element: hasAdminRole ? <AdminPanel /> : <Navigate to="/unauthorized" replace />
      },
    ],
  },
]);

这种方式的优点是直观------ 所有路由的守卫逻辑都集中在配置里,一眼就能看出哪个路由受什么条件保护。适合简单的守卫逻辑,如果逻辑复杂了,会让路由配置变得臃肿。

三种方式各有优缺点,实际开发中可以根据需求选择:简单的用配置守卫,通用的用 HOC 守卫,组件专属的用组件内守卫。

常见应用场景解析 🌰

知道了实现方式,咱们再看看实际开发中最常用的几个场景,把理论落地成代码:

1. 身份验证守卫

这是最常用的守卫 ------ 检查用户是否登录,没登录就不让进。咱们来写一个更完善的版本:

jsx 复制代码
// AuthGuard.jsx
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "../hooks/useAuth"; // 假设我们有个自定义的auth hook

const AuthGuard = ({ children, requiredRole = null }) => {
  // 从useAuth获取登录状态、用户信息、加载状态
  const { isAuthenticated, user, loading } = useAuth();
  const location = useLocation();

  // 加载中(比如正在检查token是否有效),显示加载提示
  if (loading) {
    return <div>Loading...</div>; // 这里可以换成好看的加载动画
  }

  // 没登录?去登录页
  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  // 如果需要特定角色,且用户角色不匹配?去无权限页
  if (requiredRole && user.role !== requiredRole) {
    return <Navigate to="/unauthorized" replace />;
  }

  // 所有检查通过,放行!
  return children;
};

使用的时候也很简单,想保护哪个组件就包哪个:

jsx 复制代码
// 只需要登录就能访问
<AuthGuard>
  <Profile />
</AuthGuard>

// 需要管理员角色才能访问
<AuthGuard requiredRole="admin">
  <AdminPanel />
</AuthGuard>

这个守卫不仅能检查登录状态,还能检查用户角色,相当于一个 "多功能安检门"------ 既能查门票,又能查 VIP 证件。

2. 权限守卫

有时候光有角色还不够,需要更细粒度的权限控制。比如同样是 "编辑" 角色,有的能发布文章,有的只能草稿。这时候就需要权限守卫:

jsx 复制代码
// PermissionGuard.jsx
import { Navigate } from "react-router-dom";
import { usePermissions } from "../hooks/usePermissions"; // 自定义权限hook

const PermissionGuard = ({ children, requiredPermissions = [] }) => {
  // 获取权限检查工具和加载状态
  const { hasPermission, loading } = usePermissions();

  // 权限检查中,显示提示
  if (loading) {
    return <div>Checking permissions...</div>;
  }

  // 检查是否拥有所有必需的权限
  const hasAllPermissions = requiredPermissions.every((permission) =>
    hasPermission(permission)
  );

  // 少一个权限都不行!
  if (!hasAllPermissions) {
    return <Navigate to="/unauthorized" replace />;
  }

  // 权限足够,放行
  return children;
};

使用示例:

jsx 复制代码
// 需要"文章发布"权限才能访问
<PermissionGuard requiredPermissions={["article:publish"]}>
  <ArticlePublisher />
</PermissionGuard>

// 需要同时拥有"评论查看"和"评论删除"权限
<PermissionGuard requiredPermissions={["comment:view", "comment:delete"]}>
  <CommentManager />
</PermissionGuard>

这种细粒度的权限控制,在大型应用中特别重要。就像公司里,不是所有员工都能进档案室,就算能进,也不是所有人都能删文件 ------ 权限守卫就是做这个的。

3. 数据预加载守卫

有些页面必须依赖数据才能显示。比如商品详情页,必须先获取商品 ID 对应的信息,否则页面就是空的。这时候可以用数据预加载守卫:

jsx 复制代码
// DataGuard.jsx
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

const DataGuard = ({ children, dataLoader }) => {
  const [data, setData] = useState(null); // 存储加载的数据
  const [loading, setLoading] = useState(true); // 加载状态
  const [error, setError] = useState(null); // 错误状态
  const navigate = useNavigate();

  useEffect(() => {
    // 定义加载数据的函数
    const loadData = async () => {
      try {
        // 调用传入的dataLoader函数(具体加载什么数据由外部决定)
        const result = await dataLoader();
        setData(result); // 保存数据
      } catch (err) {
        setError(err); // 记录错误
        navigate("/error", { replace: true }); // 加载失败跳错误页
      } finally {
        setLoading(false); // 无论成功失败,都结束加载状态
      }
    };

    loadData(); // 执行加载
  }, [dataLoader, navigate]);

  // 加载中显示提示
  if (loading) {
    return <div>Loading data...</div>;
  }

  // 有错误就不显示内容了(已经跳走了)
  if (error) {
    return null;
  }

  // 数据加载成功,把数据传给子组件(如果需要)
  return children(data);
};

使用示例:

jsx 复制代码
// 商品详情页的路由
<Route
  path="/product/:id"
  element={
    <DataGuard
      // 传入数据加载函数(获取商品详情)
      dataLoader={async () => {
        const id = useParams().id; // 获取URL中的商品ID
        const response = await fetch(`/api/products/${id}`);
        if (!response.ok) throw new Error("Failed to load product");
        return response.json();
      }}
    >
      {/* 数据加载成功后,通过参数接收数据并显示 */}
      {(product) => <ProductDetail product={product} />}
    </DataGuard>
  }
/>

这个守卫就像 "内容预热器"------ 在用户看到页面之前,先把需要的内容准备好,避免用户看到空白或错误的页面。用户体验直接提升一个档次!

最佳实践与技巧 💡

掌握了基本用法,咱们再看看高手是怎么用路由守卫的 ------ 几个最佳实践能让你的代码更优雅、更好维护。

1. 统一的路由守卫管理

如果一个页面需要多个守卫(比如先检查登录,再检查权限,最后加载数据),一个个嵌套会很麻烦:

jsx 复制代码
// 嵌套太多,看着就头大
<AuthGuard>
  <PermissionGuard>
    <DataGuard>
      <Dashboard />
    </DataGuard>
  </PermissionGuard>
</AuthGuard>

这时候可以写一个 "守卫组合器",把多个守卫合并成一个:

jsx 复制代码
// guards/index.js
// 创建一个函数,接收守卫数组,返回一个组合后的守卫组件
export const createRouteGuard = (guards = []) => {
  return ({ children }) => {
    // 用reduce把多个守卫"串联"起来
    return guards.reduce((acc, guard) => {
      // 前一个守卫的输出作为后一个守卫的输入
      return guard(acc);
    }, children);
  };
};

// 使用示例:组合多个守卫
const ProtectedRoute = createRouteGuard([
  (children) => <AuthGuard>{children}</AuthGuard>,
  (children) => <PermissionGuard requiredPermissions={["dashboard:read"]}>{children}</PermissionGuard>,
  (children) => <DataGuard dataLoader={loadDashboardData}>{children}</DataGuard>
]);

// 用的时候直接包一层就行,清爽多了!
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />

这种方式就像 "守卫流水线"------ 用户先过登录检查,再过权限检查,最后等数据加载,一步步来,逻辑清晰又好维护。

2. 路由元信息的巧用

如果你的路由很多,每个都写守卫逻辑会很重复。可以用 "路由元信息"(meta)来简化 ------ 把守卫需要的条件存在路由配置里,守卫自动读取这些条件:

jsx 复制代码
// routes.jsx
// 路由配置里加一个meta字段,存守卫需要的信息
const routes = [
  {
    path: "/dashboard",
    element: <Dashboard />,
    meta: {
      requiresAuth: true, // 需要登录
      requiredRole: "user", // 需要user角色
      permissions: ["dashboard:read"], // 需要dashboard:read权限
      title: "Dashboard" // 还能顺便存页面标题,一举多得
    },
  },
  {
    path: "/admin",
    element: <AdminPanel />,
    meta: {
      requiresAuth: true,
      requiredRole: "admin", // 需要admin角色
      permissions: ["admin:access"],
      title: "Admin Panel"
    },
  }
];

// 路由守卫组件:自动读取meta信息做检查
const RouteGuard = ({ route, children }) => {
  const { meta } = route; // 获取当前路由的meta信息

  // 如果需要登录且未登录,跳登录页
  if (meta?.requiresAuth && !isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  // 如果需要特定角色且角色不匹配,跳无权限页
  if (meta?.requiredRole && user.role !== meta.requiredRole) {
    return <Navigate to="/unauthorized" replace />;
  }

  // 其他检查...

  return children;
};

这种方式相当于给每个路由贴了个 "通行证要求" 标签,守卫只要照着标签检查就行 ------ 不用为每个路由写单独的检查逻辑,大大减少重复代码。

3. 异步路由守卫的处理

很多时候,守卫需要执行异步操作(比如调用 API 检查权限)。这时候要特别注意处理异步状态,避免出现 "闪屏"(用户短暂看到不该看的内容)。

jsx 复制代码
// AsyncRouteGuard.jsx
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

const AsyncRouteGuard = ({ children, guardFunction }) => {
  // 用null表示"还没检查完",true表示"可以访问",false表示"禁止访问"
  const [canAccess, setCanAccess] = useState(null);
  const navigate = useNavigate();

  useEffect(() => {
    // 异步检查函数
    const checkAccess = async () => {
      try {
        // 调用外部传入的异步守卫函数(比如API检查)
        const result = await guardFunction();
        setCanAccess(result); // 保存检查结果
      } catch (error) {
        // 出错了也禁止访问
        setCanAccess(false);
        navigate("/error", { replace: true });
      }
    };

    checkAccess(); // 执行检查
  }, [guardFunction, navigate]);

  // 还没检查完?显示加载提示(别让用户看到内容)
  if (canAccess === null) {
    return <div>Checking access...</div>;
  }

  // 检查不通过?不显示内容(已经跳走了)
  if (!canAccess) {
    return null;
  }

  // 检查通过,显示内容
  return children;
};

使用示例(检查用户是否有 VIP 权限):

jsx 复制代码
<AsyncRouteGuard
  guardFunction={async () => {
    // 调用API检查是否是VIP
    const response = await fetch("/api/user/is-vip");
    const data = await response.json();
    return data.isVip; // 返回true/false
  }}
>
  <VipContent />
</AsyncRouteGuard>

处理异步守卫的关键是:在结果出来之前,绝对不能显示受保护的内容 。就像安检还没结束,绝对不能让乘客进候车厅 ------ 异步守卫的canAccess === null状态就是这个作用。

实际项目中的应用案例 🏭

理论讲了这么多,咱们看看在实际项目中,这些守卫是怎么组合起来用的。

1. 登录状态检查(useAuth 钩子)

首先需要一个管理登录状态的工具,通常是一个自定义 hook:

jsx 复制代码
// hooks/useAuth.js
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";

export const useAuth = () => {
  const [isAuthenticated, setIsAuthenticated] = useState(false); // 是否登录
  const [user, setUser] = useState(null); // 用户信息
  const [loading, setLoading] = useState(true); // 加载状态
  const navigate = useNavigate();

  // 初始化时检查登录状态(比如刷新页面后)
  useEffect(() => {
    const checkAuth = async () => {
      try {
        // 从localStorage获取保存的token
        const token = localStorage.getItem("token");
        if (!token) {
          // 没token = 没登录
          setIsAuthenticated(false);
          return;
        }

        // 调用API验证token是否有效
        const response = await fetch("/api/auth/verify", {
          headers: { Authorization: `Bearer ${token}` },
        });

        if (response.ok) {
          // token有效,获取用户信息
          const userData = await response.json();
          setUser(userData);
          setIsAuthenticated(true);
        } else {
          // token无效,清除token
          localStorage.removeItem("token");
          setIsAuthenticated(false);
        }
      } catch (error) {
        // 网络错误等情况,也视为未登录
        setIsAuthenticated(false);
      } finally {
        // 无论结果如何,结束加载
        setLoading(false);
      }
    };

    checkAuth();
  }, []);

  // 登录函数(用户输入账号密码后调用)
  const login = async (credentials) => {
    try {
      const response = await fetch("/api/auth/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(credentials),
      });

      if (response.ok) {
        const { token, user } = await response.json();
        localStorage.setItem("token", token); // 保存token
        setUser(user);
        setIsAuthenticated(true);
        return true; // 登录成功
      } else {
        return false; // 登录失败
      }
    } catch (error) {
      return false;
    }
  };

  // 登出函数
  const logout = () => {
    localStorage.removeItem("token"); // 清除token
    setIsAuthenticated(false);
    setUser(null);
    navigate("/login"); // 跳回登录页
  };

  // 把需要的状态和方法返回出去
  return { isAuthenticated, user, loading, login, logout };
};

这个useAuth就像一个 "身份管理中心"------ 所有和登录相关的操作(检查状态、登录、登出)都由它负责。有了它,AuthGuard组件就能轻松获取登录状态了。

2. 完整路由配置示例

有了守卫组件和useAuth,就可以配置完整的路由了:

jsx 复制代码
// App.jsx
import { RouterProvider } from "react-router-dom";
import { router } from "./router";

const App = () => {
  // 用RouterProvider渲染路由
  return <RouterProvider router={router} />;
};

// router.jsx(核心路由配置)
import { createBrowserRouter } from "react-router-dom";
import { AuthGuard, PermissionGuard } from "./guards";
import Layout from "./Layout";
import Dashboard from "./pages/Dashboard";
import AdminPanel from "./pages/AdminPanel";
import Login from "./pages/Login";
import Unauthorized from "./pages/Unauthorized";

// 创建路由
const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />, // 共用的布局(导航栏+内容区)
    children: [
      // 首页(不需要守卫,所有人可访问)
      { path: "", element: <Home /> },

      // 仪表盘(需要登录+仪表盘访问权限)
      {
        path: "dashboard",
        element: (
          <AuthGuard>
            <PermissionGuard permissions={["dashboard:read"]}>
              <Dashboard />
            </PermissionGuard>
          </AuthGuard>
        ),
      },

      // 管理员面板(需要登录+管理员角色+管理员权限)
      {
        path: "admin",
        element: (
          <AuthGuard requiredRole="admin">
            <PermissionGuard permissions={["admin:access"]}>
              <AdminPanel />
            </PermissionGuard>
          </AuthGuard>
        ),
      },
    ],
  },

  // 登录页(不需要守卫,未登录用户访问)
  { path: "/login", element: <Login /> },

  // 无权限页(不需要守卫,被拒绝访问时跳转)
  { path: "/unauthorized", element: <Unauthorized /> },
]);

这个配置把前面讲的所有守卫都串起来了:

  • 未登录用户访问/dashboard?被AuthGuard拦下来,跳/login

  • 已登录但不是管理员的用户访问/admin?被AuthGuard(检查角色)拦下来,跳/unauthorized

  • 管理员但没有admin:access权限?被PermissionGuard拦下来,跳/unauthorized

  • 所有检查通过?顺利访问对应页面。

这样的路由配置,既安全又灵活,能应对大多数应用的需求。

注意事项与常见问题 ⚠️

路由守卫虽然好用,但如果用不好,可能会出现各种小问题。咱们来看看需要注意什么:

1. 避免无限重定向

这是最容易踩的坑!比如你给/login页面也加了AuthGuard,就会出现:

  • 未登录用户访问/dashboard → 被守卫跳/login

  • 访问/login时,因为未登录 → 又被守卫跳/login

  • 无限循环,页面崩溃!

解决办法很简单:登录页、注册页、无权限页这些 "公共跳转页",绝对不能加登录相关的守卫。就像医院的急诊通道,不能设置门禁 ------ 否则救护车都进不来了。

2. 性能优化要点

守卫里的逻辑会在路由跳转时执行,如果太复杂,会让跳转变慢。优化技巧:

  • 避免在守卫里做不必要的计算或 API 调用(比如重复检查同一个权限)。

  • useMemouseCallback缓存数据和函数(比如dataLoader函数可以缓存,避免每次渲染都创建新函数)。

  • 加载状态要简洁(别搞太复杂的动画,影响性能)。

简单说就是:守卫只做必要的检查,别在里面 "摸鱼" 做额外工作。

3. 用户体验与错误处理

用户被拦截时,一定要给明确的提示:

  • 加载时显示 "正在检查权限...",别让用户以为页面卡住了。

  • 被拒绝访问时,跳转到/unauthorized并显示 "您没有访问该页面的权限",别让用户一脸懵。

  • 数据加载失败时,显示 "加载失败,请重试",并提供重试按钮。

好的用户体验,就是让用户知道 "为什么被拦"、"该怎么办",而不是单纯地跳走。

4. 测试很重要

路由守卫涉及很多条件判断,一定要测试各种情况:

  • 未登录用户访问受保护页面 → 应该跳登录页。

  • 已登录用户访问受保护页面 → 应该正常显示。

  • 权限不足的用户访问 → 应该跳无权限页。

  • 刷新页面后,守卫是否还能正常工作 → (比如 token 存在时是否能保持登录状态)。

可以用 React Testing Library 写测试用例,也可以手动测试 ------ 总之别让守卫在生产环境掉链子。

总结 📝

路由守卫是 React 应用中控制页面访问的核心机制,就像应用的 "保安系统"------ 既能保护敏感内容,又能引导用户正确使用应用。

今天咱们从基础概念讲到实际应用,涵盖了:

  • 什么是路由守卫及其核心功能

  • 三种实现方式(HOC、组件内、路由配置)

  • 三大常见场景(身份验证、权限控制、数据预加载)

  • 最佳实践(守卫组合、元信息、异步处理)

  • 实际项目中的应用和注意事项

只要掌握了这些知识,你就能写出既安全又好用的路由守卫,让你的应用既不怕 "不速之客",又能给合法用户良好的体验。

最后送大家一句话:路由守卫的核心不是 "拦",而是 "引导"------ 拦对的人,引导对的人去对的地方。希望你写的守卫,既能守护应用安全,又能让用户感觉顺畅自然! 🚀

相关推荐
帅夫帅夫24 分钟前
深入理解 JWT:结构、原理与安全隐患全解析
前端
Struggler28133 分钟前
google插件开发:如何开启特定标签页的sidePanel
前端
爱编程的喵1 小时前
深入理解JSX:从语法糖到React的魔法转换
前端·react.js
代码的余温1 小时前
CSS3文本阴影特效全攻略
前端·css·css3
AlenLi1 小时前
JavaScript - 策略模式在开发中的应用
前端
xptwop1 小时前
05-ES6
前端·javascript·es6
每天开心1 小时前
告别样式冲突:CSS 模块化实战
前端·css·代码规范
wxjlkh1 小时前
powershell 批量测试ip 端口 脚本
java·服务器·前端
海底火旺1 小时前
单页应用路由:从 Hash 到懒加载
前端·react.js·性能优化
每天开心1 小时前
噜噜旅游App(3)——打造个性化用户中心:AI生成头像与交互设计
前端·前端框架