React Router

React Router

版本说明 : React Router V7 版本中,react-router-dom 在 npm 上已经被推荐使用 react-router 包。

目录

  1. [React Router 基础概念](#React Router 基础概念 "#1-react-router-%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5")
  2. 路由配置基础
  3. 路由配置列表
  4. 路由通配符与重定向
  5. 路由加载逻辑处理
  6. 权限控制与异步加载
  7. 创建路由对象
  8. 页面组件实现
  9. 路由跳转与导航
  10. 数据加载与错误处理
  11. 完整示例

1. React Router 基础概念

什么是 React Router?

React Router 是 React 应用中最流行的路由库,它允许你构建单页应用(SPA)中的客户端路由。通过 React Router,你可以:

  • 在 URL 变化时渲染不同的组件
  • 实现浏览器历史记录管理
  • 支持嵌套路由
  • 处理路由参数和查询字符串

核心概念

1.1 路由(Route)

路由定义了 URL 路径与组件之间的映射关系。

1.2 路由器(Router)

路由器是路由系统的核心,负责监听 URL 变化并渲染对应的组件。

1.3 导航(Navigation)

导航允许用户在应用的不同页面之间跳转。

1.4 嵌套路由(Nested Routes)

允许在一个路由组件内部定义子路由,形成路由层次结构。


2. 路由配置基础

2.1 路由配置接口定义

首先,我们需要定义一个路由配置的接口,用于类型安全和代码提示:

tsx 复制代码
import { redirect } from 'react-router';

// 路由配置接口
export interface RouterConfig {
  name: string;           // 路由名称,用于编程式导航
  path: string;           // 路由路径,支持动态参数
  component?: any;        // 路由对应的组件
  children?: RouterConfig[]; // 子路由配置
  meta?: {               // 路由元信息
    permission: string;   // 权限标识
    navName?: string;     // 导航显示名称
    title?: string;       // 页面标题
    icon?: string;        // 图标
  };
  loader?: () => ReturnType<typeof redirect>; // 路由加载器
  errorElement?: React.ComponentType; // 错误边界组件
}

2.2 路由路径语法

  • 静态路径 : /home - 精确匹配
  • 动态参数 : /:id - 匹配任意值,通过 useParams() 获取
  • 可选参数 : /:id? - 参数可选
  • 通配符 : * - 匹配所有路径,通常用于 404 处理

3. 路由配置列表

3.1 基础路由配置示例

下面是一个典型的多层级路由配置示例,展示了如何组织复杂的应用路由结构:

tsx 复制代码
import { redirect } from 'react-router';
import { lazy } from 'react';

// 路由页面配置列表,层级根据业务情况自定义
const routerConfig: RouterConfig[] = [
  {
    name: 'site',
    path: '/:site/:lang',  // 动态参数:site 和 lang
    component: lazy(() => import('@/App')), // 懒加载根组件
    children: [
      {
        name: 'main',
        path: 'main',  // 相对路径,完整路径为 /:site/:lang/main
        component: lazy(() => import('@/pages/Main')),
        children: [
          {
            name: 'home',
            path: 'home',  // 完整路径为 /:site/:lang/main/home
            component: lazy(() => import('@/pages/Home')),
            meta: {
              permission: 'home',  // 需要 home 权限
              navName: '首页',
              title: '首页'
            },
            children: [
              {
                name: 'lottie',
                path: 'lottie',  // 完整路径为 /:site/:lang/main/home/lottie
                component: lazy(() => import('@/pages/Lottie')),
                meta: {
                  permission: 'lottie',
                  navName: '动画页面',
                  title: 'Lottie 动画'
                },
              },
              {
                name: 'info',
                path: 'info',  // 完整路径为 /:site/:lang/main/home/info
                component: lazy(() => import('@/pages/Info')),
                meta: {
                  permission: 'info',
                  navName: '信息页面',
                  title: '信息详情'
                },
              }
            ]
          }
        ]
      },
      {
        name: 'login',
        path: 'login',  // 完整路径为 /:site/:lang/login
        component: lazy(() => import('@/pages/Login')),
        meta: {
          title: '登录页面'
        },
        children: [],  // 登录页面通常没有子路由
      }
    ]
  }
]

3.2 路由配置说明

  • 动态参数 : /:site/:lang 允许 URL 中包含动态的 site 和 lang 参数
  • 懒加载 : 使用 lazy() 函数实现组件的按需加载,提升应用性能
  • 嵌套路由 : 通过 children 属性实现路由的层级结构
  • 权限控制 : 通过 meta.permission 字段控制页面访问权限
  • 相对路径: 子路由的 path 是相对于父路由的路径

4. 路由通配符与重定向

4.1 通配符路由的作用

通配符路由(*)用于处理所有未匹配的路由,通常用于:

  • 404 页面处理
  • 默认重定向
  • 路由守卫和权限控制

4.2 重定向配置

routerConfig 数组的最后一项,添加 * 进行路由匹配重定向操作。当路由刷新、跳转操作时,匹配不到的路由地址,就会执行这里的 loader 函数:

tsx 复制代码
{
  name: 'redirect',
  path: '*',  // 通配符,匹配所有未定义的路由
  loader: () => {
    // 获取用户信息和状态
    const site = sessionStorage.getItem('site')
    const lang = sessionStorage.getItem('lang')
    const token = sessionStorage.getItem('token')
    
    // 根据登录状态进行重定向
    if (!token) {
      // 未登录用户重定向到登录页
      return redirect(`/${site}/${lang}/login`)
    }
    
    // 已登录用户重定向到默认页面
    return redirect(`/${site}/${lang}/main/home/lottie`)
  },
  children: []
}

4.3 重定向策略

可以根据不同的业务需求实现不同的重定向策略:

tsx 复制代码
// 更复杂的重定向逻辑示例
{
  name: 'redirect',
  path: '*',
  loader: () => {
    const site = sessionStorage.getItem('site')
    const lang = sessionStorage.getItem('lang')
    const token = sessionStorage.getItem('token')
    const userRole = sessionStorage.getItem('userRole')
    const lastVisited = sessionStorage.getItem('lastVisited')
    
    // 未登录用户
    if (!token) {
      return redirect(`/${site}/${lang}/login`)
    }
    
    // 根据用户角色重定向
    if (userRole === 'admin') {
      return redirect(`/${site}/${lang}/admin/dashboard`)
    }
    
    // 恢复用户上次访问的页面
    if (lastVisited) {
      return redirect(lastVisited)
    }
    
    // 默认重定向
    return redirect(`/${site}/${lang}/main/home`)
  },
  children: []
}

5. 路由加载逻辑处理

5.1 路由配置转换

封装函数,将自定义的路由配置转换成 createBrowserRouter 方法所接受的参数类型:

tsx 复制代码
import { createBrowserRouter, Outlet } from 'react-router';

/**
 * MakeRoutes 函数:将自定义路由配置转换为 React Router 标准格式
 * @param config 自定义路由配置数组
 * @returns React Router 标准路由配置
 */
function MakeRoutes(config: RouterConfig[]): Parameters<typeof createBrowserRouter>[0] {
  return config.map(({ path, component: Cmp, loader, children, meta }) => {
    // 情况1:没有提供组件(通常是布局组件或中间层)
    if (!Cmp) {
      return {
        path,
        loader,
        element: <Outlet />, // 渲染子路由的出口
        children: [...MakeRoutes(children || [])], // 递归处理子路由
      }
    }
    
    // 情况2:提供了组件,需要包装权限验证和异步加载
    return {
      path,
      loader,
      element: (
        // 权限验证包装器
        <PageRequireAuth permission={meta?.permission} key={String(Math.random())}>
          {/* 异步加载包装器 */}
          <SuspenseComponent>
            {/* 实际的路由组件 */}
            <Cmp />
          </SuspenseComponent>
        </PageRequireAuth>
      ),
      children: [...MakeRoutes(children || [])], // 递归处理子路由
    }
  })
}

5.2 函数说明

  • 递归处理 : 通过递归调用 MakeRoutes 处理嵌套路由
  • 条件渲染 : 根据是否有组件来决定渲染 <Outlet /> 还是实际组件
  • 权限包装: 每个路由组件都会被权限验证组件包装
  • 异步加载 : 使用 SuspenseComponent 处理懒加载组件的加载状态

6. 权限控制与异步加载

6.1 权限校验组件 (PageRequireAuth)

权限校验组件用于控制用户对特定页面的访问权限:

tsx 复制代码
import { ReactNode } from 'react';

// 权限组件接收的参数
interface AuthProps {
  permission?: string;  // 需要的权限标识
  children: ReactNode;  // 子组件
}

/**
 * 权限校验组件:根据用户权限控制页面渲染
 * @param permission 需要的权限标识
 * @param children 子组件
 * @returns 有权限时渲染子组件,无权限时显示无权限页面
 */
export function PageRequireAuth({ permission, children }: AuthProps) {
  // 从 sessionStorage 获取用户权限列表
  const permissions = JSON.parse(sessionStorage.getItem('permissions') || '[]')
  
  // 如果没有设置权限要求,直接渲染
  if (!permission || !permissions) {
    return <>{children}</>;
  }
  
  // 检查用户是否有对应权限
  if (permissions.includes(permission)) {
    return <>{children}</>;
  }
  
  // 无权限时显示无权限页面
  return (
    <div className="no-permission">
      <h2>无权限访问</h2>
      <p>您没有访问此页面的权限,请联系管理员。</p>
    </div>
  );
}

6.2 异步加载组件 (SuspenseComponent)

异步加载组件用于处理懒加载组件的加载状态:

tsx 复制代码
import { Suspense, ReactNode } from 'react';

// 异步加载组件的参数
interface SuspenseProps {
  children: ReactNode;
  fallback?: ReactNode; // 自定义加载状态
}

/**
 * 异步加载组件:为懒加载的组件提供加载状态
 * @param children 懒加载的子组件
 * @param fallback 加载时显示的组件
 * @returns 包装了 Suspense 的组件
 */
export function SuspenseComponent({ 
  children, 
  fallback = <div className="loading">Loading...</div> 
}: SuspenseProps) {
  return (
    <Suspense fallback={fallback}>
      {children}
    </Suspense>
  );
}

6.3 增强版权限控制

可以进一步扩展权限控制功能:

tsx 复制代码
// 更完善的权限控制组件
export function PageRequireAuth({ permission, children }: AuthProps) {
  const permissions = JSON.parse(sessionStorage.getItem('permissions') || '[]')
  const userRole = sessionStorage.getItem('userRole')
  
  // 管理员拥有所有权限
  if (userRole === 'admin') {
    return <>{children}</>;
  }
  
  // 检查具体权限
  if (!permission || permissions.includes(permission)) {
    return <>{children}</>;
  }
  
  // 可以重定向到无权限页面
  return <Navigate to="/no-permission" replace />;
}

6.4 自定义加载状态

可以创建更丰富的加载状态组件:

tsx 复制代码
// 自定义加载组件
const LoadingSpinner = () => (
  <div className="loading-container">
    <div className="spinner"></div>
    <p>页面加载中...</p>
  </div>
);

// 使用自定义加载状态
export function SuspenseComponent({ children }: SuspenseProps) {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      {children}
    </Suspense>
  );
}

7. 创建路由对象

7.1 路由创建函数

创建路由对象的核心函数,负责将配置转换为可用的路由器:

tsx 复制代码
import { createBrowserRouter, RouterProvider } from 'react-router';
import { Suspense } from 'react';

/**
 * 创建路由器的工厂函数
 * @param routerConfig 路由配置数组
 * @param basename 基础路径,会添加到所有路由前面
 * @returns 配置好的路由器实例
 */
const createRouter = (routerConfig: RouterConfig[], basename = '') => {
  // 将自定义配置转换为 React Router 标准格式
  const config = MakeRoutes(routerConfig)
  
  /*
   * 这里可以添加额外的配置和中间件:
   * - Sentry 错误上报
   * - 路由守卫
   * - 性能监控
   * - 根据环境加载不同路由
   */
  
  return createBrowserRouter(config, { 
    basename,
    // 可以添加更多配置选项
    future: {
      v7_startTransition: true, // 启用 React 18 的 startTransition
      v7_relativeSplatPath: true, // 改进相对路径处理
    }
  })
}

// 创建路由对象,basename 会添加到所有路由前面
export const router = createRouter(routerConfig, '/user')

// 导出路由渲染组件
export default function Routers() {
  return (
    <Suspense fallback={<div>应用加载中...</div>}>
      <RouterProvider router={router} />
    </Suspense>
  )
}

7.2 路由配置选项

createBrowserRouter 支持多种配置选项:

tsx 复制代码
const router = createBrowserRouter(config, {
  basename: '/app',           // 基础路径
  future: {
    v7_startTransition: true, // 使用 React 18 的并发特性
    v7_relativeSplatPath: true, // 改进相对路径处理
  },
  // 全局错误处理
  errorElement: <ErrorBoundary />,
  // 全局加载器
  loader: async () => {
    // 全局数据加载逻辑
    return null;
  }
});

7.3 环境相关路由配置

可以根据不同环境创建不同的路由配置:

tsx 复制代码
// 根据环境创建不同的路由配置
const createEnvironmentRouter = () => {
  const isDevelopment = process.env.NODE_ENV === 'development';
  const isProduction = process.env.NODE_ENV === 'production';
  
  let config = routerConfig;
  
  // 开发环境添加调试路由
  if (isDevelopment) {
    config = [
      ...config,
      {
        name: 'debug',
        path: '/debug',
        component: lazy(() => import('@/pages/Debug')),
        meta: { title: '调试页面' }
      }
    ];
  }
  
  // 生产环境移除调试功能
  if (isProduction) {
    config = config.filter(route => route.name !== 'debug');
  }
  
  return createRouter(config, '/user');
};

export const router = createEnvironmentRouter();

8. 页面组件实现

8.1 根组件 (App.tsx)

根组件是整个应用的入口,负责提供全局上下文和布局:

tsx 复制代码
import { Outlet } from 'react-router'
import { Helmet, HelmetData } from 'react-helmet-async';

/**
 * 页面标题组件
 * 线程安全:确保在服务器端进行异步操作时,Helmet的状态能够针对每个请求独立维护,从而保证线程安全
 * 状态封装:通过HelmetProvider组件为每个React组件树提供独立的状态,确保在服务器端渲染过程中安全地管理<head>标签中的内容
 * SEO优化:动态设置页面标题
 * 流式传输支持:支持流式渲染
 */
export function Title({ text }: { text: string }) {
    const helmetData = new HelmetData({});
    return (
        <Helmet helmetData={helmetData}>
            <title>{text}</title>
        </Helmet>
    );
}

/**
 * 应用根组件
 * 提供全局上下文、SEO支持和路由出口
 */
function App() {
  return (
    <>
      <Title text="我的应用" />
      {/* 
        创建全局上下文,用于:
        - 用户信息管理
        - 权限状态
        - 主题配置
        - 国际化设置
      */}
      <AuthContext>
        <Outlet />
      </AuthContext>
    </>
  )
}

export default App

8.2 布局组件 (Main.tsx)

布局组件提供页面结构框架:

tsx 复制代码
import { Outlet } from 'react-router';
import { ReactNode } from 'react';

/**
 * 主布局组件
 * 提供页面主体结构,包含导航、侧边栏等
 */
export default function Main() {
  return (
    <div className="main-layout">
      {/* 页面头部 */}
      <header className="main-header">

      </header>
      
      {/* 主要内容区域 */}
      <main className="main-content">
        <Outlet />
      </main>
      
      {/* 页面底部 */}
      <footer className="main-footer">

      </footer>
    </div>
  );
}

8.3 页面组件 (Home.tsx)

具体的页面组件实现:

tsx 复制代码
import { FC } from 'react';
import { Outlet, useNavigate, useParams } from 'react-router';

/**
 * 首页组件
 * 展示主要功能入口和子页面导航
 */
const Home: FC = () => {
  const navigate = useNavigate();
  const params = useParams(); // 获取路由参数
  
  // 退出登录
  const logout = () => {
    sessionStorage.removeItem('token');
    sessionStorage.removeItem('permissions');
    navigate('login');
  };
  
  // 导航到子页面
  const navigateToPage = (page: string) => {
    navigate(page);
  };
  
  return (
    <div className="home-page">
      <div className="home-header">
        <h2>欢迎来到首页</h2>
        <button onClick={logout} className="logout-btn">
          退出登录
        </button>
      </div>
      
      <div className="home-content">
        <div className="navigation-buttons">
          <button 
            onClick={() => navigateToPage('lottie')}
            className="nav-btn"
          >
            Lottie 动画
          </button>
          <button 
            onClick={() => navigateToPage('info')}
            className="nav-btn"
          >
            信息页面
          </button>
        </div>
        
        {/* 子路由出口 */}
        <div className="sub-content">
          <Outlet />
        </div>
      </div>
    </div>
  );
};

export default Home;

9. 路由跳转与导航

9.1 useNavigate Hook

useNavigate 是 React Router 提供的 Hook,用于编程式导航:

tsx 复制代码
import { useNavigate } from 'react-router';

const Home: FC = () => {
  const navigate = useNavigate();
  
  const logout = () => {
    // 相对路径导航
    navigate('login');
    
    // 绝对路径导航
    navigate('/user/site/zh/login');
    
    // 带状态导航
    navigate('/profile', { 
      state: { from: 'home' },
      replace: true // 替换当前历史记录
    });
    
    // 后退导航
    navigate(-1);
    
    // 前进导航
    navigate(1);
  };
  
  return <></>;
};

9.2 导航工具类

可以封装一个导航工具类,统一管理所有路由跳转:

tsx 复制代码
import { useNavigate } from 'react-router';

/**
 * 导航工具类
 * 统一管理应用中的所有路由跳转
 */
class NavigationService {
  private static navigate: ReturnType<typeof useNavigate> | null = null;
  
  // 设置导航函数
  static setNavigate(navigate: ReturnType<typeof useNavigate>) {
    this.navigate = navigate;
  }
  
  // 跳转到登录页
  static toLogin() {
    this.navigate?.('/login');
  }
  
  // 跳转到首页
  static toHome() {
    this.navigate?.('/main/home');
  }
  
  // 跳转到指定页面
  static toPage(path: string, options?: { replace?: boolean; state?: any }) {
    this.navigate?.(path, options);
  }
  
  // 后退
  static goBack() {
    this.navigate?.(-1);
  }
  
  // 前进
  static goForward() {
    this.navigate?.(1);
  }
}

// 在根组件中初始化
export function useNavigationService() {
  const navigate = useNavigate();
  NavigationService.setNavigate(navigate);
  return NavigationService;
}

使用 Link 组件进行声明式导航:

tsx 复制代码
import { Link, NavLink } from 'react-router';

// 基础 Link 组件
<Link to="/home">首页</Link>

// 带样式的 NavLink
<NavLink 
  to="/home" 
  className={({ isActive }) => isActive ? 'active' : ''}
>
  首页
</NavLink>

// 带状态的导航
<Link 
  to="/profile" 
  state={{ from: 'home' }}
>
  个人资料
</Link>

10. 数据加载与错误处理

10.1 Loader 数据加载

使用 loader 函数在路由渲染前加载数据:

tsx 复制代码
// 用户数据加载器
async function userLoader({ params }: { params: any }) {
  const userId = params.id;
  const user = await fetchUser(userId);
  
  if (!user) {
    throw new Response("用户不存在", { status: 404 });
  }
  
  return { user };
}

// 在路由配置中使用
{
  path: '/user/:id',
  component: UserProfile,
  loader: userLoader,
  errorElement: <UserError />
}

10.2 错误边界处理

为路由提供错误处理:

tsx 复制代码
import { useRouteError, isRouteErrorResponse } from 'react-router';

function ErrorBoundary() {
  const error = useRouteError();
  
  if (isRouteErrorResponse(error)) {
    return (
      <div className="error-page">
        <h1>{error.status}</h1>
        <p>{error.statusText}</p>
      </div>
    );
  }
  
  return (
    <div className="error-page">
      <h1>出错了!</h1>
      <p>发生了意外错误</p>
    </div>
  );
}

10.3 全局错误处理

在路由配置中添加全局错误处理:

tsx 复制代码
const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    errorElement: <ErrorBoundary />,
    children: [
      // 子路由...
    ]
  }
]);

11. 完整示例

11.1 入口文件使用

tsx 复制代码
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import Routers from './routers'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    {/* 使用导出的路由渲染组件 */}
    <Routers />
  </StrictMode>
)

11.2 完整路由配置

tsx 复制代码
import { createBrowserRouter, Outlet, redirect, RouterProvider } from 'react-router';
import { lazy, Suspense, FunctionComponent, ReactNode } from 'react';

export interface RouterConfig {
  name: string;
  path: string;
  component?: any;
  children?: RouterConfig[]
  meta?: {
    permission: string
    navName?: string
    title?: string
  }
  loader?: () => ReturnType<typeof redirect>
  errorElement?: React.ComponentType;
}

// 路由页面配置列表
const routerConfig: RouterConfig[] = [
  {
    name: 'site',
    path: '/:site/:lang',
    component: lazy(() => import('@/App')),
    children: [
      {
        name: 'main',
        path: 'main',
        component: lazy(() => import('@/pages/Main')),
        children: [
          {
            name: 'home',
            path: 'home',
            component: lazy(() => import('@/pages/Home')),
            meta: {
              permission: 'home',
              navName: '首页',
              title: '首页'
            },
            children: [
              {
                name: 'lottie',
                path: 'lottie',
                component: lazy(() => import('@/pages/Lottie')),
                meta: {
                  permission: 'lottie',
                  navName: '动画页面',
                  title: 'Lottie 动画'
                },
              },
              {
                name: 'info',
                path: 'info',
                component: lazy(() => import('@/pages/Info')),
                meta: {
                  permission: 'info',
                  navName: '信息页面',
                  title: '信息详情'
                },
              }
            ]
          }
        ]
      },
      {
        name: 'login',
        path: 'login',
        component: lazy(() => import('@/pages/Login')),
        meta: {
          title: '登录页面'
        },
        children: [],
      }
    ]
  },
  {
    name: 'redirect',
    path: '*',
    loader: () => {
      const site = sessionStorage.getItem('site')
      const lang = sessionStorage.getItem('lang')
      const token = sessionStorage.getItem('token')
      
      if (!token) {
        return redirect(`/${site}/${lang}/login`)
      }
      return redirect(`/${site}/${lang}/main/home/lottie`)
    },
    children: []
  }
]

// 权限验证组件
interface AuthProps {
  permission?: string
  children: ReactNode
}

export function PageRequireAuth({ permission, children }: AuthProps) {
  const permissions = JSON.parse(sessionStorage.getItem('permissions') || '[]')
  if (!permission || !permissions) return (<>{children}</>);
  if (permissions.includes(permission)) return (<>{children}</>);
  return <div>无权限</div>;
}

// 异步加载组件
export function SuspenseComponent({ children }: { children: ReactNode }) {
  return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>;
}

// 路由转换函数
function MakeRoutes(config: RouterConfig[]): Parameters<typeof createBrowserRouter>[0] {
  return config.map(({ path, component: Cmp, loader, children, meta }) => {
    if (!Cmp) return {
      path,
      element: <Outlet />,
      children: [...MakeRoutes(children || [])],
      loader
    }
    return {
      path,
      element: (
        <PageRequireAuth permission={meta?.permission} key={String(Math.random())}>
          <SuspenseComponent>
            <Cmp />
          </SuspenseComponent>
        </PageRequireAuth>
      ),
      children: [...MakeRoutes(children || [])],
      loader
    }
  })
}

// 创建路由
const createRouter = (routerConfig: RouterConfig[], basename = '') => {
  const config = MakeRoutes(routerConfig)
  return createBrowserRouter(config, { basename })
}

export const router = createRouter(routerConfig, '/user')

// 路由渲染组件
export default function Routers() {
  return (
    <Suspense>
      <RouterProvider router={router} />
    </Suspense>
  )
}
相关推荐
崔庆才丨静觅2 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了3 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅3 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅4 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊4 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax